The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Role::RunAlone - prevent multiple instances of a script from running

VERSION

Version v0.1.0

SYNOPSIS

There are many diffrent ways that a script might be crafted to compose this role. The following is just some of the obvious ways that the author has thought of. The examples below are limited to help prevent boredom.

normal mode, regular script
 #!/usr/bin/perl
  
 use strict;
 use warnings;
  
 use Role::Tiny::With;
 with 'Role::RunAlone';
  
 ...
  
 __END__ # or __DATA__
  
normal mode, modulino
 #!/usr/bin/perl
 package My::Script;
  
 use strict;
 use warnings;
  
 use Moo;
 with 'Role::RunAlone';
  
 ...
  
 __END__ # or __DATA__
  
deferred mode, regular script
 #!/usr/bin/perl
  
 use strict;
 use warnings;
  
 BEGIN {
    $ENV{RUNALONE_DEFER_LOCK} = 1;
 }
  
 use Role::Tiny::With;
 with 'Role::RunAlone';
  
 ...
  
 # exit if we are not alone
 __PACKAGE__->runalone_lock;
  
 # do work
 ...
  
 __END__ # or __DATA__
  
deferred mode, modulino
 #!/usr/bin/perl
 package My::DeferredScript;
  
 use strict;
 use warnings;
  
 BEGIN {
    $ENV{RUNALONE_DEFER_LOCK} = 1;
 }
  
 use Moo;
 with 'Role::RunAlone';
  
 ...
  
 # exit if we are not alone
 __PACKAGE__->runalone_lock;
  
 # do work
 ...
  
 __END__ # or __DATA__
  

DESCRIPTION

This Role provides a simple way for a command line script to ensure that only a single instance of said script is able to run at one time. This is accomplished by trying to obtain an exclusive lock on the script's __DATA__ or __END__ section.

The Role will send a message to STDERR indicating a fatal error and then call exit(2) if neither of those tags are present. This behavior can not be disabled and occurs when the Role is composed.

NOTE: The principle employed DOES NOT work if the script in question is being run on multiple machines. The locking mechanism only works when multiple instances are to be prevented on a single machine.

Normal Locking

If one of the aforementioned tags are present, an attempt is made (via runalone_lock()) to obtain an exclusive lock on the tag's file handle using flock with the LOCK_EX and LOCK_NB flags set. A failure to obtain an exclusive lock means that another instance of the composing script is already executing. A message will be sent to STDERR indicating a fatal condition and the Role will call exit(1).

The Role does nothing if the call to flock is successful.

Deferred Locking

The composing script can tell the Role that it should not immediately call runalone_lock() but should defer this action to the script. This is done like this:

 BEGIN {
    $ENV{RUNALONE_DEFER_LOCK} = 1;
 }
  

The Role will return immediately after checking to see whether or not one of the tags are present instead of trying to get the lock.

Note: It is the responsibility of the composing script to call runalone_lock() at an appropriate time.

Fatal Messages

There are two messages that are sent to STDERR that cannot be suppressed during normal startup:

"FATAL: No __DATA__ or __END__ tag found"
"FATAL: A copy of '$0' is already running"

Note: this message can be suppressed in deferred locking mode. See the noexit argument to runalone_lock.

METHODS

Only one method is currently exposed, but it is the workhorse when deferred mode is used.

runalone_lock

This method attempts to get an exclusive lock on the __END__ or __DATA__ handle that was located during the Role's startup. A composing script may emulate normal operation by simply calling this method with no arguments at the desired time. It will either return a Boolean true if successful, or call exit with a status code of 1 upon failure.

The method's behavior can be modified by four arguments. This allows the composing script to enable lock retries or perform custom operations as needed. (Note: the method is implemented as a class method and may be called with either a class name or a composing object.

Examples:

 # basic call with retries and progress messages enabled
 my $locked = __PACKAGE__->runalone_lock(
    attempts => 3,
    interval => 2,
    verbose  => 1,
 );
  
 # basic call with retries enabled, but silent
 my $locked = __PACKAGE__->runalone_lock(
    attempts => 3,
    interval => 2,
 );
  
 # make a single (silent) attempt, but return to the caller instead of
 # exiting if the attempt fails. also suppresses any failure message.
 my $locked = __PACKAGE__->runalone_lock(
    noexit => 1,
 );
  

Arguments

Invalid values will cause an exception to be thrown via croak so the offending caller might be more easily identified.

noexit (Boolean, default: 0)

If false, the method will call exit(1) if the call to flock fails. Setting it true will cause the method to return the result of the call to flock.

Note: if set, it will also suppress the fatal error message associated with failure to obtain a lock.

attempts (Integer, must satisfy 0 < N < 10; default: 1)

Sets how many attempts will be made to get a lock on the handle in question.

interval (Integer, must satisfy 0 < N < 10, default: 1)

Sets how long to sleep between attempts if attempts is greater than one.

verbose (Boolean, default: 0)

Enables progress messages on STDERR if set. The following messages can appear: ("pkg" will be replaced by the namespace the tag is in.)

 "Attempting to lock pkg::DATA ... Failed, retrying <N> more time(s)"
 "Attempting to lock pkg::DATA ... SUCCESS"
  

Returns

1 if the lock was obtained.

The method will either call exit(1) or return a Boolean false depending upon the value of the noexit argument.

PRIVATE METHODS

There are a few internal methods that are not documented here. All such methods begin with the string _runalone_ in an attempt to avoid namespace collision.

CAVEATS

Multiple Machines

The principle employed DOES NOT work if the script in question is being run on multiple machines. The locking mechanism only works when multiple instances are to be prevented on a single machine.

[NB: This section has been copied from Sys::RunAlone]

Execution of scripts that are (sym)linked to another script, will all be seen as execution of the same script, even though the error message will only show the specified script name. This could be considered a bug or a feature.

Changing a Running Script

[NB: This section has been copied from Sys::RunAlone]

If you change the script while it is running, the script will effectively lose its lock on the file. causing any subsequent run of the same script to be successful, therefore causing two instances of the same script to run at the same time (which is what you wanted to prevent by using Sys::RunAlone in the first place). Therefore, make sure that no instances of the script are running (and won't be started by cron jobs while making changes) if you really want to be 100% sure that only one instance of the script is running at the same time.

ACKNOWLEDGMENTS

This Role relies upon a principle that was first proposed (so far as this author knows) by Randal L. Schwartz MERLYN, and first implemented by Elizabeth Mattijsen ELIZABETH in Sys::RunAlone (currently maintained by Ben Tilly TILLY.) That module has been extended by PERLANCAR in Sys::RunAlone::Flexible with suggestions by this author.

SEE ALSO

Sys::RunAlone, Sys::RunAlone::Flexible

AUTHOR

Jim Bacon, <boftx at cpan.org>

BUGS

Please report any bugs or feature requests to bug-moox-role-runalone at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Role-RunAlone. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Role::RunAlone

You can also look for information at:

LICENSE AND COPYRIGHT

This software is Copyright (c) 2020 by Jim Bacon.

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)

DISCLAIMER OF WARRANTIES

THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.