9

I have a process that runs on cron every five minutes. Usually, it takes only a few seconds to run, but sometimes it takes several minutes. I want to ensure that only one version of this is running at a time.

I tried an obvious way...

File.open("/tmp/indexer_lock.tmp",'w') do |f|
  exit unless f.flock(File::LOCK_EX)
end

...but it's not testing to see if it can get the lock, it's blocking until the lock is released.

Any idea what I'm missing? I'd rather not hack something using ps, but that's an alternative.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Jason Butler
  • 5,619
  • 3
  • 22
  • 18

10 Answers10

24

I know this is old, but for anyone interested, there's a non-blocking constant that you can pass to flock so that it returns instead of blocking.

File.new("/tmp/foo.lock").flock( File::LOCK_NB | File::LOCK_EX )

Update for slhck

flock will return true if this process received the lock, false otherwise. So to ensure just one process is running at a time you just want to try to get the lock, and exit if you weren't able to. It's as simple as putting an exit unless in front of the line of code I have above:

exit unless File.new("/tmp/foo.lock").flock( File::LOCK_NB | File::LOCK_EX )
Community
  • 1
  • 1
smathy
  • 26,283
  • 5
  • 48
  • 68
  • Could you maybe expand this answer with a minimal working example? I'm having trouble understanding how to use that piece of code. That'd be great! – slhck Jul 17 '12 at 13:02
  • Passing the permissions to create/open the file might be necessary in some machines, e.g. linux. – David Pelaez Mar 11 '14 at 06:49
5

Depending on your needs, this should work just fine and doesn't require creating another file anywhere.

exit unless DATA.flock(File::LOCK_NB | File::LOCK_EX)

# your script here

__END__
DO NOT REMOVE: required for the DATA object above.
Lothar
  • 12,537
  • 6
  • 72
  • 121
Matt Todd
  • 817
  • 8
  • 12
  • This is very slick and seems to work well. Now to spend some time understanding exactly what it is doing. – spkane Jun 26 '13 at 18:53
  • Be aware that the `DATA` constant is only available to the first Ruby script executed, so `bundle exec` will break this, as `DATA` will be set on the bundle script, not the script you are bundle exec-ing. – dpnsan Aug 08 '17 at 16:54
3

You could create and delete a temporary file and check for existence of this file. Please check the answer to this question : one instance shell script

Community
  • 1
  • 1
shodanex
  • 14,975
  • 11
  • 57
  • 91
3

Although this isn't directly answering your question, if I were you I'd probably write a daemon script (you could use http://daemons.rubyforge.org/)

You could have your indexer (assuming its indexer.rb) be run through a wrapper script named script/index for example:

require 'rubygems'
require 'daemons'

Daemons.run('indexer.rb')

And your indexer can do almost the same thing, except you specify a sleep interval

loop do
   # code executing your indexing 

   sleep INDEXING_INTERVAL
end

This is how job processors in tandem with a queue server usually function.

ucron
  • 2,822
  • 2
  • 18
  • 6
3

There's a lockfile gem for exactly this situation. I've used it before and it's dead simple.

Sarah Mei
  • 18,154
  • 5
  • 45
  • 45
2

Here's a one-liner that should work at the top of any Ruby script:

exit unless File.new(__FILE__)).tap {|f| f.autoclose = false}.flock(File::LOCK_NB | File::LOCK_EX)

There are two issues with the original code.

First, the reason it's blocking is that the call to #flock is missing File::LOCK_NB:

Don't block when locking. May be combined with other lock options using logical or.

Second, if a File object is closed (whether at the end of an #open block as in the code above, via explicit #close, or implicitly auto-closed when the File is garbage-collected), the underlying file descriptor is closed and the lock is released. To prevent this you can set #autoclose =false.

wjordan
  • 19,770
  • 3
  • 85
  • 98
2

If your using cron it might be easier to do something like this in the shell script that cron calls:

#!/usr/local/bin/bash
#

if ps -C $PROGRAM_NAME &> /dev/null ; then
   : #Program is already running.. appropriate action can be performed here (kill it?)
else
   #Program is not running.. launch it.
   $PROGRAM_NAME
fi
pisswillis
  • 1,569
  • 2
  • 14
  • 19
1

Ok, working off notes from @shodanex's pointer, here's what I have. I rubied it up a little bit (though I don't know of a touch analogue in Ruby).

tmp_file = File.expand_path(File.dirname(__FILE__)) +  "/indexer.lock"
if File.exists?(tmp_file)
  puts "quitting"
  exit
else
  `touch #{tmp_file}`
end

.. do stuff ..

File.delete(tmp_file)
Jason Butler
  • 5,619
  • 3
  • 22
  • 18
  • Rather than create a new answer, edit your original question to show how you used the real answer to solve the problem. More people will see it there. – the Tin Man Mar 17 '11 at 03:46
0

At a higher level, you might find the lock_method gem useful:

def the_method_my_cron_job_calls
  # something really expensive
end
lock_method :the_method_my_cron_job_calls

It uses lockfiles stored on the local filesystem (what was being discussed above) by default, but you can also configure remote lock storage:

LockMethod.config.storage = Redis.new([...]) # a remote RedisToGo instance, perhaps?

Also...

def the_method_my_cron_job_calls
  # something really expensive
end
lock_method :the_method_my_cron_job_calls, (60*60) # automatically expire lock after an hour
Seamus Abshere
  • 8,326
  • 4
  • 44
  • 61
0

Can you not add File::LOCK_NB to your lock, to make it non-blocking (i.e. it fails if it can't get the lock)

That would work in C, Perl etc.

MarkR
  • 62,604
  • 14
  • 116
  • 151