Locking, and How to Test it

This is a two part question. Which type of locking should I use
(optimistic vs. pessimistic) and then how do I account for locking in
my tests?

My scenario is essentially the purchase of a unique item where the
first person to click “Buy” gets the item and everyone else is out of
luck. As a single transaction, I need to determine whether the item
has already been purchased; if so provide an error message, otherwise
buy it. If I did not lock, race conditions may lead multiple people
to think they were succesful (and it would make a mess out of my
otherwise simple accounting).

I’m leaning towards optimistic because: 1) my rails app is the only
thing touching the DB, 2) I’m trying to remain agnostic on DB software
for now, but 3) I don’t really want a read-lock. Am I overlooking
anything that might push me in the other direction?

For testing, I’m trying to be a good TDD citizen, so exactly what test
would cause me to write the logic to handle
ActiveRecord::StaleObjectError? (Or if I go with pessimistic,
the :lock => true option) Considering the record is located, updated,
and saved by the same method I can’t think of a good way to actually
cause the race condition for a test. But common sense tells me it
will happen frequently once I have multiple users interacting with the
app.

On Jul 13, 1:04 am, Brian [email protected] wrote:

otherwise simple accounting).
and saved by the same method I can’t think of a good way to actually
cause the race condition for a test. But common sense tells me it
will happen frequently once I have multiple users interacting with the
app.

for optimistic locking,

It can be quite handy to just mock save! and have it throw
StaleObjectError. depending on your code you may also be able to do

firstInstance = SomeModel.find 1
secondInstance = SomeModel.find 1
secondInstance.created_at_will_change! #or anything that will make the
save not be a no-op
secondInstance.save!

#do something with firstInstance (it is now stale)

Pessimistic locking is a little different - not much will change for
your code except that a select or a write may just block for a little
longer than usual. You should be prepared to handle whatever your
database does if it times out getting a lock or detects a deadlock.

Fred

Ah, great idea. So in the absence of a mock object framework*, is
something like the following fairly standard?

MyModel.send(:define_method, :save!) { raise

ActiveRecord::StaleObjectError, “Boom!” }
#test handling of race condition here
MyModel.send(:undef, :save!)

Since “save!” is inherited, I don’t think I need to worry about
aliasing the old one out of the way or anything.

On Jul 12, 8:38 pm, Frederick C. [email protected]

This technique works well. I’m able to reimplement save (or save!) to
simulate pretty much any race condition I want to handle. I ended up
with something like this:

mock save to simulate another user getting there first.

sneaky_user = users(:brian)
ItemForSale.send(:define_method, :save) do
buyer = sneaky_user
raise ActiveRecord::StaleObjectError, “Boom!”
end

item = itemsforsale(:vase)
assert !item.sell_to(users(:allen)) # uses save internally
assert item.errors.invalid?(:buyer)

models aren’t reloaded, so I need to clean up

ItemForSale.send(:undef_method, :save)

Which should work well as a test for this:

def sell_to(user)
buyer = user

begin
save
rescue ActiveRecord::StaleObjectError
errors.add(:buyer, “already assigned for this item”)
return false
end
end

Brian wrote:

This is a two part question. Which type of locking should I use
(optimistic vs. pessimistic) and then how do I account for locking in
my tests?

My scenario is essentially the purchase of a unique item where the
first person to click “Buy” gets the item
[…]

I doubt that you need explicit locking. Skilful use of transactions
should do the trick and be much easier to manage.

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]