Skip to content

Improve set_rollback() behaviour#6922

Open
adamchainz wants to merge 4 commits intoencode:mainfrom
adamchainz:fix_issue_6921_atomic_requests
Open

Improve set_rollback() behaviour#6922
adamchainz wants to merge 4 commits intoencode:mainfrom
adamchainz:fix_issue_6921_atomic_requests

Conversation

@adamchainz
Copy link
Copy Markdown
Contributor

@adamchainz adamchainz commented Sep 10, 2019

Description

Fixes #6921.

Added tests that fail before and pass afterwards.

Remove the check for connection.in_atomic_block to determine if the current request is under a transaction.atomic from ATOMIC_REQUESTS. Instead, duplicate the method that Django itself uses in BaseHandler.

This requires fetching the actual view function from as_view(), as seen by the URL resolver / BaseHandler. Since this requires request, I've also changed the accesses in get_exception_handler_context to be direct attribute accesses rather than getattr(). It seems the getattr defaults not accessible since self.request, self.args, and self.kwargs are always set in dispatch() before handle_exception() can ever be called. This is useful since request is always needed for the new set_rollback logic.

This also fixes a bug with multi-DB compatibility - previously set_rollback would only be called on the default DB when there are multiple DB's.

@adamchainz
Copy link
Copy Markdown
Contributor Author

So there are a bunch of test failures, all:

AttributeError: 'NoneType' object has no attribute 'func'

This is all due to use of RequestFactory, which doesn't set resolver_match on its requests as it bypasses URL resolution. In a real world Django app, URL resolution doesn't get bypassed like this.

This could be fixed by moving all those tests to use the test Client, which does set resolver_match, but it's a lot of work.

@auvipy
Copy link
Copy Markdown
Collaborator

auvipy commented Sep 23, 2019

I can help with updating the tests, can you share your suggested changes for the tests? I can come up with a PR

@rpkilby
Copy link
Copy Markdown
Contributor

rpkilby commented Sep 23, 2019

This could be fixed by moving all those tests to use the test Client, which does set resolver_match, but it's a lot of work.

An off the cuff reaction, but I don't think we'd want to go this route. Ignoring the headache of updating the test suite, wouldn't this imply that users are unable to test their views with the request factory? Users would have to use the test client instead, right?

Would it be sufficient to do a best effort to get the non_atomic_requests flag? e.g., from this:

    view_func = request.resolver_match.func
    non_atomic_requests = getattr(view_func, '_non_atomic_requests', set())

to this (and document the deviation from the BaseHandler):

    try:
        non_atomic_requests = request.resolver_match.func._non_atomic_requests
    except AttributeError:
        non_atomic_requests = set()

@adamchainz adamchainz force-pushed the fix_issue_6921_atomic_requests branch from 7059fdc to 13b4c47 Compare September 24, 2019 21:58
@adamchainz
Copy link
Copy Markdown
Contributor Author

An off the cuff reaction, but I don't think we'd want to go this route. Ignoring the headache of updating the test suite, wouldn't this imply that users are unable to test their views with the request factory? Users would have to use the test client instead, right?

You're right, it's probably not a good idea.

Would it be sufficient to do a best effort to get the non_atomic_requests flag?

I guess so - I've updated the PR accordingly.

@rpkilby rpkilby added this to the 3.11 Release milestone Sep 25, 2019
Copy link
Copy Markdown
Contributor

@rpkilby rpkilby 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 update. Added to the 3.11 milestone.

rpkilby
rpkilby previously approved these changes Dec 11, 2019
@rpkilby rpkilby requested a review from xordoquy December 11, 2019 14:48

for db in connections.all():
if db.settings_dict['ATOMIC_REQUESTS'] and db.alias not in non_atomic_requests:
transaction.set_rollback(True, using=db.alias)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm a bit wary of this. Why is connection.in_atomic_block not sufficient, since it seems like it'd be the right thing to be doing? What does connection.in_atomic_block return when inside a non_atomic_request decorated view?

Is it possible to isolate the multi-DB fix in this PR from the _non_atomic_requests change in the PR, or are they tightly linked?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Why is connection.in_atomic_block not sufficient, since it seems like it'd be the right thing to be doing? What does connection.in_atomic_block return when inside a non_atomic_request decorated view?

connection.in_atomic_block means "is the default DB in a transaction?" not "is any DB in a transaction started by ATOMIC_REQUESTS?"

This has a number of subtleties. connection.in_atomic_block can return True in a view with @non_atomic_requests if there is an atomic block from another source than ATOMIC_REQUESTS. This is most likely to be from Django's TestCase, but could also be a custom view decorator or middleware for managing transactions.

More information on the specific case I had was:

  • Move a client's app to ATOMIC_REQUESTS = True on default DB because they had a number of errors due to not using transactions
  • Wrap some views with @non_atomic_requests because they weren't safe for ATOMIC_REQUESTS, and instead manually decorate with @atomic internally
  • Have unit tests for those views using django's TestCase, which sets up two atomic blocks around tests.
  • Those unit tests crash. DRF's set_rollback() sees connection.in_atomic_block is True, despite the transaction not coming from ATOMIC_REQUESTS. Tests checking error paths in the views cause attempt to rollback the transaction from TestCase, which TestCase also tries to rollback, which breaks the whole test run due to unbalanced transactions.

