Why No Race Condition in Ruby

Why no race condition in Ruby

This is because MRI Ruby threads are not really parallel due to GIL (see here), at CPU level they are executed one at a time.

Each command in a thread is executed one at a time hence @count in each thread is always updated correctly.

Race condition can be simulated by adding another variable like:

class Counter
attr_accessor :count, :tmp

def initialize
@count = 0
@tmp = 0
end

def increment
@count += 1
end

end

c = Counter.new

t1 = Thread.start { 1000000.times { c.increment; c.tmp += 1 if c.count.even?; } }
t2 = Thread.start { 1000000.times { c.increment; c.tmp += 1 if c.count.even?; } }

t1.join
t2.join

p c.count #200_0000
p c.tmp # not 100_000, different every time

A nice example of race condition is given here, copied below for completeness

class Sheep
def initialize
@shorn = false
end

def shorn?
@shorn
end

def shear!
puts "shearing..."
@shorn = true
end
end

sheep = Sheep.new

5.times.map do
Thread.new do
unless sheep.shorn?
sheep.shear!
end
end
end.each(&:join)

Here's the result I see from running this on MRI 2.0 several times.

$ ruby check_then_set.rb => shearing...

$ ruby check_then_set.rb => shearing... shearing...

$ ruby check_then_set.rb => shearing...
shearing...

Sometimes the same sheep is being shorn twice!

Does race condition exist in `puts`?

That's because puts doesn't write to STDOUT right away, but buffers the string and writes in bigger chunks.

You can get ruby to write immediately with the following:

STDOUT.sync = true

which should resolve your ordering issue.

How does unique index solve the race condition in validates_uniqueness_of in RoR?

The race condition occurs because rails can't lock down the database and often runs multiple threads at the same time.

To understand how the race-condition occurs, think about what is needed for a uniqueness-check, and I'll give an example of two threads:

Lets suppose Person A and B want to save a new BlogPost with attribute name which must be unique.

  1. Person A hits the save button
  2. Person B hits the save button
  3. Rails starts thread A that goes and queries the database to see if
    there are any BlogPosts with name "My BlogPost"
  4. Rails starts thread B that goes and queries the database to see if there are any BlogPosts with name "My BlogPost"
  5. Thread A returns "nope, no blogposts with that name, all clear to save"
  6. Thread B returns "nope, no blogposts with that name, all clear to save"
  7. Thread A saves the blogpost for person A
  8. So does Thread B

...and now there's two blogposts with the same name.

There's nothing stopping this from occurring, this is because the "lookup[" and "save" actions are two separate things.. and thus can occur in the manner described above.

However... when you put a unique index on the database... what happens is this:

  1. Person A hits the save button
  2. Person B hits the save button
  3. Rails starts thread A that goes and queries the database to see if
    there are any BlogPosts with name "My BlogPost"
  4. Rails starts thread B that goes and queries the database to see if there are any BlogPosts with name "My BlogPost"
  5. Thread A returns "nope, no blogposts with that name, all clear to save"
  6. Thread B returns "nope, no blogposts with that name, all clear to save"
  7. Thread A saves the blogpost for person A
  8. Thread B tries to save the blogpost, but the database says "NOPE! I failed a uniqueness constraint"

Result: only one BlogPost with the name.

Now - to what you asked... and what I'm assuming is the mistaken understanding... and index is not an id.
Each record does not get the same index.
No record gets an index.

You can kinda pretend that an index is a lookup-table of all the values that are already set for this column.

What happens with a non-unique index is that you have a list of all the values... and the list of which records have that value. eg:

Widgets:
colour:
blue ids: 1,3,7
green ids: 2,4
red ids: 5,6

(totally made up example, nothing like reality)

When the index has a uniqueness constraint, it just has the same list, but only allows the db to store one id per value and if you try to store another one... it raises an exception

Race Condition in Ruby on Rails

after_create is processed within the database transaction saving the text message. Therefore the callback that hits another controller cannot read the text message. It is not a good idea to have an external call within a database transaction, because the transaction blocks parts of the database for the whole time the slow external request takes.

The simples solution is to replace after_save with after_commit (see: http://apidock.com/rails/ActiveRecord/Transactions/ClassMethods/after_commit)

Since callbacks tend to become hard to understand (and may lead to problems when testing), I would prefer to make the call explicit by calling another method. Perhaps something like this:

# use instead of .save 
def save_and_sent_sms
save and sent_sms
end

Perhaps you want to sent the sms in the background, so it does not slow down the web request for the user. Search for the gems delayed_job or resque for more information.

How do I avoid a simple race condition in Rails?

In order to achieve this you have to use some kind of locking. Basically you have 3 options: optimistic/pessimistic rails locking and some external locking backend (like Redis::Lock).

I personally would go for pessimistic locking if high performance is not really the case here

photo = Photo.find(photo_id)
photo.with_lock do
photo.num_votes += 1
photo.save!
end

I should also point out that sticking to only wrapping incrementing num_votes and save into one transaction would not solve the race-condition. Most RDBMS by default work in read committed mode. Which doesn't prevent such a race condition.

FYI See Pessimistic and Optimistic Locking reference



Related Topics



Leave a reply



Submit