package Narada::Lock;
use warnings;
use strict;
use Carp;
our $VERSION = 'v2.2.0';
use Perl6::Export::Attrs;
use Narada;
use Fcntl qw( :DEFAULT :flock F_SETFD FD_CLOEXEC );
use Errno;
use Time::HiRes qw( sleep );
use constant IS_NARADA1 => eval { local $SIG{__DIE__}; Narada::detect('narada-1') } || undef;
use constant DIR => IS_NARADA1 ? 'var/' : q{};
use constant LOCKNEW => DIR.'.lock.new';
use constant LOCKFILE => DIR.'.lock';
use constant TICK => 0.1;
my $F_lock;
sub shared_lock :Export {
my $timeout = shift;
return 1 if $ENV{NARADA_SKIP_LOCK};
sysopen $F_lock, LOCKFILE, O_RDONLY|O_CREAT or croak "open: $!";
while (1) {
next if -e LOCKNEW;
last if flock $F_lock, LOCK_SH|LOCK_NB;
$!{EWOULDBLOCK} or croak "flock: $!";
} continue {
return if defined $timeout and (($timeout-=TICK) < TICK);
sleep TICK;
}
return 1;
}
sub exclusive_lock :Export {
return if $ENV{NARADA_SKIP_LOCK};
sysopen $F_lock, LOCKFILE, O_WRONLY|O_CREAT or croak "open: $!";
while (1) {
last if flock $F_lock, LOCK_EX|LOCK_NB;
$!{EWOULDBLOCK} or croak "flock: $!";
system('touch', LOCKNEW) == 0 or croak "touch: $!/$?";
sleep TICK;
}
system('touch', LOCKNEW) == 0 or croak "touch: $!/$?";
return;
}
sub unlock_new :Export {
return if $ENV{NARADA_SKIP_LOCK};
unlink LOCKNEW;
return;
}
sub unlock :Export {
return if $ENV{NARADA_SKIP_LOCK};
if ($F_lock) {
flock $F_lock, LOCK_UN or croak "flock: $!";
}
return;
}
sub child_inherit_lock :Export {
my ($is_inherit) = @_;
return if $ENV{NARADA_SKIP_LOCK};
if ($F_lock) {
fcntl $F_lock, F_SETFD, $is_inherit ? 0 : FD_CLOEXEC or croak "fcntl: $!";
}
return;
}
1; # Magic true value required at end of module
__END__
=encoding utf8
=head1 NAME
Narada::Lock - manage project locks
=head1 VERSION
This document describes Narada::Lock version v2.2.0
=head1 SYNOPSIS
use Narada::Lock qw( shared_lock unlock child_inherit_lock );
use Narada::Lock qw( exclusive_lock unlock_new unlock );
shared_lock();
unlock();
shared_lock(0) or die "Can't get lock right now";
unlock();
shared_lock(5) or die "Can't get lock in 5 seconds";
unlock();
shared_lock();
system('sleep 1');
child_inherit_lock(1);
system('sleep 10 &');
child_inherit_lock(0);
system('sleep 1');
unlock();
exclusive_lock();
# do critical operations, reboot-safe
unlock_new();
# do non-critical operations, still in exclusive mode
unlock();
=head1 DESCRIPTION
To allow safe backup/update/maintenance of project, there should be
possibility to guarantee consistent state of project, at some point.
To reach this goal, ALL operations which modify project data on disk
(including both project files and database) must be done under shared lock,
and all operations which require consistent project state must be done
under exclusive lock.
This module contain helper functions to manage project locks, but all
operations required for these locks can be implemented using any
programming language, so all applications in project (including non-perl
applications) are able to manage project locks.
Shared lock is set using flock(2) LOCK_SH on file C<.lock>.
Exclusive lock is set using flock(2) LOCK_EX on file C<.lock>.
=head2 FREEZE NEW TASKS
There exists scenario when it's impossible to set exclusive lock:
if new tasks will start and set shared lock before old tasks will drop shared
lock (and so shared lock will be set all of time).
To work around this scenario another file C<.lock.new> should be used
as semaphore - it should be created before trying to set exclusive lock,
and new tasks shouldn't try to set shared lock while this file exists.
This file should be removed after finishing critical operations - this
guarantee project data will not change even if system will be rebooted,
because after reboot existence of file C<.lock.new> will prevent from
starting new tasks with shared locks but not prevent from placing
exclusive lock again and continue these critical operations.
=head1 INTERFACE
=over
=item B<shared_lock>( $timeout )
Try to get shared lock which is required to modify any project data (files or
database).
If $timeout undefined - will wait forever until lock will be granted.
If $timeout >=1 will try to get lock every 1 second until $timeout expire.
Use unlock() to free this lock.
Return: true if able to get lock.
=item B<exclusive_lock>()
Try to get exclusive lock which is required to guarantee consistent project
state (needed while backup/update/maintenance operations).
Set two locks: create file C<.lock.new> which signal other scripts to
not try to set shared lock while this file exists and get LOCK_EX on file
C<.lock> to be sure all current tasks finished and unlocked their
shared locks.
Will delay until get lock.
Use unlock_new() in combination with exit() or unlock() to free these locks.
Return: nothing.
=item B<unlock_new>()
Free first lock set by exclusive_lock() (i.e. remove file C<.lock.new>).
This allow other tasks to get shared_lock() after this process exit or
call unlock().
Return: nothing.
=item B<unlock>()
Free lock set by shared_lock() (or second lock set by exclusive_lock()).
Return: nothing.
=item B<child_inherit_lock>( $is_inherit )
By default, child processes don't inherit our FD with lock.
This is acceptable only if we don't run child in background or if
child will get own locks on start.
In other cases you should call child_inherit_lock() with true value in
$is_inherit to force child to inherit our lock (just like DJB's `setlock`
or Pepe's `chpst -[lL]` do).
Calling child_inherit_lock() with false value in $is_inherit will switch
back to default behaviour (new child will not inherit FD with lock).
Examples:
# OK: not in background
system("rm -rf var/something");
# OK: in background, but this is our script,
# which will get lock on it's own
system("./another_script_of_this_project &");
# ERROR: in background, no lock
system("( sleep 5; rm -rf var/something ) &");
# OK: in background, inherit lock
child_inherit_lock(1); # from now all childs will inherit lock
system("( sleep 5; rm -rf var/something ) &");
child_inherit_lock(0); # next child will not inherit lock
Return: nothing.
=back
=head1 CONFIGURATION AND ENVIRONMENT
Narada::Lock requires configuration files and directories provided by
Narada framework.
If $ENV{NARADA_SKIP_LOCK} is set to any true value then shared_lock(),
exclusive_lock(), unlock_new(), unlock() and child_inherit_lock() will do
nothing (shared_lock() will return true).
=head1 COMPATIBILITY
Narada 1.x project use C<var/.lock> instead of C<.lock>.
Narada 1.x project use C<var/.lock.new> instead of C<.lock.new>.
=head1 SUPPORT
=head2 Bugs / Feature Requests
Please report any bugs or feature requests through the issue tracker
at L<https://github.com/powerman/Narada/issues>.
You will be notified automatically of any progress on your issue.
=head2 Source Code
This is open source software. The code repository is available for
public review and contribution under the terms of the license.
Feel free to fork the repository and submit pull requests.
L<https://github.com/powerman/Narada>
git clone https://github.com/powerman/Narada.git
=head2 Resources
=over
=item * MetaCPAN Search
L<https://metacpan.org/search?q=Narada>
=item * CPAN Ratings
L<http://cpanratings.perl.org/dist/Narada>
=item * AnnoCPAN: Annotated CPAN documentation
L<http://annocpan.org/dist/Narada>
=item * CPAN Testers Matrix
L<http://matrix.cpantesters.org/?dist=Narada>
=item * CPANTS: A CPAN Testing Service (Kwalitee)
L<http://cpants.cpanauthors.org/dist/Narada>
=back
=head1 AUTHOR
Alex Efros E<lt>powerman@cpan.orgE<gt>
=head1 COPYRIGHT AND LICENSE
This software is Copyright (c) 2008-2015 by Alex Efros E<lt>powerman@cpan.orgE<gt>.
This is free software, licensed under:
The MIT (X11) License
=cut