Is it possible to isolate the multi-DB fix in this PR from the _non_atomic_requests change in the PR, or are they tightly linked?

The multi-DB fix comes "for free" because set_rollback() here now copies what Django does in BaseHandler. I think moving away from this is riskier than trying to make this patch focussed only on the single DB case.

I have monkey patched the implementation in this PR into my client's app to fix things there and there have been no issues for 3 months now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Gotcha. Is there any more graceful way we can do connection.in_atomic_block on a per-db basis?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Eg, is something along these lines possible instead?...

for db in connections.all():
    if db.settings_dict['ATOMIC_REQUESTS'] and db.in_atomic_block:
        transaction.set_rollback(True, using=db.alias)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes that should work but it wouldn't fix my bug with testing the @non_atomic_requests views under TestCase

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

it wouldn't fix my bug with testing the @non_atomic_requests views under TestCase

Sorry, walk me through that. Do you mean would be broken for @non_atomic_requests views, or that it would be broken for test cases of @non_atomic_requests views?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The latter (TransactionTestCase should still work).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Okey dokes. So would we be able to update the PR to use the style in the comment above, and switch any test cases to TransactionTestCase if required?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

However this doesn't fix #6921. I will still need that patch in place for my client because their tests use TestCase on @non_atomic_requests views. I remembered while writing that commit that even raise Http404 counts as an error on DRF ;)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"I will still need that patch in place for my client because their tests use TestCase on @non_atomic_requests views."

Okay, that seems to fall within documented expected behavior...

https://docs.djangoproject.com/en/2.2/topics/testing/tools/#django.test.TransactionTestCase "Django’s TestCase class is a more commonly used subclass of TransactionTestCase that makes use of database transaction facilities to speed up the process of resetting the database to a known state at the beginning of each test. A consequence of this, however, is that some database behaviors cannot be tested within a Django TestCase class. For instance, you cannot test that a block of code is executing within a transaction, as is required when using select_for_update(). In those cases, you should use TransactionTestCase."

@lovelydinosaur lovelydinosaur removed this from the 3.11 Release milestone Dec 12, 2019
@lovelydinosaur
Copy link
Copy Markdown
Contributor

I'd like to release 3.11, and I'm not clear enough about this one yet, so going to drop this from the milestone.

If we absolutely need to we can always include it on a point release so long as we make sure to handle the potential signature change on the exception handler in a backwards compat way.

\## Description

Fixes encode#6921.

Added tests that fail before and pass afterwards.

