App-karr
view release on metacpan or search on metacpan
lib/App/karr/Role/ClaimTimeout.pm view on Meta::CPAN
# ClaimedBy as a side effect of asking the question; in karr that would
# change what the require_claim check in
# L<App::karr::Role::TaskMutation/apply_status_change> sees a few lines
# later, turning an allowed move into a refused one. Expired claims are
# reaped where they always were, by `karr pick`.
#
# * the message stays "Task N is claimed by X", the wording `karr handoff`
# has always used, rather than kanban-md's "add --claim X" hint: `karr
# delete` has no --claim option, so that hint would be unfollowable for one
# of the four callers.
#
# * the expired case is recorded, because it used to be the one answer this
# method gave -- here and in kanban-md both -- with nothing said anywhere
# (ticket #177). A live claim held by somebody else is refused loudly and
# the refusal names the holder, which is how #176's confused agent gets its
# own lost claim name back; then the claim expires and that signal
# disappears, at the moment it is most useful. `move` and `handoff --claim`
# re-stamp claimed_by on the way through, so the previous holder is gone
# from the card too, and karr-foundation, which attributes stalls per claim
# name, is left attributing to a name nobody ever held.
#
# * a task in a terminal status is not guarded at all, whoever holds the
# claim on it. kanban-md's CheckClaim never looks at the status, so this
# one is karr's rule rather than parity, and it comes from karr's own
# vocabulary: CONTEXT.md defines a Claim as the lease an agent holds
# *while working* a task, calls it released once the task reaches a
# terminal status, and keeps claimed_by there for provenance and interop
# only. That is not a private reading either -- `karr board` already prints
# no claimant on a finished card and leaves it out of its "N claimed"
# footer, both keyed on the very
# L<App::karr::Config/is_terminal_status> asked here, and `karr pick`
# refuses to hand a terminal card out again
# (L<App::karr::Role::PickRules>), so nothing can route a second agent onto
# finished work through this door. The code was the one place still
# treating the field as a live lease: the agent that finished a card went
# on guarding it for the rest of claim_timeout -- an hour by default, and
# precisely the hour in which the closing note gets appended, the card gets
# archived, or someone reopens it. Worse, the refusal demanded a name the
# board deliberately hides on exactly those cards, so the way through was
# to read it off `karr show`. And it never was a lock: `karr edit ID
# --release` takes any claim off without knowing whose it is, which made
# the rule two commands instead of one for whoever knew the way round and a
# dead end for everybody else (ticket #223). What still protects a finished
# card is what protects every card here: update_task_guarded's
# compare-and-swap, which is about concurrent writes and not about
# ownership.
#
# That last case is checked last, after the expiry test rather than before it,
# although either order allows the same calls. The difference is the record: a
# terminal card whose claim had *also* expired keeps reporting the takeover it
# reported before (see expired_claim_report and #177), instead of losing that
# line to a case that answers earlier and says nothing.
#
# Recorded, not printed, and for the same reason as
# App::karr::Role::DependencyCheck: check_claim runs inside
# App::karr::Role::TaskMutation/update_task_guarded's callback, which re-runs
# when another agent gets in first, so a print here would come out once per
# attempt -- and once for an attempt that was then discarded. A slot keyed by
# task id and cleared on entry is replaced by the attempt that wins instead,
# which is also what collapses `karr delete`'s two checks (once outside the
# guard to decide about the prompt, once inside it) into one line.
#
# What is *not* done here: nothing is refused that was not refused before, and
# no new state goes on the card. Taking over an expired claim is the documented
# purpose of claim_timeout, so this makes it audible, not harder. And the
# takeover is not the same event as the holder outliving its own timeout: the
# claimant-matches case returns above without reaching this, so a long-running
# agent never gets warned about itself.
sub check_claim {
my ($self, $task, $claimant) = @_;
delete $self->_expired_claims->{ $task->id };
return 1 unless $task->has_claimed_by && length $task->claimed_by;
return 1 if defined $claimant && length $claimant && $task->claimed_by eq $claimant;
if ( $self->_claim_expired( $task, $self->claim_timeout_secs ) ) {
$self->_expired_claims->{ $task->id } = {
held_by => $task->claimed_by,
claimed_at => $task->claimed_at,
};
return 1;
}
return 1 if $self->store->is_terminal_status( $task->status );
die sprintf "Task %d is claimed by %s\n", $task->id, $task->claimed_by;
}
# The channels are App::karr::Role::DependencyCheck/dependency_report's, not a
# second convention: STDERR keeps STDOUT parseable, --json carries the same fact
# as data because a JSON consumer never reads STDERR, and --quiet silences the
# human copy only -- the pair is data, not chatter, so a drain loop that does not
# want the line can still read who held the card.
#
# The pair is a structure rather than the sentence DependencyCheck ships,
# because the caller this exists for is karr-foundation, which attributes stalls
# per claim name and must not have to parse that name out of English.
sub expired_claim_report {
my ( $self, $id ) = @_;
my $expired = $self->_expired_claims->{$id};
return () unless $expired;
printf STDERR
"Warning: task %s: overriding the expired claim held by %s (claimed %s)\n",
$id, $expired->{held_by}, $expired->{claimed_at}
unless $self->json || $self->quiet;
return ( expired_claim => $expired );
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
App::karr::Role::ClaimTimeout - Shared claim timeout logic
( run in 0.282 second using v1.01-cache-2.11-cpan-aadc1410aed )