NAME
App::karr::Role::SyncLifecycle - Role providing sync lifecycle with retry and guard insurance
VERSION
version 0.601
DESCRIPTION
This role provides sync_before and sync_after methods that wrap Git pull and push operations with retry logic. sync_before creates a App::karr::SyncGuard and retains it on the object as insurance: if the command body dies or croaks before sync_after runs, the guard's DESTROY pushes with 3 retries. Because the guard is held by the role (not by the caller), commands may call both methods in void context; sync_after neutralises the guard so it never pushes twice.
Holding the guard on the command object is also why the CLI cannot rely on DESTROY alone: MooX::Cmd's command chain keeps that object alive past bin/karr's error handler, so on the die path the guard is only reaped in global destruction, where pushing is forbidden. bin/karr therefore drains "flush_armed" in App::karr::SyncGuard from an END block.
Commands that compose this role must also have a store attribute (provided by App::karr::Role::BoardDiscovery) with a git accessor.
"sync_pull_foundation" and "sync_push_foundation" are the same two halves for refs/karr-foundation/* (#190). They share the retry loop rather than copying it -- there is exactly one in this file, and every attempt count, retry banner and --quiet rule is decided in it.
Local-only mode
The --local-only option (k291) makes a writing command write to the board's local refs/karr/* and touch the remote in neither direction: "sync_before" skips the fetch and "sync_after" skips the push. It exists for the multi-board case where a mutating command (move, edit, handoff, create, and every other command that composes this role) would otherwise block in the caller's timeout on an unreachable or silent configured remote, because the fetch and push are inline and each is bounded only by KARR_TRANSPORT_TIMEOUT per attempt over three attempts.
A board with no configured remote already syncs as a clean no-op -- both "pull" in App::karr::Git and "push" in App::karr::Git return early on has_remote without a round trip -- so --local-only is not needed there and changes nothing. It is the reachable-but-slow or unreachable configured remote that it removes from the command's critical path. The default is unchanged: without the option the fetch and push run exactly as before. The refs written under --local-only are canonical local board state; karr sync publishes them once the remote is reachable again.
METHODS
sync_pull
$self->sync_pull; # the ordinary guarded pull
$self->sync_pull( accept_wipe => 1 ); # karr sync --prune
$self->sync_pull( accept_foreign => 1 ); # karr sync --accept-foreign-board
Pulls refs from remote with up to 3 attempts, and nothing else. Output is retry-only: the first attempt is silent, retries are announced from attempt 2 ("Pull retry 2 of 3..."), and errors always reach STDERR. --quiet additionally suppresses the retry announcements but never the errors. Each distinct error is shown once: neither a repeat of the previous attempt's error nor the message that ends the command prints it again.
Any named arguments are handed to "pull" in App::karr::Git unchanged, on every attempt. That is how karr sync's two safety valves reach the pull -- and they are passed only when the caller passes them, so a command that calls this (or "sync_before") bare still gets the wholesale-wipe and board-identity refusals (#82, #95). Those refusals die out of "pull" in App::karr::Git rather than returning false, so they end the pull on the first attempt instead of being retried: they are the remote's state, not a transport failure.
This is "sync_before" without the App::karr::SyncGuard, for the one caller that pulls and is not going to push -- karr sync --pull, where an armed guard would be a push at process teardown that the flag exists to prevent. Everything that pulls in order to write calls sync_before.
sync_before
$self->sync_before;
$self->sync_before( accept_wipe => 1 ); # options reach Git::pull
"sync_pull" plus the insurance: it runs the same retrying pull, forwarding any named arguments to "pull" in App::karr::Git, and then creates a App::karr::SyncGuard, retains it on the object (so it outlives the call and covers the command body), and also returns it for callers that want to manage it explicitly. sync_after clears it on a successful push.
Under --local-only (see "Local-only mode") the pull is skipped and the returned guard is handed back already spent, so the callers that call $guard->done on it still get an object and no push can fire at teardown.
sync_after
$self->sync_after; # push with up to 3 attempts
Pushes refs to remote with up to 3 attempts, using the same retry-only output convention as "sync_before" (silent first attempt, retries announced from attempt 2, errors always on STDERR, --quiet silencing only the announcements). It marks the retained guard done and clears it on both outcomes: after a successful push there is nothing left to insure, and after a failed one the guard's three attempts have just been spent, so re-running them from "flush_armed" in App::karr::SyncGuard would only repeat the failure.
A push the remote rejected per ref (a pre-receive hook, a protected ref) is not retried at all: the connection worked and the far side gave its answer, which the error message carries ref by ref. A rejection that is only contention -- two pushes racing for the same ref, see "push_contention" in App::karr::Git -- is retried like any other transient failure, because the same refspec lands on the next attempt.
Under --local-only (see "Local-only mode") this returns immediately without pushing: the fetch was skipped and the guard already spent in "sync_before", so the local refs stay put until karr sync.
sync_pull_foundation
$self->sync_pull_foundation;
"sync_pull" for refs/karr-foundation/* -- karr-foundation's shared chain, run logs and question mailbox (App::karr::Foundation::ChainStore). Same three attempts, same retry-only output, same --quiet contract, because it is the same loop; only what it calls differs ("pull_foundation" in App::karr::Git).
The terminal message is its own, and that is the point: this half runs behind the board's, so "Nothing was changed" would be a lie about a board that has just been synced. There are no accept_wipe/accept_foreign options to forward, because the fleet namespace has neither guard -- see "pull_foundation" in App::karr::Git.
karr sync is the only caller. The implicit sync every writing command makes ("sync_before", "sync_after") stays board-only: a stale chain costs a planning round, a stale board costs correctness, and nothing outside the fleet should pay for a namespace it does not have.
sync_push_foundation
$self->sync_push_foundation;
"sync_after" for refs/karr-foundation/*, minus the App::karr::SyncGuard: the guard insures a command body that wrote board refs and died, and nothing in a command body writes this namespace. The rejection verdict is the same one "sync_after" uses, contention included, since it is the same remote answering.
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)