Remove the check for `connection.in_atomic_block` to determine if the current request is under a `transaction.atomic` from `ATOMIC_REQUESTS`. Instead, duplicate the method that Django itself uses [in BaseHandler](https://github.com/django/django/blob/964dd4f4f208722d8993a35c1ff047d353cea1ea/django/core/handlers/base.py#L64).

This requires fetching the actual view function from `as_view()`, as seen by the URL resolver / BaseHandler. Since this requires `request`, I've also changed the accesses in `get_exception_handler_context` to be direct attribute accesses rather than `getattr()`. It seems the `getattr` defaults not accessible since `self.request`, `self.args`, and `self.kwargs` are always set in `dispatch()` before `handle_exception()` can ever be called. This is useful since `request` is always needed for the new `set_rollback` logic.
@adamchainz adamchainz force-pushed the fix_issue_6921_atomic_requests branch from 9405aa3 to 5c65845 Compare December 13, 2019 20:53
# Rollback all connections that have ATOMIC_REQUESTS set, if it looks their
# @atomic for the request was started
# Note this check in_atomic_block may be a false positive due to
# transactions started another way, e.g. through testing with TestCase
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps we ought to be a bit more specific here and link to the https://docs.djangoproject.com/en/2.2/topics/testing/tools/#django.test.TransactionTestCase docs?

@stale
Copy link
Copy Markdown

stale bot commented Jun 19, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Jun 19, 2022
Copy link
Copy Markdown
Collaborator

@auvipy auvipy left a comment

Choose a reason for hiding this comment

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

OK Adam, can you please come back to it and make it merge able again? And perhaps from where anyone should retake it?

@stale
Copy link
Copy Markdown

stale bot commented Apr 27, 2025

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Apr 27, 2025
@auvipy auvipy removed the stale label Apr 28, 2025
@auvipy
Copy link
Copy Markdown
Collaborator

auvipy commented Apr 28, 2025

I was able to fix the merge errors, somehow. and the tests seems still passing. but will left this for both of you to decide if we should move it with it or not

@stale
Copy link
Copy Markdown

stale bot commented Jun 27, 2025

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Jun 27, 2025
@ulgens
Copy link
Copy Markdown
Contributor

ulgens commented Jun 27, 2025

I think this is still relevant and shouldn't be marked as stale.

@auvipy auvipy requested review from Copilot and removed request for xordoquy April 2, 2026 14:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to correct DRF’s rollback behavior when handling exceptions under ATOMIC_REQUESTS, including improving multi-database behavior and adding regression coverage for non-atomic views.

Changes:

  • Update set_rollback() to apply rollback per database alias (multi-DB) and adjust transaction API usage.
  • Add/adjust atomic/non-atomic request tests and URL routing used by those tests.
  • Change exception handler context to use direct self.args/self.kwargs/self.request attribute access.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
rest_framework/views.py Adjusts rollback logic and modifies exception handler context attribute access.
tests/test_atomic_requests.py Updates atomic/non-atomic transaction tests and URL patterns to exercise rollback behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +98 to +99
with transaction.atomic(), self.assertRaises(Exception):
self.view(request)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The assert not transaction.get_rollback() line is unreachable because self.view(request) raises inside the combined with transaction.atomic(), self.assertRaises(Exception): context manager. As written, this test no longer verifies that rollback is not set when the exception is caught inside the atomic block. Consider nesting the context managers (or separating them) so the rollback assertion runs after the exception is handled, while still inside the atomic block.

Suggested change
with transaction.atomic(), self.assertRaises(Exception):
self.view(request)
with transaction.atomic():
with self.assertRaises(Exception):
self.view(request)

Copilot uses AI. Check for mistakes.
Comment on lines 66 to +73
def set_rollback():
# Rollback all connections that have ATOMIC_REQUESTS set, if it looks their
# @atomic for the request was started
# Note this check in_atomic_block may be a false positive due to
# transactions started another way, e.g. through testing with TestCase
for db in connections.all():
if db.settings_dict['ATOMIC_REQUESTS'] and db.in_atomic_block:
db.set_rollback(True)
transaction.set_rollback(True, using=db.alias)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

set_rollback() still gates rollback purely on ATOMIC_REQUESTS + db.in_atomic_block. This is the behavior called out in #6921 as inaccurate: in_atomic_block can be true due to an outer transaction (e.g. TestCase wrapping) even when the resolved view is marked @transaction.non_atomic_requests, and in that case DRF should not mark the transaction for rollback. To address the issue, set_rollback likely needs access to the resolved view callable (e.g. via request.resolver_match.func) and to replicate Django's make_view_atomic/non_atomic_requests checks per-DB alias before calling transaction.set_rollback(..., using=db.alias).

Copilot uses AI. Check for mistakes.
Comment on lines 173 to +180
@override_settings(ROOT_URLCONF='tests.test_atomic_requests')
class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase):
def setUp(self):
connections.databases['default']['ATOMIC_REQUESTS'] = True

def tearDown(self):
connections.databases['default']['ATOMIC_REQUESTS'] = False
@self.addCleanup
def restore_atomic_requests():
connections.databases['default']['ATOMIC_REQUESTS'] = False
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

This regression test uses TransactionTestCase, which does not wrap each test in an outer transaction.atomic(). The false-positive behavior described in #6921 occurs specifically when connection.in_atomic_block is true for reasons other than ATOMIC_REQUESTS (e.g. Django TestCase’s per-test atomic wrapper). Consider switching this test to TestCase (or otherwise ensuring an outer atomic is active during the request) so it actually fails under the pre-fix behavior and guards against the reported scenario.

Copilot uses AI. Check for mistakes.
Comment on lines +236 to +238
'args': self.args,
'kwargs': self.kwargs,
'request': self.request,
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Switching get_exception_handler_context() to direct attribute access can raise AttributeError and mask the original exception in some edge cases (e.g., if an exception occurs before self.request is set in dispatch(), or if a subclass/third-party code calls this helper outside the normal dispatch() flow). Keeping the previous getattr(..., default) pattern (or initializing self.request/self.args/self.kwargs earlier) would preserve the prior, more robust behavior while still allowing request to be present in the common path.

Suggested change
'args': self.args,
'kwargs': self.kwargs,
'request': self.request,
'args': getattr(self, 'args', ()),
'kwargs': getattr(self, 'kwargs', {}),
'request': getattr(self, 'request', None),

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +70
# Rollback all connections that have ATOMIC_REQUESTS set, if it looks their
# @atomic for the request was started
# Note this check in_atomic_block may be a false positive due to
# transactions started another way, e.g. through testing with TestCase
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The comment has a couple of grammar issues that make it harder to parse (e.g., “if it looks their @atomic…” / missing “for” in “Note this check …”). Consider rewording for clarity (e.g., “if it looks like an ATOMIC_REQUESTS atomic block was started for the request”).

Suggested change
# Rollback all connections that have ATOMIC_REQUESTS set, if it looks their
# @atomic for the request was started
# Note this check in_atomic_block may be a false positive due to
# transactions started another way, e.g. through testing with TestCase
# Rollback all connections that have ATOMIC_REQUESTS set, if it looks like
# the @atomic block for the request was started.
# Note that this in_atomic_block check may be a false positive due to
# transactions started in other ways, e.g. when testing with TestCase.

Copilot uses AI. Check for mistakes.
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.

set_rollback's atomic check is inaccurate

7 participants