Skip to content

Instantly share code, notes, and snippets.

@fodra
Created January 9, 2018 01:23
Show Gist options
  • Save fodra/7383d557b075e539f6e5679f7e1f80f4 to your computer and use it in GitHub Desktop.
Save fodra/7383d557b075e539f6e5679f7e1f80f4 to your computer and use it in GitHub Desktop.
This post is a summary of the bug I fixed for our web app throwing "django.db.transaction.TransactionManagementError: This is forbidden when an 'atomic' block is active."

The convoluted stack trace looks like this.

Traceback

[07/Jan/2018 15:04:32] ERROR [rq.worker:757] django.db.transaction.TransactionManagementError: This is forbidden when an 'atomic' block is active.
Traceback (most recent call last):
File "/srv/gametraka_project_v3-master/apps/events/parse_gpx.py", line 174, in process_imu_data
records = create_imu_data(track, records)
File "/usr/lib/python3.6/contextlib.py", line 52, in inner
return func(*args, **kwds)
File "/srv/gametraka_project_v3-master/apps/events/parse_gpx.py", line 72, in create_imu_data
Track.objects.get(pk=track.pk)
File "/srv/venv/lib/python3.6/site-packages/django/db/models/manager.py", line 85, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "/srv/venv/lib/python3.6/site-packages/django/db/models/query.py", line 380, in get
self.model._meta.object_name
apps.events.models.model_track.DoesNotExist: Track matching query does not exist.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/srv/venv/lib/python3.6/site-packages/rq/worker.py", line 713, in perform_job
rv = job.perform()
File "/srv/venv/lib/python3.6/site-packages/rq/job.py", line 556, in perform
self._result = self._execute()
File "/srv/venv/lib/python3.6/site-packages/rq/job.py", line 562, in _execute
return self.func(*self.args, **self.kwargs)
File "/usr/lib/python3.6/contextlib.py", line 52, in inner
return func(*args, **kwds)
File "/srv/gametraka_project_v3-master/apps/events/parse_gpx.py", line 177, in process_imu_data
transaction.rollback()
File "/srv/venv/lib/python3.6/site-packages/django/db/transaction.py", line 49, in rollback
get_connection(using).rollback()
File "/srv/venv/lib/python3.6/site-packages/django/db/backends/base/base.py", line 272, in rollback
self.validate_no_atomic_block()
File "/srv/venv/lib/python3.6/site-packages/django/db/backends/base/base.py", line 443, in validate_no_atomic_block
"This is forbidden when an 'atomic' block is active.")
django.db.transaction.TransactionManagementError: This is forbidden when an 'atomic' block is active.

From what can I tell, there are two parts to this story.

One

Something in the database was queried but the record does not exist.

apps.events.models.model_track.DoesNotExist: Track matching query does not exist.

The code that is responsible for is found at this file:

File "/srv/gametraka_project_v3-master/apps/events/parse_gpx.py", line 72, in create_imu_data
Track.objects.get(pk=track.pk)

At first I thought, the exception was not handled properly.

@transaction.atomic
def create_imu_data(track, imu_data):
    # first guarantee that the track actually exists.
    Track.objects.get(pk=track.pk)
    ...
    return []  # assuming success, return a new empty container for more records

But looking at the function that calls create_imu_data, the exception is handled there. This leads me to the conclusion that the error is not in this function, all I had to do is write up some tests to have this assumption written down somewhere.

test_function.py

def test_create_imu_data_throws_exception_when_track_does_not_exist(self, mock_customer):
    # build does not save in the database
    mock_track = TrackFactory.build(id=2)
    mock_imu_data = [
        IMUDataFactory.build(track=mock_track),
        IMUDataFactory.build(track=mock_track)
    ]
    with self.assertRaises(Track.DoesNotExist):
        result = create_imu_data(mock_track, mock_imu_data)

So, the investigation continues where the error occurs...

Two

The error occurs outside create_imu_data, it clearly said "During handling of the above exception, another exception occurred:"

django.db.transaction.TransactionManagementError: This is forbidden when an 'atomic' block is active.

caused by:

File "/srv/gametraka_project_v3-master/apps/events/parse_gpx.py", line 177, in process_imu_data
transaction.rollback()

I am not really that familiar with atomic transactions on Django until I looked into this issue. Doing a little bit of reading, I found out that with an atomic transaction that's gone bad, Django will automatically rollback the database transaction once the exception handler is called.

That gave me a hint on how to fix the issue.

parse_gpx.py

def some_function(self):
    try:
        records = create_imu_data(track, records)
    except Track.DoesNotExist:
        transaction.rollback()
        ...
        return

We explicitly call transaction.rollback(), but the create_imu_data function is decorated with @transaction.atomic, hence we need not call transaction.rollback() here because it is done automatically for us.

Fix

The fix was simple enough, I just had to remove the transaction.rollback() calls in every exception handler.

Verify

I just had to write a test that says when you do an unnecessary rollback on a atomic transaction, you'll get the TransactionManagementError

@transaction.atomic
def some_function(self):
    try:
        # build does not save the track to db
        mock_track = TrackFactory.build(id=2)
        mock_imu_data = [
            IMUDataFactory.build(track=mock_track),
            IMUDataFactory.build(track=mock_track)
        ]
        create_imu_data(mock_track, mock_imu_data)
    except Track.DoesNotExist:
        transaction.rollback()

def test_create_imu_data_caller_with_rollback(self):
    with self.assertRaises(transaction.TransactionManagementError):
        self.some_function()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment