Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_CBC_SHA1:256) (Exim 4.89) (envelope-from ) id 1j33HC-0003tg-07 for psycopg@arkaria.postgresql.org; Sat, 15 Feb 2020 19:38:54 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.89) (envelope-from ) id 1j33H9-00022U-Hi for psycopg@arkaria.postgresql.org; Sat, 15 Feb 2020 19:38:51 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_CBC_SHA1:256) (Exim 4.89) (envelope-from ) id 1j33H9-00022N-2S for psycopg@lists.postgresql.org; Sat, 15 Feb 2020 19:38:51 +0000 Received: from mail-il1-f170.google.com ([209.85.166.170]) by makus.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.92) (envelope-from ) id 1j33H2-0002r4-DH for psycopg@postgresql.org; Sat, 15 Feb 2020 19:38:49 +0000 Received: by mail-il1-f170.google.com with SMTP id l4so10971088ilj.1 for ; Sat, 15 Feb 2020 11:38:44 -0800 (PST) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:mime-version:references:in-reply-to:from:date :message-id:subject:to; bh=E3xvnFVZGHSJPDiBDRPfYO7UNfnsaop0FiQW74wO/eA=; b=ahmNeO8YKfCmC/BAPXTfz446/SQwXd5xGmdKgelEe1aF9O8BZ9V6s2UubH3AI4HK19 Yc+7a611gkFQPhaC1Df/PAzaMN3g+7BqP2IV6SefM7oeSOLomo+lsG8Si9wtNdUSNMej 2FjZtMe1eNrKmVKiL4/snpUkbnZ5vgt1I7IqeOK0/JBqkthuLOO+XA755tjrj4GgeNkJ lRsec1vIT3YW/aYQSmF+7A2I1LK/C22v3uZBI/tjV3QekWyKBxGDADW1JsATYaAw2aaz FubSfNkPJojdxiL+e6OqH9fZlcyTREu8g3kwhXUHE3rQWoeNWKqYCFlfCGScD/HFbQQx 4tRg== X-Gm-Message-State: APjAAAWyqaygdUbIri7VA8EHBkFKJ1saArQCy2RZ8i2VtgPVEKuOEGST 3pPTyJGT18Zz/mYZbBEnuv3nK/BsXtGAXpt+rJB5MSbY X-Google-Smtp-Source: APXvYqygLmIR9Fden4MjU47ZJjzV9bXSR0TL5A+/WF0QjNutrmhwWGnRJYFLlJ3gsmdwopcOwPMBTAsHi/k3iTql2LE= X-Received: by 2002:a92:5cc9:: with SMTP id d70mr8189244ilg.210.1581795523365; Sat, 15 Feb 2020 11:38:43 -0800 (PST) MIME-Version: 1.0 References: In-Reply-To: From: Daniel Fortunov Date: Sat, 15 Feb 2020 19:38:32 +0000 Message-ID: Subject: Nested transactions support for code composability To: psycopg@postgresql.org Content-Type: multipart/alternative; boundary="0000000000004711d0059ea27881" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Precedence: bulk --0000000000004711d0059ea27881 Content-Type: text/plain; charset="UTF-8" Based on the motivations outlined in the below message to this list (3 years ago), I have implemented a Transaction context manager that provides what I think is the most intuitive way to deal with transactions. (Functionally similar to xact / Django atomic, but available directly on psycopg2 connections) We have been using this in production for over a year in a fairly large codebase that does a lot of database interactions directly using psycopg2 (without any other libraries layered on top). I'd always though that this would be a good candidate for inclusion in psycopg2, and now that it is stable, I'd like to solicit feedback. This Transaction context manager replaces the DB-API model of handling transactions (which I have found to be not very intuitive, with frequent unexpected pitfalls, and does not lend itself to code composability) with a context manager that provides: 1. Seamless support for nested transactions (implemented using Postgres savepoints). 2. Inner transactions are truly independent (meaning that a unit of code can decide whether to commit or rollback its own changes, without influencing any outer transaction(s), or even needing to be aware if outer transactions exist or not) 3. Transaction contexts function correctly inside "classic" transaction management, meaning that you can implement this incrementally, starting at the innermost libraries and working outward. For a motivating example, please see my original email to this list (below). For details on the library, see the README at https://github.com/asqui/psycopg-nestedtransactions Is this worthy of inclusion in psycopg2.extras (or elsewhere in psycopg2?) Thanks in advance, Daniel Fortunov On Mon, 16 Jan 2017 at 23:26, Daniel Fortunov < psycopg-list@danielfortunov.com> wrote: > I'd like to implement support for nested transactions in psycopg2 using a > context manager that internally uses postgres savepoints to implement the > ability to nest transactions within each other, with sensible commit and > rollback semantics. > > The end goal is to allow code composability through support for nested > transactions. > > Before I go ahead and implement this I wanted to solicit feedback on the > suggestion, because I find it hard to imagine I'm the first person to dream > up this idea, so I imagine I'm missing something. (Perhaps it's already > implemented somewhere that I've not found, or maybe it's not implemented > because it's a terrible idea, and if so I'd like to understand why!) > > The rest of this email describes what I'm trying to achieve, using some > motivating examples. > > Please share your thoughts and suggestions. > > Thanks in advance, > Daniel Fortunov > > ------------------ > > Suppose we have a database storing data about widgets, and we'd like to > implement a function that does some updates on a few related widget tables. > We'd like to do this atomically, so we wrap it in a transaction. > > Let's assume that `Transaction` is a psycopg transaction context manager > with support for nested transactions. It begins a transaction on __enter__, > commits it on __exit__, and rolls back if there is an exception. (This is a > simplified version of what I propose to implement.) > > def update_widget(cxn, widget): > with Transaction(cxn): > cur = cxn.cursor() > cur.execute(...) # Update table A > cur.execute(...) # Update table B > cur.execute(...) # Update table C > > So far this works fine, and is also currently achievable with the > 'autocommit' paradigm. Nothing new here. Anyone can call this code, provide > a connection, and get an atomic update of tables A, B, and C. > > Now, I'd like to write a maintenance job that updates a whole load of > widgets. If the updates on any one widget fail, I'd still like the job to > continue, and process as many widgets as it can. Specifically, I want to > implement this job _without_ changing the existing maintain_widget() > function, which is already in use. > > maintain_widgets(cxn): > with Transaction(cxn): > for widget in widgets: > try: > update_widget(cxn, widget) > except: > pass # log exception and continue with other widgets > > The Transaction context manager would implement the begin/commit/rollback > transaction semantics by beginning/releasing/rolling back to a savepoint, > respectively. In this context, update_widget would seamlessly switch to > using savepoints is it is not the outermost transaction. > > This is not possible to achieve with the 'autocommit' paradigm, because > you need the ability to rollback the update of a single widget, and then > continue the transaction that updates the rest. It is possible to implement > with explicit savepoints, however that requires maintain_widget() to be > updated, and breaks existing code, because it will no longer be possible to > call maintain_widget outside of a transaction! > > Now I want to take it a level further, and write a database integration > test for my widget maintainer script, which executes the test scenario > within a transaction, guaranteeing that the database is left untouched > after my test, regardless of whether the test succeeds, fails, or aborts > unexpectedly (e.g. maybe the test is running in an interactive debugger and > the I abort the debugging session! :-) > > def test_maintain_widget_successful_run(self): > with Transaction(cxn) as txn: > # Set up starting database state > # .... > maintaint_widgets(cxn) # Call code under test > # Check database state against expectations > # ... > txn.rollback() # Rollback all changes > > Note that for this final ROLLBACK to work, none of the called code is > allowed to execute COMMIT at any point. Which is why this breaks down > completely with the autocommit = False; cxn.commit() paradigm, because we'd > be calling commit() explicitly all over the place! > > With the current paradigm you're forced to manage transactions at the > uppermost level, which means callers to maintain_widgets() are obliged to > call it within a pre-existing transaction in order to get correct behaviour > (namely, atomic update of all three tables in the face of errors). And even > then, this precludes having a test one level above, which executes that > "uppermost level" code within a transaction and then rolls everything back! > > So, what am I missing? Am I the first to have this crazy idea? Is this > already implemented? How do others solve the problem of code composability > and transactions? Is this a terrible idea? (if so, why?) > > Feedback and comments welcome. > Thanks for reading this far! > --0000000000004711d0059ea27881 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
Based on the motivations outlined in the below message to this list= (3 years ago), I have implemented a Transaction context manager that provi= des what I think is the most intuitive way to deal with transactions. (Func= tionally similar to xact / Django atomic, but available directly on psycopg= 2 connections)

