Forum: Ruby on Rails proper way to ensure atomic model changes

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
D077ca0bec0f2a7c0d218cae8bef0110?d=identicon&s=25 Kirill Kuvaldin (kuvkir)
on 2009-05-19 10:57
(Received via mailing list)
Hi!

Say we have a rails active-record-based model called "instance" which
can have different states - STOPPED, RUNNING, STARTING_UP,
SHUTTING_DOWN, etc...

There is a method #start which changes the instance state to
STARTING_UP only if it is STOPPED.

def start
  if self.state == STOPPED
   self.state = STARTING_UP
   self.save
   ...
  end
end

As you understand if we have multiple concurrent requests for #start,
there is a possibility of a race condition, where two or more
processes are running the if branch.

I want the #start request to be atomic, meaning that only one of the
concurrent processes can actually execute the if branch, others must
be waiting or get some kind of error.

Should the request be somehow wrapped into a DB transaction?

-Kirill
81b61875e41eaa58887543635d556fca?d=identicon&s=25 Frederick Cheung (Guest)
on 2009-05-19 11:41
(Received via mailing list)
On 19 May 2009, at 09:56, kirill.kuvaldin@gmail.com wrote:
>
take a look at optimistic locking.

Fred
Bce1d1b7c3ec7b577dcb42e254899e6b?d=identicon&s=25 Michael Schuerig (Guest)
on 2009-05-19 12:25
(Received via mailing list)
On Tuesday 19 May 2009, kirill.kuvaldin@gmail.com wrote:

>    self.save
> be waiting or get some kind of error.
>
> Should the request be somehow wrapped into a DB transaction?

Yes, that at any rate. However, don't confuse transaction blocks with
multiple exclusion blocks. A transaction, by itself, doesn't preclude
concurrent users/processes from reading and updating the same data. The
potential for conflict arises only through concurrent updates.

The race condition in your unadorned code results from a difference
between time of check (state == x) and time of change (state = ...).
There are two strategies to avoid the resulting inconsistencies:

Optimistic locking, as Fred indicated in another reply. With optimistic
locking, you run headlong into the race condition, but when writing to
the database you ensure that it can only succeed if is based on
consistent data. On updating a record, ActiveRecord checks that the
updated_at timestamp of the record as currently stored is the same as
the timestamp when the object was read. An identical timestamp indicates
that there haven't been any intervening updates.

Pessimistic locking is another way. You can either #lock! an object you
already have or #find(..., :lock => true) get a locked object to begin
with. Locking an object like this precludes any changes to the
corresponding database row until the locking transaction is either
committed or rolled back.

Rails gives you optimistic locking automatically for tables that have
the requisite timestamp columns (updated_at). Pessimistic locking you
have to do explicitly. As a guess, I'd say that pessimistic locking is
only worth your and the database's effort if conflicts are likely.

At any rate, with both locking strategies you have to take into account
the possibility of a conflict. With optimistic locking, you get an
ActiveRecord::StaleObjectError exception in that case. I'm not sure
about pessimistic locking, but I guess you'll get an indistinctive
ActiveRecord::StatementInvalid exception.

Michael

--
Michael Schuerig
mailto:michael@schuerig.de
http://www.schuerig.de/michael/
81b61875e41eaa58887543635d556fca?d=identicon&s=25 Frederick Cheung (Guest)
on 2009-05-19 12:51
(Received via mailing list)
On May 19, 11:24 am, Michael Schuerig <mich...@schuerig.de> wrote:
> Rails gives you optimistic locking automatically for tables that have
> the requisite timestamp columns (updated_at). Pessimistic locking you
> have to do explicitly. As a guess, I'd say that pessimistic locking is
> only worth your and the database's effort if conflicts are likely.

You mean lock_version (integer, default 0)

>
> At any rate, with both locking strategies you have to take into account
> the possibility of a conflict. With optimistic locking, you get an
> ActiveRecord::StaleObjectError exception in that case. I'm not sure
> about pessimistic locking, but I guess you'll get an indistinctive
> ActiveRecord::StatementInvalid exception.

If you hold a lock then anyone trying to update/select that row will
wait until you release your lock (or until they give up waiting).

Fred
Bce1d1b7c3ec7b577dcb42e254899e6b?d=identicon&s=25 Michael Schuerig (Guest)
on 2009-05-19 13:09
(Received via mailing list)
On Tuesday 19 May 2009, Frederick Cheung wrote:
> On May 19, 11:24 am, Michael Schuerig <mich...@schuerig.de> wrote:
> > Rails gives you optimistic locking automatically for tables that
> > have the requisite timestamp columns (updated_at). Pessimistic
> > locking you have to do explicitly. As a guess, I'd say that
> > pessimistic locking is only worth your and the database's effort if
> > conflicts are likely.
>
> You mean lock_version (integer, default 0)

Yes, of course.

> > At any rate, with both locking strategies you have to take into
> > account the possibility of a conflict. With optimistic locking, you
> > get an ActiveRecord::StaleObjectError exception in that case. I'm
> > not sure about pessimistic locking, but I guess you'll get an
> > indistinctive ActiveRecord::StatementInvalid exception.
>
> If you hold a lock then anyone trying to update/select that row will
> wait until you release your lock (or until they give up waiting).

And if they give up waiting that manifests itself in some kind of
error/exception that needs to be handled.

Locking out other readers (SELECT) through SELECT ... FOR UPDATE is the
default locking mode used by ActiveRecord when :lock => true is used.
Nevertheless, the mode can be explicitly specified to allow concurrent
readers, but block writers.

PostgreSQL: :lock => 'FOR SHARE'
MySQL: :lock => 'LOCK IN SHARE MODE'


Michael

--
Michael Schuerig
mailto:michael@schuerig.de
http://www.schuerig.de/michael/
This topic is locked and can not be replied to.