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 <butler.bria...-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org> 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 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.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 Cheung <frederick.che...-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org> wrote:> On Jul 13, 1:04 am, Brian <butler.bria...-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org> 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 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. > > 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- Hide quoted text - > > - Show quoted text -
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 On Jul 12, 9:02 pm, Brian <butler.bria...-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org> wrote:> 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 Cheung <frederick.che...-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org> > wrote: > > > > > On Jul 13, 1:04 am, Brian <butler.bria...-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org> 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 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. > > > 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- Hide quoted text - > > > - Show quoted text -- Hide quoted text - > > - Show quoted text -
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 marnen-sbuyVjPbboAdnm+yROfE0A@public.gmane.org -- Posted via http://www.ruby-forum.com/.