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 )