Rails provides ActiveRecord::Locking::Optimistic as a method to ensure that multiple changes happening to the same record at roughly the same time do not clobber each other. In theory it makes sense, in practice Optimistic Locking is bound to model logic. Incorporating it into Rails’ stateless transactions is difficult at best.
In a nutshell, Optimistic Locking compares an attribute of a record in memory against the source record in the database before saving. If those attributes are equal the model has not been changed, the save can proceed safely and the attribute is updated to a previously unused value. Otherwise the transaction is cancelled and an error is thrown.
This all falls apart when your application needs to apply optimistic locking across a request. Take the following use case for example:
A user should not be able to update a record if that record has changed since the user last requested the record.
For a Rails application where multiple users have read/write access to records, this can be a common problem. It’s the perfect use for optimistic locking, but unfortunately optimistic locking is only effective in the case where another user changes the record between loading the object into memory and saving it, which does not apply to changes that occur to a record between the time a user loads a form and submits it. On the PUT request, the modified record will be loaded into memory, but, the change has already happened, so optimistic locking doesn’t trigger and the form data will overwrite the recent modifications. Which raises the question:
How can I be sure that my users are not overwriting each other’s data?
When I first asked this question, a lot of thought and a fair bit of Googling, and discarding countless dead ends, I arrived at three potential solutions.
- Preserve state in the session
- Periodically update the view with AJAX
- Add a parameter to preserve state to the requests and write custom before filters
I found the first two options to be incomplete solutions. In a world where there is only one browser tab or window allowed, preserving state in the session would be fine. However it fails when a user access a resource thorugh multiple tabs or windows. Periodically updating the view with AJAX can be foiled by the race condition. The third choice is easily the safest but can be tedious and not very DRY.
Eventually, I bit the bullet and implemented the third choice.
Continue on to Solving Rails’ Optimistic Locking Problem(Part 2: The Solution)
Comments