-
Notifications
You must be signed in to change notification settings - Fork 5
feat(db): raise when entering transaction if testcase has pending on_… #87
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
feat(db): raise when entering transaction if testcase has pending on_… #87
Conversation
…commit (kraken-tech#31) Add guard that raises on entering db.transaction() (and transaction_if_not_already() when it opens) if the testcase transaction already has pending on_commit callbacks. Introduce _PendingTestcaseAfterCommitCallbacks and _error_if_pending_testcase_on_commit_callbacks; integrate with transaction() and transaction_if_not_already(). Add opt-out setting SUBATOMIC_RAISE_IF_PENDING_TESTCASE_ON_COMMIT_ON_ENTER (default: True). Fix transaction_if_not_already(): preserve semantics (no new atomic if already in one) and pass savepoint when innermost atomic wraps testcase. Tests: add tests/test_on_commit_guard.py covering raise + opt-out. Docs: update settings reference; CHANGELOG updated. Closes kraken-tech#31
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the fix! If we go with this solution, I have some suggestions:
with _error_if_pending_testcase_on_commit_callbacks(using=using): | ||
with _execute_on_commit_callbacks_in_tests(using), django_transaction.atomic( | ||
using=using, durable=True | ||
): | ||
yield |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We prefer this style for nested context managers:
with _error_if_pending_testcase_on_commit_callbacks(using=using): | |
with _execute_on_commit_callbacks_in_tests(using), django_transaction.atomic( | |
using=using, durable=True | |
): | |
yield | |
with ( | |
_error_if_pending_testcase_on_commit_callbacks(using=using), | |
_execute_on_commit_callbacks_in_tests(using), | |
django_transaction.atomic(using=using, durable=True), | |
): | |
yield |
if pending: # non vide => des callbacks ont été enregistrés avant | ||
raise _PendingTestcaseAfterCommitCallbacks( | ||
pending=len(pending), | ||
database=getattr(connection, "alias", None), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A connection
will always have alias
set.
database=getattr(connection, "alias", None), | |
database=connection.alias, |
|
||
if _innermost_atomic_block_wraps_testcase(using=using): | ||
connection = django_transaction.get_connection(using=using) | ||
pending = getattr(connection, "run_on_commit", None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
connection
should always have a run_on_commit
attribute.
pending = getattr(connection, "run_on_commit", None) | |
pending = connection.run_on_commit |
if in_transaction(using=using): | ||
# Already in a transaction; don't open another. | ||
yield | ||
else: | ||
# If the innermost atomic block comes from the testcase, create a SAVEPOINT. | ||
savepoint = _innermost_atomic_block_wraps_testcase(using=using) | ||
with _error_if_pending_testcase_on_commit_callbacks(using=using): | ||
with _execute_on_commit_callbacks_in_tests(using), django_transaction.atomic( | ||
using=using, savepoint=savepoint | ||
): | ||
yield |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we want the in_transaction
here:
if in_transaction(using=using): | |
# Already in a transaction; don't open another. | |
yield | |
else: | |
# If the innermost atomic block comes from the testcase, create a SAVEPOINT. | |
savepoint = _innermost_atomic_block_wraps_testcase(using=using) | |
with _error_if_pending_testcase_on_commit_callbacks(using=using): | |
with _execute_on_commit_callbacks_in_tests(using), django_transaction.atomic( | |
using=using, savepoint=savepoint | |
): | |
yield | |
savepoint = _innermost_atomic_block_wraps_testcase(using=using) | |
with ( | |
_error_if_pending_testcase_on_commit_callbacks(using=using), | |
_execute_on_commit_callbacks_in_tests(using), | |
django_transaction.atomic(using=using, savepoint=savepoint), | |
): | |
yield |
dj_tx.on_commit(lambda: None) | ||
with self.assertRaises(_PendingTestcaseAfterCommitCallbacks): | ||
with db.transaction(): | ||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes sense to assert
that the on_commit
callback also doesn't run in this case:
dj_tx.on_commit(lambda: None) | |
with self.assertRaises(_PendingTestcaseAfterCommitCallbacks): | |
with db.transaction(): | |
pass | |
calls = [] | |
dj_tx.on_commit(lambda: calls.append("ok")) | |
with self.assertRaises(_PendingTestcaseAfterCommitCallbacks): | |
with db.transaction(): | |
pass | |
self.assertEqual(calls, []) |
dj_tx.on_commit(lambda: None) | ||
with self.assertRaises(_PendingTestcaseAfterCommitCallbacks): | ||
with db.transaction_if_not_already(): | ||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dj_tx.on_commit(lambda: None) | |
with self.assertRaises(_PendingTestcaseAfterCommitCallbacks): | |
with db.transaction_if_not_already(): | |
pass | |
calls = [] | |
dj_tx.on_commit(lambda: calls.append("ok")) | |
with self.assertRaises(_PendingTestcaseAfterCommitCallbacks): | |
with db.transaction_if_not_already(): | |
pass | |
self.assertEqual(calls, []) |
@attrs.frozen | ||
class _PendingTestcaseAfterCommitCallbacks(Exception): | ||
pending: int | ||
database: str | None = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
database: str | None = None | |
database: str |
hank you @LilyFirefly @samueljsb for your feedback; I will take it into account and, as already mentioned, let's first agree on the direction to take before moving forward. cc @meshy |
Hi @HamaBarhamou. Thank you for taking the time to address this issue. In hindsight I realise that I should have assigned myself to #31 when I started working on it, so that other people such as yourself didn't consider it "open". My apologies: I'll make an effort to "claim" tickets in future to prevent this kind of duplicated work. I have now committed and pushed my (draft) implementation to #93. |
@meshy OK, thanks for the feedback. No problem, it's my pleasure to contribute. I think we can close this draft and focus on yours. |
Add a guard that raises when entering
db.transaction()
/transaction_if_not_already()
if the testcase transaction already has pendingon_commit
callbacks (opt-out via setting); closes #31.