Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Per-session transactions in Django

I'm making a Django web-app which allows a user to build up a set of changes over a series of GETs/POSTs before committing them to the database (or reverting) with a final POST. I have to keep the updates isolated from any concurrent database users until they are confirmed (this is a configuration front-end), ruling out committing after each POST.

My preferred solution is to use a per-session transaction. This keeps all the problems of remembering what's changed (and how it affects subsequent queries), together with implementing commit/rollback, in the database where it belongs. Deadlock and long-held locks are not an issue, as due to external constraints there can only be one user configuring the system at any one time, and they are well-behaved.

However, I cannot find documentation on setting up Django's ORM to use this sort of transaction model. I have thrown together a minimal monkey-patch (ew!) to solve the problem, but dislike such a fragile solution. Has anyone else done this before? Have I missed some documentation somewhere?

(My version of Django is 1.0.2 Final, and I am using an Oracle database.)

like image 882
Alice Purcell Avatar asked Jun 23 '09 17:06

Alice Purcell


2 Answers

Multiple, concurrent, session-scale transactions will generally lead to deadlocks or worse (worse == livelock, long delays while locks are held by another session.)

This design is not the best policy, which is why Django discourages it.

The better solution is the following.

  1. Design a Memento class that records the user's change. This could be a saved copy of their form input. You may need to record additional information if the state changes are complex. Otherwise, a copy of the form input may be enough.

  2. Accumulate the sequence of Memento objects in their session. Note that each step in the transaction will involve fetches from the data and validation to see if the chain of mementos will still "work". Sometimes they won't work because someone else changed something in this chain of mementos. What now?

  3. When you present the 'ready to commit?' page, you've replayed the sequence of Mementos and are pretty sure they'll work. When the submit "Commit", you have to replay the Mementos one last time, hoping they're still going to work. If they do, great. If they don't, someone changed something, and you're back at step 2: what now?

This seems complex.

Yes, it does. However it does not hold any locks, allowing blistering speed and little opportunity for deadlock. The transaction is confined to the "Commit" view function which actually applies the sequence of Mementos to the database, saves the results, and does a final commit to end the transaction.

The alternative -- holding locks while the user steps out for a quick cup of coffee on step n-1 out of n -- is unworkable.

For more information on Memento, see this.

like image 103
S.Lott Avatar answered Nov 04 '22 00:11

S.Lott


In case anyone else ever has the exact same problem as me (I hope not), here is my monkeypatch. It's fragile and ugly, and changes private methods, but thankfully it's small. Please don't use it unless you really have to. As mentioned by others, any application using it effectively prevents multiple users doing updates at the same time, on penalty of deadlock. (In my application, there may be many readers, but multiple concurrent updates are deliberately excluded.)

I have a "user" object which persists across a user session, and contains a persistent connection object. When I validate a particular HTTP interaction is part of a session, I also store the user object on django.db.connection, which is thread-local.

def monkeyPatchDjangoDBConnection():
    import django.db
    def validConnection():
        if django.db.connection.connection is None:
            django.db.connection.connection = django.db.connection.user.connection
        return True
    def close():
        django.db.connection.connection = None
    django.db.connection._valid_connection = validConnection
    django.db.connection.close = close
monkeyPatchDBConnection()

def setUserOnThisThread(user):
    import django.db
    django.db.connection.user = user

This last is called automatically at the start of any method annotated with @login_required, so 99% of my code is insulated from the specifics of this hack.

like image 23
Alice Purcell Avatar answered Nov 04 '22 01:11

Alice Purcell