We have been using this in prod= uction for over a year in a fairly large codebase that does a lot of databa= se interactions directly using psycopg2 (without any other libraries layere= d on top).

I'd always though that this would b= e a good candidate for inclusion in psycopg2, and now that it is stable, I&= #39;d like to solicit feedback.

This Transaction c= ontext manager replaces the DB-API model of handling transactions (which I = have found to be not very intuitive, with frequent unexpected pitfalls, and= does not lend itself to code composability) with a context manager that pr= ovides:
1. Seamless support for nested transactions (implemented = using Postgres savepoints).
2. Inner transactions are truly indep= endent (meaning that a unit of code can decide whether to commit or rollbac= k its own changes, without influencing any outer transaction(s), or even ne= eding to be aware if outer transactions exist or not)
3. Transact= ion contexts function correctly inside "classic" transaction mana= gement, meaning that you can implement this incrementally, starting at the = innermost libraries and working outward.

For a mot= ivating example, please see my original email to this list (below).

For details on the library, see the README at http= s://github.com/asqui/psycopg-nestedtransactions

Is this worthy of inclusion in psycopg2.extras (or elsewhere in psycopg2?= )

Thanks in advance,
Daniel Fortunov


On Mon, 16 Jan 2017 at 23:26, Daniel Fortunov <psycopg-list@= danielfortunov.com> wrote:
I'd like to implement support f= or nested transactions in psycopg2 using a context manager that internally = uses postgres savepoints to implement the ability to nest transactions with= in each other, with sensible commit and rollback semantics.
<= br>
The end goal is to allow code composability through support f= or nested transactions.

