-
-
Notifications
You must be signed in to change notification settings - Fork 7k
Rollback the transaction on error if ATOMIC_REQUESTS is set. #2887
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
Changes from all commits
27fd485
c2d2417
d1371cc
8ad3820
34dc98e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
from __future__ import unicode_literals | ||
|
||
from django.db import connection, connections, transaction | ||
from django.test import TestCase | ||
from django.utils.unittest import skipUnless | ||
from rest_framework import status | ||
from rest_framework.exceptions import APIException | ||
from rest_framework.response import Response | ||
from rest_framework.test import APIRequestFactory | ||
from rest_framework.views import APIView | ||
from tests.models import BasicModel | ||
|
||
|
||
factory = APIRequestFactory() | ||
|
||
|
||
class BasicView(APIView): | ||
def post(self, request, *args, **kwargs): | ||
BasicModel.objects.create() | ||
return Response({'method': 'GET'}) | ||
|
||
|
||
class ErrorView(APIView): | ||
def post(self, request, *args, **kwargs): | ||
BasicModel.objects.create() | ||
raise Exception | ||
|
||
|
||
class APIExceptionView(APIView): | ||
def post(self, request, *args, **kwargs): | ||
BasicModel.objects.create() | ||
raise APIException | ||
|
||
|
||
@skipUnless(connection.features.uses_savepoints, | ||
"'atomic' requires transactions and savepoints.") | ||
class DBTransactionTests(TestCase): | ||
def setUp(self): | ||
self.view = BasicView.as_view() | ||
connections.databases['default']['ATOMIC_REQUESTS'] = True | ||
|
||
def tearDown(self): | ||
connections.databases['default']['ATOMIC_REQUESTS'] = False | ||
|
||
def test_no_exception_conmmit_transaction(self): | ||
request = factory.post('/') | ||
|
||
with self.assertNumQueries(1): | ||
response = self.view(request) | ||
self.assertFalse(transaction.get_rollback()) | ||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
assert BasicModel.objects.count() == 1 | ||
|
||
|
||
@skipUnless(connection.features.uses_savepoints, | ||
"'atomic' requires transactions and savepoints.") | ||
class DBTransactionErrorTests(TestCase): | ||
def setUp(self): | ||
self.view = ErrorView.as_view() | ||
connections.databases['default']['ATOMIC_REQUESTS'] = True | ||
|
||
def tearDown(self): | ||
connections.databases['default']['ATOMIC_REQUESTS'] = False | ||
|
||
def test_generic_exception_delegate_transaction_management(self): | ||
""" | ||
Transaction is eventually managed by outer-most transaction atomic | ||
block. DRF do not try to interfere here. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not immediately sure what this means - would the test be different if we were using the test client, rather than calling the view directly with the request? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, views are wrapped by https://github.com/django/django/blob/master/django/core/handlers/base.py#L75 In the context of the test, I just check that drf's exception_handler doesn't intefere and let |
||
|
||
We let django deal with the transaction when it will catch the Exception. | ||
""" | ||
request = factory.post('/') | ||
with self.assertNumQueries(3): | ||
# 1 - begin savepoint | ||
# 2 - insert | ||
# 3 - release savepoint | ||
with transaction.atomic(): | ||
self.assertRaises(Exception, self.view, request) | ||
self.assertFalse(transaction.get_rollback()) | ||
assert BasicModel.objects.count() == 1 | ||
|
||
|
||
@skipUnless(connection.features.uses_savepoints, | ||
"'atomic' requires transactions and savepoints.") | ||
class DBTransactionAPIExceptionTests(TestCase): | ||
def setUp(self): | ||
self.view = APIExceptionView.as_view() | ||
connections.databases['default']['ATOMIC_REQUESTS'] = True | ||
|
||
def tearDown(self): | ||
connections.databases['default']['ATOMIC_REQUESTS'] = False | ||
|
||
def test_api_exception_rollback_transaction(self): | ||
""" | ||
Transaction is rollbacked by our transaction atomic block. | ||
""" | ||
request = factory.post('/') | ||
num_queries = (4 if getattr(connection.features, | ||
'can_release_savepoints', False) else 3) | ||
with self.assertNumQueries(num_queries): | ||
# 1 - begin savepoint | ||
# 2 - insert | ||
# 3 - rollback savepoint | ||
# 4 - release savepoint (django>=1.8 only) | ||
with transaction.atomic(): | ||
response = self.view(request) | ||
self.assertTrue(transaction.get_rollback()) | ||
self.assertEqual(response.status_code, | ||
status.HTTP_500_INTERNAL_SERVER_ERROR) | ||
assert BasicModel.objects.count() == 0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ envlist = | |
{py27,py32,py33,py34}-django{17,18,master} | ||
|
||
[testenv] | ||
commands = ./runtests.py --fast | ||
commands = ./runtests.py --fast {posargs} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder what this is for - won't block me for merging. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Allows positional arguments to be passed from running There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @xordoquy $ tox -e py34-django18 -- -x here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure i was just wondering how this related to the pull request |
||
setenv = | ||
PYTHONDONTWRITEBYTECODE=1 | ||
deps = | ||
|
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.
Wondering if it'd make more sense to instead have this behavior once, immediately after the point at which the exception handler is called by the view?
That'd ensure that custom exception handlers don't need to deal with rollbacks explicitly.
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.
Make sense, but since
exception_handler
is hook-able throughsettings.EXCEPTION_HANDLER
, it allows end-users to control transactional behavior.Just an idea.