0

In my app, multiple users can edit the same object in the database. If user1 loads data into a form, yet user2 writes data to the database AFTER user1 loads the form but before user1 submits the data, I would like to prevent the save of user1's data.

Is there anything built-in to Rails, or available in the Ruby/Rails community libraries that aims to solve this problem?

I could attempt it myself, by having a virtual attribute on all models read_at that all UI elements send back. But I know problems can be more complicated than they seem, and if there's a built-in solution, I'd like to know.

Barring a pre-built solution, are there Rails-specific characteristics I might consider using? I know there is a form token? I wonder if I could get time info from that?

pixelearth
  • 13,674
  • 10
  • 62
  • 110
  • 2
    You could try having a hidden field `last_updated_at` in the form which should have the value of record's `updated_at` at the time form was rendered. If someone else updates it, `updated_at` would change and you can reject the form submit of first user as `params[:last_updated_at] < record.updated_at`. – Jagdeep Singh May 05 '21 at 17:35

2 Answers2

2

The link to ActiveRecord::Locking::Optimistic in the answer by Andreas Gebhard technically says all you need to know. However, it might be more clear if I spelled it out for you.

If you add a field named lock_version in your table, by default ActiveRecord will enable optimistic locking. However you are not done yet.

The problem you are having is called "The Lost Update Problem" where a second person's update clobbers an earlier person's update. If all you do is enable optimistic locking in the database layer you will still have a lost update problem in your web application if you program it normally and do not take the necessary steps.

Normally your update action does a find of a record, then replaces all the fields with values sent from the form. As far as the database and ActiveRecord know, you started with a fresh recent record and updated it. Thus optimistic locking will be satisfied with allowing the update but you still have a lost update problem.

What you need to do is include the lock_version field as a hidden input in your form. Then when the second person doing an update tries to update, setting the lock_version to the old value before the prior update was done will cause optimistic locking to fail.

In pessimistic locking, the key to avoiding the lost update problem is to get the edit form you must first get the lock. If someone else has it locked, you cannot edit. A very important key to solving the lost update problem is the human in the loop by presenting the values in the database before their edit so only the human makes a decision to change them. In the lost update someone did not know about someone else's update and so saved the previous value unaware that it had been updated.

In reality there is no solution to the problem. Someone will always lose. In the lost update problem the first person to save loses. In pessimistic (aka exclusive locking) the second person to try to get the lock loses. In optimistic locking the second person to save loses.

The downside with pessimistic/exclusive locking is someone trying to get the edit form is denied because someone else has the lock. Also locks might not get released when they should and you can have deadlocks.

The downside with optimistic locking is someone can do all the work on their edit and be denied when they try to save.

In any case it is a surprise to someone just trying to edit. The downside of the lost update problem which makes it the worst is the surprise happens quietly without anyone noticing. At least with exclusive or optimistic locking the user gets informed. With optimistic locking they have to "merge" possibly having to redo the work of their edit starting from fresh data. With exclusive locking they never have that problem but they may have to wait and might not understand why. The general preference for optimistic locking is that most of the time there is no contention for the update and it just works whereas for exclusive locking there is always the step of going into edit mode and refreshing the data on the edit form.

Marlin Pierce
  • 9,931
  • 4
  • 30
  • 52
  • 2
    thanks for taking your time for spelling this topic out. I am aware that SO encourage users to spell things out, but this is so super-basic with tons of googleable resources and official docs that I really saw no need to do so. Still, nice job! – Andreas Gebhard May 06 '21 at 15:22
  • 1
    @AndreasGebhard It is basic if you know it and would ask if Rails has a solution ot the "Lost Update Problem" or built in locking. I get the feeling that people think activerecord has optimistic locking so maybe you have to enable it but then you don't have to do anything, but then you might still have a lost update problem because you have to make sure the form is passing the `lock_version` field. So the documentation is complete but can leave people in the dark. – Marlin Pierce May 07 '21 at 07:02
  • 1
    I perfectly agree! – Andreas Gebhard May 07 '21 at 12:12
  • Getting back to this issue a bit late, but thank you both for your answers, and @MarlinPierce thank you for "spelling it out". The lost update issue is my biggest concern where I work. Multiple people may have loaded a form for the same resource. Or worse, have multiple browser tabs open of the same resource. I'm mostly looking for methods to avoid clobbering previous updates. See next comment. – pixelearth Jul 01 '21 at 15:04
  • Rails locking using an extra db field per resource (we have hundreds) seems heavy-handed. I have what might be a naive question. How is this more accurate than simply passing the `updated_at` attribute from the form (or any data)? My current simplest design (that I haven't implemented) would be to override the `updated_at` setter for all of active record, and do a check of the `updated_at` value coming in (from form or other) to the `updated_at` current value, and if it is earlier, through an error. @AndreasGebhard @MarlinPierce Do you see any disadvantages to this approach? – pixelearth Jul 01 '21 at 15:11
  • Well even with tables with millions of records I wouldn't sweat an additional field. The advantage of using the standard rails locking is that is it built it, it has been properly designed and debugged, and other developers may be familiar with it or it will be worth it for them to get familiar. A disadvantage with a timestamp is the database does not promise that timestamps are unique, so trying to avoid updates at the same time runs that risk. It was a big deal when timestamps had one second granularity. – Marlin Pierce Jul 01 '21 at 17:30
  • I take that back about timestamps not being unique. What is relevant is time between getting the timestamp for the form as opposed to what's in the database so there is no problem with collisions at the same time. – Marlin Pierce Jul 01 '21 at 19:03
  • A problem with implementing your own optimistic locking field is that you want to test if it is ok to do the update at the same time you do the update. So if you are implementing your own, you want to put a WHERE clause in your update to only update the record WHERE it has the expected `updated_at` value, so if the `updated_at` has changed no update occurs. Then test if the update happened. Just saying if you are going to implement your own, that is the way you need to do it. Which works if you build the SQL but might be hard with the rails `save` method. – Marlin Pierce Jul 01 '21 at 19:07
1

Rails has two mechanisms baked right-in for your issue: optimistic and pessimistic locking. Personally, I find the optimistic flavor much easier to use. There is lots of documentation on the web, so this is just a starting point. It already covers most of the caveats like multi-process race conditions and hidden form fields to deal with them!

Felix
  • 4,510
  • 2
  • 31
  • 46
Andreas Gebhard
  • 361
  • 2
  • 8