background tasks race conditions

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
     end

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

   def self.down
     drop_table :semaphores
   end
end

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 +true+
   # 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 inserting
     # 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
   end

   # Pretty much the opposite of lock... We don't return anything meaningful.
   # 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
   end

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

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
    end

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

Dave