Before I go ahead and impl= ement this I wanted to solicit feedback on the suggestion, because I find i= t hard to imagine I'm the first person to dream up this idea, so I imag= ine I'm missing something. (Perhaps it's already implemented somewh= ere that I've not found, or maybe it's not implemented because it&#= 39;s a terrible idea, and if so I'd like to understand why!)
=
The rest of this email describes what I'm trying to achi= eve, using some motivating examples.

Please sh= are your thoughts and suggestions.

Thanks in advan= ce,
Daniel Fortunov

------------------

Suppose we have a database storing data about widge= ts, and we'd like to implement a function that does some updates on a f= ew related widget tables. We'd like to do this atomically, so we wrap i= t in a transaction.

Let's assume that `Transac= tion` is a psycopg transaction context manager with support for nested tran= sactions. It begins a transaction on __enter__, commits it on __exit__, and= rolls back if there is an exception. (This is a simplified version of what= I propose to implement.)

def update_widget(cxn, w= idget):
=C2=A0 =C2=A0 with Transaction(cxn):
=C2=A0 =C2= =A0 =C2=A0 =C2=A0 cur =3D cxn.cursor()
=C2=A0 =C2=A0 =C2=A0 =C2= =A0 cur.execute(...) =C2=A0# Update table A
=C2=A0 =C2=A0 =C2=A0 = =C2=A0 cur.execute(...) =C2=A0# Update table B
=C2=A0 =C2=A0 = =C2=A0 =C2=A0 cur.execute(...) =C2=A0# Update table C

So far this works fine, and is also currently achievable with the &= #39;autocommit' paradigm. Nothing new here. Anyone can call this code, = provide a connection, and get an atomic update of tables A, B, and C.
=

Now, I'd like to write a maintenance job that updat= es a whole load of widgets. If the updates on any one widget fail, I'd = still like the job to continue, and process as many widgets as it can. Spec= ifically, I want to implement this job _without_ changing the existing main= tain_widget() function, which is already in use.

m= aintain_widgets(cxn):
=C2=A0 =C2=A0 with Transaction(cxn):
=C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0for widget in widgets:
= =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0try:
=C2=A0 =C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0update_widget(cxn, widget)<= /div>
=C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0 =C2=A0=C2=A0=C2=A0except:
=
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0pass =C2= =A0# log exception and continue with other widgets

The Transaction context manager would implement the begin/commit/roll= back transaction semantics by beginning/releasing/rolling back to a savepoi= nt, respectively. In this context, update_widget would seamlessly switch to= using savepoints is it is not the outermost transaction.
<= br>
This is not possible to achieve with the 'autocommit'= paradigm, because you need the ability to rollback the update of a single = widget, and then continue the transaction that updates the rest. It is poss= ible to implement with explicit savepoints, however that requires maintain_= widget() to be updated, and breaks existing code, because it will no longer= be possible to call maintain_widget outside of a transaction!
Now I want to take it a level further, and write a database in= tegration test for my widget maintainer script, which executes the test sce= nario within a transaction, guaranteeing that the database is left untouche= d after my test, regardless of whether the test succeeds, fails, or aborts = unexpectedly (e.g. maybe the test is running in an interactive debugger and= the I abort the debugging session! :-)

def test_m= aintain_widget_successful_run(self):
=C2=A0 =C2=A0 with Transacti= on(cxn) as txn:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 # Set up starting dat= abase state
=C2=A0 =C2=A0 =C2=A0 =C2=A0 # ....
=C2=A0 = =C2=A0 =C2=A0 =C2=A0 maintaint_widgets(cxn) =C2=A0# Call code under test
=C2=A0 =C2=A0 =C2=A0 =C2=A0 # Check database state against expectat= ions
=C2=A0 =C2=A0 =C2=A0 =C2=A0 # ...
=C2=A0 =C2=A0 = =C2=A0 =C2=A0 txn.rollback() =C2=A0# Rollback all changes

Note that for this final ROLLBACK to work, none of the called code = is allowed to execute COMMIT at any point. Which is why this breaks down co= mpletely with the autocommit =3D False; cxn.commit() paradigm, because we&#= 39;d be calling commit() explicitly all over the place!

With the current paradigm you're forced to manage transactions at= the uppermost level, which means callers to maintain_widgets() are obliged= to call it within a pre-existing transaction in order to get correct behav= iour (namely, atomic update of all three tables in the face of errors). And= even then, this precludes having a test one level above, which executes th= at "uppermost level" code within a transaction and then rolls eve= rything back!

So, what am I missing? Am I the firs= t to have this crazy idea? Is this already implemented? How do others solve= the problem of code composability and transactions? Is this a terrible ide= a? (if so, why?)

Feedback and comments welcome.
Thanks for reading this far!
--0000000000004711d0059ea27881--