App-karr

 view release on metacpan or  search on metacpan

lib/App/karr/Lock.pm  view on Meta::CPAN

            if length $current && $current ne $email;

        return () unless $git->delete_ref_cas( $ref, $oid );
        return ( 1, "released" );
    } );
}


1;

__END__

=pod

=encoding UTF-8

=head1 NAME

App::karr::Lock - Lock management via Git refs

=head1 VERSION

version 0.500

=head1 SYNOPSIS

    my $lock = App::karr::Lock->new(git => $git, ttl => 300);
    my ($ok, $msg) = $lock->acquire(12, 'agent@example.com');

=head1 DESCRIPTION

L<App::karr::Lock> manages lightweight per-task locks stored in Git refs. It is
used by commands such as C<karr pick> to avoid concurrent agents selecting the
same task at the same time.

The lock is an optimisation, not the thing that makes C<karr pick> exclusive.
Its holder identity is the clone's C<user.email>, which every agent on one
machine shares, so it cannot separate them from each other at all; what actually
binds a pick is the compare-and-swap on the task card itself
(L<App::karr::BoardStore/save_task_cas>). What the lock buys is that agents do
not all pile onto the same candidate and lose the same race.

=head2 Expiry

A lock has a TTL, because an agent that dies between C<acquire> and C<release>
otherwise leaves a ref that no future run will ever clear -- and that task then
stays unpickable forever, with no way out from inside karr (#45). Age is the
committer time of the commit the lock ref points at, so it needs no payload of
its own and travels with the ref.

A lock past its TTL may be taken over. The takeover is itself a compare-and-swap
against the OID whose age was judged, so a holder that refreshes its lock in
between wins and is never silently evicted. The TTL is deliberately B<not>
C<claim_timeout>: see L<App::karr::Cmd::Pick>.

=head2 Locks are local, and live outside the board

Lock refs live under C<refs/karr-local/>, which nothing pushes, fetches, prunes
or snapshots. A lock says "this process, in this clone, is mid-pick right now",
and that sentence has no meaning anywhere else: a clone that receives one cannot
tell whether the holder is still alive, and has no way to find out.

They used to live at C<refs/karr/tasks/N/lock>, inside the namespace C<karr>
pushes. Any sync that fired while a lock was held published it, other clones
pulled it, and it then blocked their picks until somebody ran C<karr unlock> --
a lock that outlived the process holding it and the machine it ran on (#93). It
also turned every board backup into a snapshot of somebody's momentary lock.
Moving the refs out is what makes that impossible, rather than making it depend
on the timing of when a lock happens to be released.

Locks left in the old place by a C<karr> older than this one -- or pulled from a
remote that still has them -- are not acted on: they cannot say anything about
this process, and a pick's exclusivity does not rest on them anyway. They are
not ignored either. C<locks> reports them, marked C<legacy>, and C<break_lock>
clears them, so C<karr unlock> is the way out of the mess the old layout left
behind.

=head2 new

    my $lock = App::karr::Lock->new( git => $git, task_id => 12, ttl => 300 );
    my $lock = App::karr::Lock->new( dir => '.' );   # builds its own Git

Takes C<git> (an L<App::karr::Git> instance), or C<dir> to build one via
C<< App::karr::Git->new(dir => $dir) >> when no C<git> is given. C<task_id>
and C<ttl> are both optional -- see L</task_id> and L</ttl>.

=head2 task_id

The task this lock instance was constructed for. Every method that names a
lock (L</ref_name>, L</legacy_ref_name>, L</get>, L</acquire>, L</release>,
L</break_lock>) takes an explicit C<$task_id> and falls back to this only
when none is given, so one C<App::karr::Lock> can be reused across tasks by
always passing C<$task_id> explicitly -- as C<karr pick> does, trying one
candidate after another with a single lock object -- or dedicated to one
task by setting this instead.

=head2 git

The L<App::karr::Git> instance the lock reads and writes refs through. Set
from the C<git> argument to L</new>, or built there from C<dir> when not
given.

=head2 ttl

Seconds a lock may be held before L</expired> considers it stale and
L</acquire> is allowed to take it over. Falls back to C<300> (the
C<DEFAULT_TTL> constant) when not given at L</new> -- but direct
construction is the exception: C<karr pick> builds its lock with the
board's own C<lock_timeout> config value instead (see
L<App::karr::Cmd::Pick/LOCK EXPIRY>), so that is what governs expiry in
practice. A C<ttl> of C<0> or a negative number disables expiry outright:
L</expired> always answers false and no lock built with it is ever taken
over.

=head2 ref_name

    my $ref = $lock->ref_name(12);   # 'refs/karr-local/tasks/12/lock'
    my $ref = $lock->ref_name;       # uses $lock->task_id

The current-layout ref name for a task's lock, under C<refs/karr-local/> --
outside every namespace C<karr> pushes, fetches, prunes or snapshots (see



( run in 0.926 second using v1.01-cache-2.11-cpan-14f38c9f855 )