On Jun 20, 2008, at 11:23 PM, Greg H. wrote:
eg what happens here if Usea 2 starts their request just after User 1?
Q1: Is rails based on optimistic locking?
Q2: Does rails wrap a transaction around all database calls that
happen within the one request/action?
Q3: Is it possible with Rails to do a table-lock on my projections
table? This might be the best?
I have my own pessimistic system to handle situations like these and
most day-to-day concurrent editing in admin-like UIs.
I’m not a fan of optimistic locking. At least, I’ve never seen a case
where it make sense compared to pessimistic locking the way I do it.
IMO dealing with collisions that happen under optimistic locking is a
lot more hassle than just dealing with pessimistic locking right up
front.
The theory is simple, but it takes some code to deal with and work
around ActiveRecord.
Theory:
-
add at least two, optionally three, fields to your models/tables:
lock_id, locked_at, and locked_by (optional)
-
lock_id = id of the lock owner’s user record (IOW current_user.id)
-
locked_at = full time stamp of when a lock is claimed
-
locked_by = friendly name of user – handy for work group scenarios
where you might want to literally display who has a current lock.
-
add a method to your model (I have this abstracted as a mixin to
ActiveRecord) called find_and_lock which is responsible for finding
the records you want to lock and making sure they are available. If
they are available, then lock them by populating lock_id and
locked_at. One problem I had with applying this to Rails is
ActiveRecord’s validation. I ended up using direct SQL to avoid AR.
Not evil, but requires a layer to abstract queries for RDBMS engines,
or you just live with db-specific code.
-
next you’ll need a method for save_using_lock. This method is
responsible for first checking the lock fields of a record about to
be saved to see if our claim is still valid. The main cause for loss
of lock is that it expires. If it has expired, but the lock_id still
matches our own (no one else came along to claim a lock), I allow it
to be saved anyway (that’s a little optimistic logic thrown in).
-
I mentioned a lock expiring. My compromise for the stateless web
environment is to treat a lock much like a session. It’s valid for X
time in minutes. Depending on the app and data involved, I might have
a very short one of 5 minutes where a field or two is being edited,
and access requests might be common, to an hour to allow somone to
write or approve an article where request freqency will be very low.
By time stamping every lock, I can compare current time, locked_at
time, and the allowed lock duration.
People can abandon locked records. Start a form to edit a record and
click a UI link to go who knows where in the web site, that record is
still locked. The expiration handles the worst case. If nothing else
the find_and_lock will ignore a lock that has expired. However, to
make the web app more proactive in handling abandoned locks, I have a
method called clear_pessimistic_locks. Every lock claimed adds to a
session variable that stores the database and table of the record
that was locked. clear_pessimistic_locks iterates through that list
and clears the locks held by current_user. I pepper controller
actions of likely entry pages in the site with that method (it’s a
mixin to ActiveController). If the user editing a record jumps to the
site home page, then that action will clean up the abandoned lock.
I’ve used this system on another platform for years. It’s not fast
enough to use for highly competitive requests for updates like very
heavy auctions, but it has worked very well in all my apps so far.
For hyper applications, you’d need a high speed threaded entry-point.
I had that in one of my first versions, but found it unnecessary for
my apps, so I haven’t transported that to this Rails version.
I’m just getting started with this Rails version (written as a
plugin), so I don’t know yet what quirks there might be. So far I
discovered I need to use raw SQL to avoid AR’s autopilot actions
everytime I just want to set or clear the lock fields. Seems to work
just fine so far.
Seems like it should work well for your scenario too.
–
def gw
writes_at ‘www.railsdev.ws’
end