Conversation
|
So there are a bunch of test failures, all: This is all due to use of This could be fixed by moving all those tests to use the test |
|
I can help with updating the tests, can you share your suggested changes for the tests? I can come up with a PR |
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 view_func = request.resolver_match.func
non_atomic_requests = getattr(view_func, '_non_atomic_requests', set())to this (and document the deviation from the try:
non_atomic_requests = request.resolver_match.func._non_atomic_requests
except AttributeError:
non_atomic_requests = set() |
7059fdc to
13b4c47
Compare
You're right, it's probably not a good idea.
I guess so - I've updated the PR accordingly. |
rpkilby
left a comment
There was a problem hiding this comment.
Thanks for the update. Added to the 3.11 milestone.
|
|
||
| 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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Why is
connection.in_atomic_blocknot sufficient, since it seems like it'd be the right thing to be doing? What doesconnection.in_atomic_blockreturn 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 = Trueon default DB because they had a number of errors due to not using transactions - Wrap some views with
@non_atomic_requestsbecause they weren't safe for ATOMIC_REQUESTS, and instead manually decorate with@atomicinternally - 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()seesconnection.in_atomic_blockisTrue, despite the transaction not coming from ATOMIC_REQUESTS. Tests checking error paths in the views cause attempt to rollback the transaction fromTestCase, whichTestCasealso 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.
There was a problem hiding this comment.
Gotcha. Is there any more graceful way we can do connection.in_atomic_block on a per-db basis?
There was a problem hiding this comment.
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)There was a problem hiding this comment.
Yes that should work but it wouldn't fix my bug with testing the @non_atomic_requests views under TestCase
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
The latter (TransactionTestCase should still work).
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ;)
There was a problem hiding this comment.
"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."
|
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.
9405aa3 to
5c65845
Compare
| # 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 |
There was a problem hiding this comment.
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?
|
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. |
|
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. |
|
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 |
|
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. |
|
I think this is still relevant and shouldn't be marked as stale. |
There was a problem hiding this comment.
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.requestattribute 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.
| with transaction.atomic(), self.assertRaises(Exception): | ||
| self.view(request) |
There was a problem hiding this comment.
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.
| with transaction.atomic(), self.assertRaises(Exception): | |
| self.view(request) | |
| with transaction.atomic(): | |
| with self.assertRaises(Exception): | |
| self.view(request) |
| 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) |
There was a problem hiding this comment.
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).
| @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 |
There was a problem hiding this comment.
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.
| 'args': self.args, | ||
| 'kwargs': self.kwargs, | ||
| 'request': self.request, |
There was a problem hiding this comment.
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.
| 'args': self.args, | |
| 'kwargs': self.kwargs, | |
| 'request': self.request, | |
| 'args': getattr(self, 'args', ()), | |
| 'kwargs': getattr(self, 'kwargs', {}), | |
| 'request': getattr(self, 'request', None), |
| # 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 |
There was a problem hiding this comment.
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”).
| # 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. |
Description
Fixes #6921.
Added tests that fail before and pass afterwards.
Remove the check for
connection.in_atomic_blockto determine if the current request is under atransaction.atomicfromATOMIC_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 requiresrequest, I've also changed the accesses inget_exception_handler_contextto be direct attribute accesses rather thangetattr(). It seems thegetattrdefaults not accessible sinceself.request,self.args, andself.kwargsare always set indispatch()beforehandle_exception()can ever be called. This is useful sincerequestis always needed for the newset_rollbacklogic.This also fixes a bug with multi-DB compatibility - previously
set_rollbackwould only be called on the default DB when there are multiple DB's.