NAME
App::karr::Role::ClaimTimeout - Shared claim timeout logic
VERSION
version 0.600
DESCRIPTION
Shared helper role for commands that need to interpret claim_timeout values and determine whether an existing claim should still block other agents.
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 "update_task_guarded" in App::karr::Role::TaskMutation.
claim_timeout_secs
my $secs = $self->claim_timeout_secs;
In a command class that composes this role, returns the board's configured claim_timeout in seconds, parsed with the full Go time.ParseDuration grammar kanban-md writes (e.g. 1h30m), not just ^\d+[hms]$. Falls back to one hour (3600) when the board has no claim_timeout set or the value does not parse -- except an explicit 0s, which is honoured verbatim and means "claims never expire" (see karr unlock). This is the timeout "check_claim" applies; App::karr::Cmd::Pick's lock timeout is a separate, shorter fallback and does not go through this method.
claim_held
$self->claim_held( $task );
$self->claim_held( $task, $secs );
True when somebody holds $task right now: 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 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.
$secs is the claim window in seconds and defaults to "claim_timeout_secs". Pass it explicitly when asking about many cards in one command run, so one window covers the whole run. 0 is not the shortest window but no window at all: on a board with 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: "pickable" in App::karr::Role::PickRules asks it about the card karr pick is about to hand out, and karr list --unclaimed asks it about every card on the board. A second spelling of the test is how list and pick come to disagree about which work is available (tickets #59, #198).
It is not "check_claim" with the dying left out. That method answers a different question -- may 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 "expired_claim_report". This one asks only whether the card is held, by anybody, and records nothing.
check_claim
$self->check_claim( $task, $self->claim ); # $self->claim may be undef
In a command class that composes this role, decides whether $task's existing claim blocks the caller and either returns true or dies with "Task N is claimed by X\n". Five cases, checked in order:
the task is not claimed at all -- always allowed;
$claimantis defined, non-empty, and matches$task->claimed_byexactly -- the current claimant may always proceed;the claim is older than "claim_timeout_secs" -- an expired claim no longer blocks anyone, but it is recorded for "expired_claim_report" so the override does not go unsaid, and it is not cleared as a side effect of asking (that stays kanban-md's behaviour, not karr's -- see the comment above this method for why). A
claim_timeoutof0smeans claims never expire, so on such a board this case never fires at all and nothing is ever recorded;the task sits in a status this board calls terminal ("is_terminal_status" in App::karr::Config) -- a finished card is not being worked on, so the claim on it guards nothing:
claimed_byis kept there as provenance, which is the same readingkarr boardapplies when it prints no claimant on a finished card. Checked after the expiry case above so that a terminal card whose claim had also expired still reports the takeover;otherwise -- the task belongs to someone still working on it, and the call dies rather than silently taking the claim over.
Call it against the same task revision the caller then writes -- see "update_task_guarded" in App::karr::Role::TaskMutation -- since a check made against a stale read can pass or fail against bytes that are no longer there.
expired_claim_report
return { id => $task->id, ..., $self->expired_claim_report( $task->id ) };
Emits whatever "check_claim" recorded about an expired claim it let the caller step over, and returns it as the expired_claim => { held_by => ..., claimed_at => ... } pair for the command's --json payload -- or the empty list when no claim was overridden, so the key is absent rather than null.
The takeover itself is allowed and stays allowed: an expired claim not blocking anybody is what claim_timeout is for. What this adds is the trace it left nowhere. claimed_by is re-stamped by move and handoff on the way through, so without this the previous holder is gone from the card, from STDOUT and from STDERR alike -- while the very same mismatch against a live claim is refused with the holder's name in the message (ticket #177, the behavioural half of #176).
Call it after the write has landed, never from inside the guarded callback, for the reason given at "dependency_report" in App::karr::Role::DependencyCheck: a warning about a mutation that then lost its compare-and-swap is a warning about something that did not happen.
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/karr/issues.
IRC
Join #langertha on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is Copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)