package File::Flock::Retry;

our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
our $DATE = '2021-08-10'; # DATE
our $DIST = 'File-Flock-Retry'; # DIST
our $VERSION = '0.632'; # VERSION

use 5.010001;
use strict;
use warnings;

use Fcntl ':DEFAULT', ':flock';

sub lock {
    my ($class, $path, $opts) = @_;
    $opts //= {};
    my %h;

    defined($path) or die "Please specify path";
    $h{path}    = $path;
    $h{retries} = $opts->{retries} // 60;
    $h{shared}  = $opts->{shared} // 0;
    $h{mode}    = $opts->{mode} // (O_CREAT | O_RDWR);

    my $self = bless \%h, $class;
    $self->_lock;
    $self;
}

# return 1 if we lock, 0 if already locked. die on failure.
sub _lock {
    my $self = shift;

    # already locked
    return 0 if $self->{_fh};

    my $path = $self->{path};
    my $existed = -f $path;
    my $exists;
    my $tries = 0;
  TRY:
    while (1) {
        $tries++;

        # 1
        sysopen $self->{_fh}, $path, $self->{mode}
            or die "Can't open lock file '$path': $!";

        # 2
        my @st1 = stat($self->{_fh}); # stat before lock

        # 3
        if (flock($self->{_fh}, ($self->{shared} ? LOCK_SH : LOCK_EX) | LOCK_NB)) {
            # if file is unlinked by another process between 1 & 2, @st1 will be
            # empty and we check here.
            redo TRY unless @st1;

            # 4
            my @st2 = stat($path); # stat after lock

            # if file is unlinked between 3 & 4, @st2 will be empty and we check
            # here.
            redo TRY unless @st2;

            # if file is recreated between 2 & 4, @st1 and @st2 will differ in
            # dev/inode, we check here.
            redo TRY if $st1[0] != $st2[0] || $st1[1] != $st2[1];

            # everything seems okay
            last;
        } else {
            $tries <= $self->{retries}
                or die "Can't acquire lock on '$path' after $tries seconds";
            sleep 1;
        }
    }
    $self->{_acquired} = 1;
    1;
}

# return 1 if we unlock, 0 if already unlocked. die on failure.
sub _unlock {
    my ($self) = @_;

    my $path = $self->{path};

    # don't unlock if we are not holding the lock
    return 0 unless $self->{_fh};

    unlink $self->{path} if $self->{_acquired} && !(-s $self->{path});

    {
        # to shut up warning about flock on closed filehandle (XXX but why
        # closed if we are holding the lock?)
        no warnings;

        flock $self->{_fh}, LOCK_UN;
    }
    close delete($self->{_fh});
    1;
}

sub release {
    my $self = shift;
    $self->_unlock;
}

sub unlock {
    my $self = shift;
    $self->_unlock;
}

sub handle {
    my $self = shift;
    $self->{_fh};
}

sub DESTROY {
    my $self = shift;
    $self->_unlock;
}

1;
# ABSTRACT: Yet another flock module

__END__

=pod

=encoding UTF-8

=head1 NAME

File::Flock::Retry - Yet another flock module

=head1 VERSION

This document describes version 0.632 of File::Flock::Retry (from Perl distribution File-Flock-Retry), released on 2021-08-10.

=head1 SYNOPSIS

 use File::Flock::Retry;

 # try to acquire exclusive lock. if fail to acquire lock within 60s, die.
 my $lock = File::Flock::Retry->lock($file);

 # explicitly unlock
 $lock->release;

 # automatically unlock if object is DESTROY-ed.
 undef $lock;

=head1 DESCRIPTION

This is yet another flock module. It is a more lightweight alternative to
L<File::Flock> with some other differences:

=over 4

=item * OO interface only

=item * Autoretry (by default for 60s) when trying to acquire lock

I prefer this approach to blocking/waiting indefinitely or failing immediately.

=back

=for Pod::Coverage ^(DESTROY)$

=head1 METHODS

=head2 lock

Usage:

 $lock = File::Flock::Retry->lock($path, \%opts)

Attempt to acquire an exclusive lock on C<$path>. By default, C<$path> will be
created if not already exists (see L</mode>). If C<$path> is already locked by
another process, will retry every second for a number of seconds (by default
60). Will die if failed to acquire lock after all retries.

Will automatically unlock if C<$lock> goes out of scope. Upon unlock, will
remove C<$path> if it is still empty (zero-sized).

Available options:

=over

=item * mode

Integer. Default: O_CREAT | O_RDWR.

File open mode, to be passed to Perl's C<sysopen()>. For example, if you want to
avoid race condition between creating and locking the file, you might want to
use C<< O_CREAT | O_EXCL | O_RDWR >> to fail when the file already exists. Note
that the constants are available after you do a C<< use Fcntl ':DEFAULT'; >>.

=item * retries

Integer. Default: 60.

Number of retries (equals number of seconds, since retry is done every second).

=item * shared

Boolean. Default: 0.

By default, an exclusive lock (LOCK_EX) is attempted. However, if this option is
set to true, a shared lock (LOCK_SH) is attempted.

=back

=head2 unlock

Usage:

 $lock->unlock

Unlock. will remove lock file if it is still empty.

=head2 release

Usage:

 $lock->release

Synonym for L</unlock>.

=head2 handle

Usage:

 my $fh = $lock->handle;

Return the file handle.

=head1 HOMEPAGE

Please visit the project's homepage at L<https://metacpan.org/release/File-Flock-Retry>.

=head1 SOURCE

Source repository is at L<https://github.com/perlancar/perl-File-Flock-Retry>.

=head1 BUGS

Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=File-Flock-Retry>

When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.

=head1 CAVEATS

Not yet tested on Windows. Some filesystems do not support inode?

=head1 SEE ALSO

L<File::Flock>, a bit too heavy in terms of dependencies and startup overhead,
for my taste. It depends on things like L<File::Slurp> and
L<Data::Structure::Util> (which loads L<Digest::MD5>, L<Storable>, among
others).

L<File::Flock::Tiny> which is also tiny, but does not have the autoremove and
autoretry capability which I want. See also:
L<https://github.com/trinitum/perl-File-Flock-Tiny/issues/1>

flock() Perl function.

An alternative to flock() is just using sysopen() with O_CREAT|O_EXCL mode to
create lock files. This is supported on more filesystems (particularly network
filesystems which lack flock()).

=head1 AUTHOR

perlancar <perlancar@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2021, 2019, 2017, 2015, 2014 by perlancar <perlancar@cpan.org>.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut