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;

  • $claimant is defined, non-empty, and matches $task->claimed_by exactly -- 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_timeout of 0s means 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_by is kept there as provenance, which is the same reading karr board applies 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)