-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Problematic rollback semantics with nested transactions #2767
Comments
I am not going to change the semantics of the In other words, if you do not want the exception to cause a rollback, catch it inside the nested Alternatively, you can implement your own transactional context manager if you prefer to only roll-back the outermost, but I think that's problematic since an unhandled exception in a transaction should imply that the whole transaction should be aborted (since there is, effectively, only one transaction). I'm struggling to think of a real-world scenario where this problem would actually occur. |
Sure. An API unit test is wrapped in a transaction/atomic, doesn't matter. The test setup involves fixturing a user. The endpoint, for resetting a password, has code like: with db.transaction():
try:
code = UserResetCode.verify_code_with_token(token)
except UserResetCode.Unusable:
return error_response()
code.user.verify_email().save() In the model: class UserResetCode:
@classmethod
def verify_code_with_token(cls, token):
code = cls.find_usable(token=token)
with db.transaction():
if not code:
raise Unusable()
# Mark the code used, etc The
That's what I'm trying to do. However I can't actually manage internal nesting, because it leaks and rolls back transactions opened up by outer contexts.
Right, and I would, but I don't want automatic savepoints. Why not make the savepoint behavior of EDIT (8/16/2023T10:50:00 Pacific): Fixed the example, there was a nested transaction in the verify method I originally missed. |
I'm still struggling to see it. It doesn't quite make sense to me, e.g., here:
Why wouldn't that exception be raised outside the transaction block? The current semantics are such that raising an unhandled exception within a So, with all this being said, the peewee docs are quite clear: I don't intend to change it, as other people may rely on the current functionality -- and it should be quite easy to write your own context-manager subclass if you prefer the semantics you described in the original comment. Nor am I aware of any downsides to permitting savepoints to be used via the |
It gets handled at the endpoint level, and an appropriate error is returned.
Sure, and when I'm programming in Go I don't use exceptions for flow control, but we're dealing with a language featuring
This feels like depending on undefined C compiler behavior but ok. I think you can easily define the behavior, though- inner transactions are no-ops, only the outermost transaction does does anything. This is how all the other similar mechanisms in other languages I've used work (I believe it's also this way in other Python frameworks but can't say for sure). Python, maybe because Django did it, seems to be the only one with
Are you saying adding a |
I think you misunderstood the main issue. Consider this example. If this code is run with the changes you proposed, then any queries executed after the db = PostgresqlDatabase('peewee_test')
class Reg(db.Model):
key = CharField(primary_key=True)
db.create_tables([Reg])
with db.transaction() as tx:
Reg.create(key='k1')
try:
with db.transaction() as tx2:
Reg.create(key='k1')
except IntegrityError:
pass
print(len(Reg)) # Cannot do this, transaction state is messed-up. With the current Peewee code, the above code runs and produces an output of Neither of these seem quite correct. That is why it is strongly recommended to use |
Maybe we should take a step back and look at the what I believe we can agree is a valid use case, and that there's no way to achieve it with peewee's transaction semantics (at least that I can find). As a user of this library, I would like to be able to reason about a single transaction boundary, and any errors that reach said boundary will result in a rollback. This boundary respects both internal error handling (raising and catching of Python exceptions not having to do with the database) and database error handling (errors result in an aborted transaction). With the current behavior, there is no way to achieve this- Python exceptions can result in database rollbacks, and database errors can be recovered from. Put differently, as a user of this library, this library would be easier to reason about if the transaction semantics allowed this:
However, the current behavior requires I reason about this:
Do you disagree that users of this library should be able to reason about a single transaction boundary? Especially when it can be achieved in a relatively straightforward manner (making nested |
Yeah I'm starting to agree that your point-of-view on this one would be an improvement. I've made a small change to the I will note this in the changelog -- any users affected by the change I will deal with if/when they come up. I am curious why the aversion to using savepoints, but nonetheless I do appreciate your persistence and agree that this is an improvement. |
Perfect, thanks Charles! I ended up subclassing The aversion to using savepoints is that 1) they add database calls that may not even be necessary. I have looked at DB logs of applications where most of the database traffic are By the way if you ever find yourself writing Ruby I'd highly recommend Thanks again, |
See coleifer#2767 for discussion.
3.17.0 has been released, which contains this fix. |
Hi there, thanks for building this library!
I am trying to do something like this:
But what's happening is:
BEGIN
INSERT
ROLLBACK
ROLLBACK
That is, the BEGIN and ROLLBACK are unbalanced.
I believe you are aware this is the case, as you have recommended folks use
atomic
, which handles this balancing. However I do not want to useatomic
, since it must create a savepoint, and I don't want to use savepoints (I find it makes the semantics of the transaction far more difficult to reason about, Python ecosystem seems to disagree but I'm not here to argue).What I'd humbly suggest/ask is either:
Add support for
atomic(savepoint=False)
, so we can use atomic without savepoints, to get the balanced transaction behavior.Modify
_transaction.__exit__
so it only rolls back if we're dealing with the top transaction, something like:I don't believe the current
transaction
rollback behavior is desirable since it breaks the semantics of context managers by leaking actions across contexts (the inner context manager rolls back the transaction begun by the outer transaction manager).The text was updated successfully, but these errors were encountered: