Skip to content

feat: per-job max_runtime to cap rsync/rclone wall-clock duration#16

Merged
jkleinne merged 13 commits intomainfrom
feat/per-job-timeout
Apr 18, 2026
Merged

feat: per-job max_runtime to cap rsync/rclone wall-clock duration#16
jkleinne merged 13 commits intomainfrom
feat/per-job-timeout

Conversation

@jkleinne
Copy link
Copy Markdown
Owner

Summary

Adds a max_runtime = "2h" field on [[job]] entries that caps the wall-clock duration of each rsync/rclone invocation. When a job exceeds its budget, Shuttle kills the tool and reports a new ✗ timed out status (counts as a failure, exit 1). Applies per invocation: a job with N rsync sources or N rclone remotes gets N independent timeouts. CleanupArchives deliberately keeps the parent context so housekeeping can't fail a job.

The classification helper (classifyExitStatus) distinguishes deadline-exceeded contexts from parent cancellation, so SIGINT/SIGTERM still maps to exit 130 — only the per-job deadline produces StatusTimedOut.

jkleinne added 13 commits April 18, 2026 14:34
New status constant for jobs killed by per-invocation deadline. IsFailure
returns true so Summary.HasErrors and the CLI exit-code path treat it as
a failure. statusSymbol maps it to ✗ (red); itemStatsText renders "timed out"
(red when color enabled). aggregateStatus already classifies any IsFailure
status as StatusFailed at the aggregate level — no changes needed there.
Adds classifyExitStatus(ctx, runErr) in runner.go. Both RsyncExecutor
and RcloneExecutor route their cmd.Start and cmd.Wait exit paths through
it: context.DeadlineExceeded maps to StatusTimedOut; context.Canceled
and other errors remain StatusFailed.

Covers both the Start path (already-expired context rejects the spawn)
and the Wait path (deadline fires mid-run). Five-subtest classifier unit
test pins the "deadline first then parent cancel" ordering guarantee;
two executor integration tests use WithDeadline(past) to exercise the
full path.
Adds jobContext helper that wraps the parent context with WithTimeout
when max_runtime is set, or returns the parent unchanged (with a no-op
cancel) when it is empty. Wires the derived context into runRsyncJob
(synchronous cancel per source loop iteration) and runRcloneJob (defer
cancel). CleanupArchives keeps the parent context unchanged.
@jkleinne jkleinne merged commit 6896ced into main Apr 18, 2026
2 checks passed
@jkleinne jkleinne deleted the feat/per-job-timeout branch April 18, 2026 22:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant