Prevent Cron Cancer With a File-based Mutex
Recently at work I needed to write a script that would walk through a directory's contents that matched a regex, perform some operations on the files, and then remove the files afterwards. I didn't want to have the script running all the time, so I decided to add a cronjob to run the script every minute (if there aren't any files to process, then the script terminates immediately).
But there was a wrinkle to my plan: the script could potentially run for longer than one minute, and having two copies of the script running simultaniously would be a Bad Thing. After considering this problem for a few minutes I realized a simple and sufficient solution: using a file as a mutex.
Concept
The system's functioning is pretty basic:
- When the script begins, check if a file named
my_script.lock
exists. - If it doesn't exist, create the file and save it with the current time since epoch as its only contents.
- If it does exist, and the time since epoch contained within it is within the last hour, then abort the script.
- (Unless you aborted in step three) continue with the script.
- Once the script has completed, delete
my_script.lock
.
At first I didn't bother putting a timestamp into the file, but by adding a timestamp it makes it possible to expire the lock if it has been locked for an unreasonably long period of time (suggesting that the script crashed) without manually deleting the lock file.
Implementation in Perl
Implementing this file-based mutex in Perl is quick and painless. Unless you're a miserable Perl coder, then it'll take you a while to piece together all the necessary documentation.
It took me a while to piece it together.
my $lock_file = '/tmp/my_script.lock';
my $expire_lock_after = 60 * 30;
sub is_locked {
# -e is a function that checks for file existence
if (-e $lock_file) {
# this line declares there is no line delimiter
local $/=undef;
open LOCK, "<$lock_file" or warn "Couldn't open $lock_file";
my $ts = <LOCK>;
close LOCK;
if ( $ts > time() - $expire_lock_after ) {
return 1;
}
}
return undef;
}
sub lock {
open LOCK, ">$lock_file" or warn "Couldn't open $lock_file";
print LOCK time();
close LOCK;
}
sub unlock {
unlink($lock_file);
}
if (is_locked) die "$lock_file is locked";
lock();
print 'do whatever';
unlock();
Yeah. I still don't understand Perl-style for booleans. Perhaps a kind strange will clarify a more idiomatic way to write the above code. I'd certainly appreciate it if they did1.
In my lame defense, I did go questing about for clarification on Perl booleans, but mostly the answers seemed to be that booleans in Perl are
undefined
and everything else. Is that as deep as the answer goes?↩