Forum: Ruby on Rails background tasks race conditions

Announcement (2017-05-07): is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see and for other Rails- und Ruby-related community platforms.
Ae477ee7167a8775fe92ab921ffd7a42?d=identicon&s=25 Ra PM (rpm13)
on 2007-05-26 06:52
I need to run some cron jobs that clean up a table T which is used and
created by the foreground rails controller.  This could naturally lead
to synchronization issues/race conditions etc

eg. Let RC = rails controller. BJ background cron job

RC: Queries T for a record using find
BJ: queries T for the same record and removes it
RC: proceeds as though record still exists

Are there any special methods for handling this?

5d06917e13b29bcff1c1609492c06873?d=identicon&s=25 Dave Thomas (Guest)
on 2007-05-27 06:11
(Received via mailing list)
On May 25, 2007, at 11:52 PM, Ra PM wrote:

> Are there any special methods for handling this?

You could set up a simple Semaphore table:

class CreateSemaphores < ActiveRecord::Migration

   def self.up
     create_table(:semaphores, :id => false, :primary_key => :name)
do |t|
       t.column :name, :string,     :null => false, :limit => 20
       t.column :counter, :integer, :default => 1, :null => false

     execute("alter table semaphores add constraint pk_semaphores
primary key(name)")

   def self.down
     drop_table :semaphores

and then write a model that gives you locked access to a named thing:

# A basic in-database class. Call ::lock to lock a given name,
# ::unlock to unlock it

class Semaphore < ActiveRecord::Base

   # These are the names you can lock

   RESOURCE1 = "Some name"
   RESOURCE2  = "Some other name"

   # Attempt to lock the given name by decrementing the count. Return
   # if the lock succeeds, +false+ otherwise
   def self.lock(name)

     # start by assuming the row exists and is lockable. That gives
us an early
     # exit 99.99% of the time
     count = update_all("counter = counter - 1", [ "counter > 0 &&
name = ?", name])
     return true if count == 1

     # If no row found, might be because there isn't one. Let's try
     # it. If it fails, then we know that there was one already
(because +name+
     # is the primary key) and we can exit. If the insert succeeds, then
     # the count is zero, because it now is claimed

     create(:name => name, :counter => 0) rescue false

   # Pretty much the opposite of lock... We don't return anything
   # It's an error to unlock a semaphore that doesn't exist
   def self.unlock(name)
     count = update_all("counter = counter + 1", [ "name = ?", name])
     fail "Unknown semaphore: #{name}" unless count == 1

   # Run a block if a semaphore is available, otherwise return false
   def self.run_block(name)
     if (result = lock(name))
     return result

Given that, you could write something like the following in both your
controller and your external script

    locked = Semaphore.lock(Semaphore::RESOURCE1) do
       mess around
       with shared resource

   if !locked
     flash[:notice] = "Sorry, but the resource was busy..."

This topic is locked and can not be replied to.