Skip to content

Conversation

HamaBarhamou
Copy link

Add a guard that raises when entering db.transaction()/transaction_if_not_already() if the testcase transaction already has pending on_commit callbacks (opt-out via setting); closes #31.

…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
@HamaBarhamou HamaBarhamou requested a review from a team as a code owner October 8, 2025 16:45
@samueljsb
Copy link
Collaborator

Thanks so much for getting involved! 😁

I think @meshy has a plan to solve this one slightly differently. It might be worth discussing what the right approach would be on the issue before moving forward with this implementation.

Copy link
Collaborator

@LilyFirefly LilyFirefly left a 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:

Comment on lines +79 to +83
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
Copy link
Collaborator

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:

Suggested change
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),
Copy link
Collaborator

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.

Suggested change
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)
Copy link
Collaborator

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.

Suggested change
pending = getattr(connection, "run_on_commit", None)
pending = connection.run_on_commit

Comment on lines +117 to +127
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
Copy link
Collaborator

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:

Suggested change
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

Comment on lines +12 to +15
dj_tx.on_commit(lambda: None)
with self.assertRaises(_PendingTestcaseAfterCommitCallbacks):
with db.transaction():
pass
Copy link
Collaborator

@LilyFirefly LilyFirefly Oct 9, 2025

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:

Suggested change
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, [])

Comment on lines +28 to +31
dj_tx.on_commit(lambda: None)
with self.assertRaises(_PendingTestcaseAfterCommitCallbacks):
with db.transaction_if_not_already():
pass
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
database: str | None = None
database: str

@HamaBarhamou
Copy link
Author

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

@meshy
Copy link
Collaborator

meshy commented Oct 9, 2025

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.

@HamaBarhamou
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Raise error in tests if entering transaction and the testcase-transaction already has on-commit callbacks

4 participants