App-karr
view release on metacpan or search on metacpan
lib/App/karr/Role/ClaimTimeout.pm view on Meta::CPAN
# ABSTRACT: Shared claim timeout logic
package App::karr::Role::ClaimTimeout;
our $VERSION = '0.601';
use Moo::Role;
# Loaded without importing, and every call below is qualified. A Moo::Role
# composes every sub in its package into its consumers, imported ones included,
# so `use Time::Piece;` here put its localtime/gmtime replacements on every
# command that composes this role (#105). Worse than the #38 cases, because
# those two shadow builtins: a future `sub localtime` on a command class would
# fight an inherited export and look like a core function misbehaving.
# App::karr::Role::Output and App::karr::Role::BoardDiscovery state the rule.
#
# Time::Piece is not a drop-in for that treatment -- replacing the builtins is
# its whole point -- so the two sites below were decided one at a time:
# ->strptime was already a class method and needs no import, and the gmtime in
# _claim_expired wants the overloaded object (the builtin returns a string in
# that scalar context, and the subtraction would be nonsense), so it is spelled
# Time::Piece::gmtime().
use Time::Piece ();
use App::karr::Config;
# Loaded without importing, for the reason spelled out in
# App::karr::Role::Output: a Moo::Role composes every sub in its package into
# its consumers, imported ones included, so `use App::karr::Error qw( ... )`
# here would quietly make those names methods on every command that composes
# this role. The refusal message is built with command_hint and original_argv,
# both called qualified below.
use App::karr::Error ();
# What this role calls on its consumer, said out loud (ticket #144; the rule is
# ticket #128's, and this is the last of the three mutation-path roles to get
# it). It declared nothing at all until then, and composed cleanly into
# anything, while claim_timeout_secs reads $self->store to find the board's
# configured claim_timeout -- store being App::karr::Role::BoardDiscovery's
# attribute, which every consumer happens to bring along via
# App::karr::Role::BoardAccess. That is the accident, not the guarantee: a
# consumer without it got check_claim regardless, and would have learned about
# the gap from inside a mutation as "Can't locate object method", on the one run
# where a task was actually claimed.
#
# json and quiet joined store in ticket #177, when check_claim stopped being a
# pure decision and started reporting the one case it lets through silently (see
# expired_claim_report). They are the same two names
# App::karr::Role::DependencyCheck declares for the same reason, and every
# consumer of this role already has both -- App::karr::Cmd::Unlock and
# App::karr::Cmd::Pick compose App::karr::Role::Output for json and reach quiet
# through App::karr::Role::SyncLifecycle, and App::karr::Role::TaskMutation's
# five commands do too. That is what kept this a widening of the contract rather
# than the role split ticket #137 needed: there, `create` composed
# DependencyCheck for its set-time half alone and had no --json to declare.
#
# Three names, not more. The other five calls in this file -- _parse_timeout,
# _parse_claim_stamp, _claim_expired, claim_timeout_secs and the _expired_claims
# attribute -- are defined right here, so the consumer never supplies them;
# requiring one would be worse than redundant, since Role::Tiny installs a
# role's methods into the consumer *before* it checks the requires
# (role_application_steps), so the check would find what the composition had
# just put there, in every consumer, always, and read as a promise that had
# been verified when nothing had. Unlike
# App::karr::Role::TaskMutation, which composes two roles and gets check_claim
# and check_dependencies from them, this role composes nothing -- so "the role's
# own" here means only "defined in this file".
#
lib/App/karr/Role/ClaimTimeout.pm view on Meta::CPAN
# not with a second --claim that Getopt::Long would refuse. Both spellings the
# caller could have typed are handled: the space form and the `=` form.
sub _claim_refusal_hint {
my ( $self, $task ) = @_;
my $argv = App::karr::Error::original_argv();
return () unless $argv && @$argv;
my @tokens = @$argv;
my $holder = $task->claimed_by;
my $replaced = 0;
for my $i ( 0 .. $#tokens ) {
if ( $tokens[$i] eq '--claim' ) {
$tokens[ $i + 1 ] = $holder if defined $tokens[ $i + 1 ];
$replaced = 1;
last;
}
if ( $tokens[$i] =~ /\A--claim=(.*)\z/ ) {
$tokens[$i] = '--claim=' . $holder;
$replaced = 1;
last;
}
}
push @tokens, '--claim', $holder unless $replaced;
return @tokens;
}
# 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
=head1 VERSION
version 0.601
=head1 DESCRIPTION
Shared helper role for commands that need to interpret C<claim_timeout> values
and determine whether an existing claim should still block other agents.
C<check_claim> is the one claim-ownership rule in karr. Every command that
mutates an existing task has to apply it, and has to apply it against the same
revision of the task it then writes -- see
L<App::karr::Role::TaskMutation/update_task_guarded>.
=head2 claim_timeout_secs
my $secs = $self->claim_timeout_secs;
In a command class that composes this role, returns the board's configured
C<claim_timeout> in seconds, parsed with the full Go C<time.ParseDuration>
grammar kanban-md writes (e.g. C<1h30m>), not just C<< ^\d+[hms]$ >>. Falls
back to one hour (3600) when the board has no C<claim_timeout> set or the
value does not parse -- except an explicit C<0s>, which is honoured verbatim
and means "claims never expire" (see C<karr unlock>). This is the timeout
L</check_claim> applies; L<App::karr::Cmd::Pick>'s lock timeout is a separate,
shorter fallback and does not go through this method.
=head2 claim_held
$self->claim_held( $task );
$self->claim_held( $task, $secs );
True when somebody holds C<$task> right now: C<claimed_by> is set and not the
empty string, and the claim is not older than the timeout. False when the card
carries no claim, carries C<claimed_by: ""> -- which is kanban-md's way of
writing "unclaimed" and has to be read as one (ticket #59) -- or carries a
claim that has expired and so no longer blocks anybody.
C<$secs> is the claim window in seconds and defaults to
L</claim_timeout_secs>. Pass it explicitly when asking about many cards in one
command run, so one window covers the whole run. C<0> is not the shortest
window but no window at all: on a board with C<claim_timeout: 0s> a claim
never expires, so every claimed card stays held until the claim is released.
This is the only definition of "free" in karr, and both callers of it are
meant to stay callers: L<App::karr::Role::PickRules/pickable> asks it about
the card C<karr pick> is about to hand out, and C<karr list --unclaimed> asks
it about every card on the board. A second spelling of the test is how C<list>
and C<pick> come to disagree about which work is available (tickets #59,
#198).
It is B<not> L</check_claim> with the dying left out. That method answers a
different question -- may I<this caller> write this card -- and its extra
cases say so: the current claimant is let through by name, a card in a
terminal status is not guarded at all, and an expired claim stepped over is
recorded for L</expired_claim_report>. This one asks only whether the card is
held, by anybody, and records nothing.
=head2 check_claim
( run in 1.271 second using v1.01-cache-2.11-cpan-85d3896f969 )