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 1j31tV-0001YM-Kg for psycopg@arkaria.postgresql.org; Sat, 15 Feb 2020 18:10:21 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.89) (envelope-from ) id 1j31tU-0007xe-65 for psycopg@arkaria.postgresql.org; Sat, 15 Feb 2020 18:10:20 +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 1j31tT-0007xX-Hi for psycopg@lists.postgresql.org; Sat, 15 Feb 2020 18:10:19 +0000 Received: from mail-io1-f51.google.com ([209.85.166.51]) by makus.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.92) (envelope-from ) id 1j31tQ-00028z-Cw for psycopg@postgresql.org; Sat, 15 Feb 2020 18:10:18 +0000 Received: by mail-io1-f51.google.com with SMTP id z8so2582139ioh.0 for ; Sat, 15 Feb 2020 10:10:16 -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=7PegzKNTprsk2AaJyIIY46NWFH1va53qo+pY+waQVc0=; b=dk6S91C2fJzzf0Q13GBaD0RwJaGAveY8caFMcXbOeVfMjfXKU4hJfuhjGno89oSU8L bIc5+9oE/zRYVYbZ9zh/QmcrH6SXhxxWjes50EFoOnMHiGxXjXnGC3CicJt9nXw51Ghb XtGQYYvGDFnORn3FnviRlDtnrhYSfCyLL7oMPWg9EQ71uFCod2Csi8kxocD7n+VI1+d4 q19ag8dU3S8sZGGliyF89QvKUrGknfMQSNurIJ6qYS+epQW6erKKMpel0MOfEWDqi9Em x6Ez2mHH5DxorZmpCbUmxA/+rjggr5fl2GFziUwRyxor5AuosZc4VuyIHWFiAzYN5crI SjWQ== X-Gm-Message-State: APjAAAXUlGCP0Vds/z2llLzah5/PFR6YrxbFELk3/ExM+Q2MRP6DP2Sx HRyr1cmIJJ808LMfQ8z2pC2iGJYLJ7RbV5OepMmbsrhv X-Google-Smtp-Source: APXvYqz15l6KG17GMPYzkQBbm8E4rMFqJCvGyyXKKN7C48yoJR7LsSbahTS4Gg8hErXGckQ6JeVJY7sY26Gr81exGN0= X-Received: by 2002:a02:9f06:: with SMTP id z6mr6778896jal.2.1581790213612; Sat, 15 Feb 2020 10:10:13 -0800 (PST) MIME-Version: 1.0 References: In-Reply-To: From: Daniel Fortunov Date: Sat, 15 Feb 2020 17:55:49 +0100 Message-ID: Subject: Re: Nested transactions support for code composability To: psycopg@postgresql.org Content-Type: multipart/alternative; boundary="000000000000caa85a059ea13b1d" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Precedence: bulk --000000000000caa85a059ea13b1d 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 Do people think this is 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! > --000000000000caa85a059ea13b1d 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 Transac= tion context manager that provides what I think is the most intuitive way t= o deal with transactions. (Functionally similar to xact / Django atomic, bu= t available directly on psycopg2 connections)

= We have been using this in production for over a year in a fairly large cod= ebase that does a lot of database interactions directly using psycopg2 (wit= hout 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 fr= equent unexpected pitfalls, and does not lend itself to code composability)= with a context manager that provides:
1. Seamless support for ne= sted transactions (implemented using Postgres savepoints).
2. Inn= er transactions are truly independent (meaning that a unit of code can deci= de whether to commit or rollback its own changes, without influencing any o= uter transaction(s), or even needing to be aware if outer transactions exis= t 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 emai= l to this list (below).

For details on the library= , see the README at https://github.com/asqui/psycopg-nestedtransacti= ons

Do people think this is worthy of inclusio= n in psycopg2.extras (or elsewhere in psycopg2?)

T= hanks in advance,
Daniel Fortunov


<= div class=3D"gmail_quote">
On Mon, 16 = Jan 2017 at 23:26, Daniel Fortunov <psycopg-list@danielfortunov.com> wr= ote:
I'd like to implement support for nested transactions in psy= copg2 using a context manager that internally uses postgres savepoints to i= mplement 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 solici= t 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 th= is email describes what I'm trying to achieve, using some motivating ex= amples.

Please share your thoughts and suggest= ions.

Thanks in advance,
Daniel Fortunov=

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

Suppo= se we have a database storing data about widgets, and we'd like to impl= ement a function that does some updates on a few related widget tables. We&= #39;d like to do this atomically, so we wrap it in a transaction.

Let's assume that `Transaction` is a psycopg transactio= n context manager with support for nested transactions. It begins a transac= tion on __enter__, commits it on __exit__, and rolls back if there is an ex= ception. (This is a simplified version of what I propose to implement.)

def update_widget(cxn, widget):
=C2=A0 =C2= =A0 with Transaction(cxn):
=C2=A0 =C2=A0 =C2=A0 =C2=A0 cur =3D cx= n.cursor()
=C2=A0 =C2=A0 =C2=A0 =C2=A0 cur.execute(...) =C2=A0# U= pdate 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 '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):
=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)
=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 cont= inue with other widgets

The Transaction conte= xt manager would implement the begin/commit/rollback transaction semantics = by beginning/releasing/rolling back to a savepoint, respectively. In this c= ontext, update_widget would seamlessly switch to using savepoints is it is = not the outermost transaction.

This is not p= ossible to achieve with the 'autocommit' paradigm, because you need= the ability to rollback the update of a single widget, and then continue t= he transaction that updates the rest. It is possible to implement with expl= icit savepoints, however that requires maintain_widget() to be updated, and= breaks existing code, because it will no longer be possible to call mainta= in_widget outside of a transaction!

Now I want to = take it a level further, and write a database integration test for my widge= t 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 th= e test is running in an interactive debugger and the I abort the debugging = session! :-)

def test_maintain_widget_successful_r= un(self):
=C2=A0 =C2=A0 with Transaction(cxn) as txn:
= =C2=A0 =C2=A0 =C2=A0 =C2=A0 # Set up starting database state
=C2= =A0 =C2=A0 =C2=A0 =C2=A0 # ....
=C2=A0 =C2=A0 =C2=A0 =C2=A0 maint= aint_widgets(cxn) =C2=A0# Call code under test
=C2=A0 =C2=A0 =C2= =A0 =C2=A0 # Check database state against expectations
=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 COMMI= T at any point. Which is why this breaks down completely with the autocommi= t =3D False; cxn.commit() paradigm, because we'd be calling commit() ex= plicitly all over the place!

With the current para= digm you're forced to manage transactions at the uppermost level, which= means callers to maintain_widgets() are obliged to call it within a pre-ex= isting 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&qu= ot; 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 compos= ability and transactions? Is this a terrible idea? (if so, why?)
=
Feedback and comments welcome.
Thanks for reading = this far!
--000000000000caa85a059ea13b1d--