Skip to content

πŸ› fix(contribute): persist task leases across hub restarts (v4 port of #5688) - #5721

Merged
kubestellar-prow[bot] merged 3 commits into
v4from
fix/5681-v4-task-lease-persistence
Sep 2, 2026
Merged

πŸ› fix(contribute): persist task leases across hub restarts (v4 port of #5688)#5721
kubestellar-prow[bot] merged 3 commits into
v4from
fix/5681-v4-task-lease-persistence

Conversation

@clubanderson

Copy link
Copy Markdown
Member

What

v4 port of #5688 (authored by Danathar β€” the fix and its analysis are his work; this PR carries his commits with authorship preserved). The live hub runs v4 and loses every in-flight contributor task lease on each restart, which today's :stable roll churn makes a recurring interruption rather than a rare one.

A relay holds one task at a time and keeps working through a brief disconnect, re-asserting the task on reconnect. The hub honours that re-assertion only against a server-issued lease β€” but leases lived only in process memory, so every restart emptied the registry, every in-flight resume was answered no active lease for this task, the agent was interrupted mid-turn, and the identical issue was handed back seconds later. The registry is now persisted to /data/contributors/task-leases.json on every assignment, renewal and release, and restored at startup.

Fixes #5681

Correctness properties preserved from the v5 review

All verified on v4 by the ported test suite (10 cases, all passing):

  • Generation fence: taskGen is re-derived past every restored lease at boot, so a post-restart assignment can never mint a fencing token that aliases a pre-restart one and slip a straggler past the generationAccepted gate β€” pinned by TestLeaseRestart_GenerationAdvancesPastRestoredLeases.
  • Bounded grace hold: for 2 minutes after startup a restored lease holds its work item in the double-assignment guard (TestLeaseRestart_RestoredLeaseHoldsItsIssue), and the hold lapses after the grace window (TestLeaseRestart_HoldLapsesAfterGrace) β€” a lease is deliberately not a hold in steady state.
  • Persist under leaseMu: snapshot and write happen under the same lock so concurrent mutations cannot land renames out of order and leave the file describing an older registry than memory.
  • 0600 file mode: the registry is the C4 authorization record a resume is matched against, owner-only unlike the 0644 sibling report ledgers (TestLeaseRestart_FileIsOwnerOnly).
  • No resurrection after expiry: expired leases are skipped at save and dropped at load (TestLeaseRestart_ExpiredLeaseIsNotRestored, TestLeaseRestart_RevokedLeaseStaysRevoked); the renewed window is what gets persisted (TestLeaseRestart_RenewedWindowSurvives).
  • Restart round-trip: TestLeaseRestart_ResumeSurvivesHubRestart drives the real protocol through a hub replaced by a freshly constructed one over the same /data, and fails on the incident's own string when the restore is removed.

v4 adaptations

The fix commit cherry-picked cleanly β€” v4's contribute_ws.go carries the same generation fence and lease registry despite v5's dashboard decomposition. The docs commit needed three adaptations:

  1. changelog.d fragment instead of CHANGELOG.md β€” v4 moved to one-fragment-per-PR (πŸ”§ CHANGELOG.md Unreleased appends give every pair of landing PRs a structural conflict β€” move entries to changelog.d/ fragmentsΒ #5675) after the v5 original was written.
  2. agent-state-inventory.md rows dropped β€” that design document does not exist on v4; its load-bearing residual-2 warning (persisting leases without the generation counter silently breaks the fence) is carried in loadLeases' comments and pinned by test instead.
  3. Persist idiom upgraded to the [architect] refactor: crash-safe persist for pkg/convergence/mutation Ledger (unique temp + fsync)Β #5625 standard (third commit) β€” the review noted the fixed-name tmp + rename idiom as a follow-up; since this file is what the next process boots from, the port lands the crash-safe form directly: unique os.CreateTemp name, explicit chmod pinning 0600, fsync of the bytes before the rename, rename, then fsync of the directory. Failed attempts remove their unique temp file. The 0644 sibling report ledgers keep their existing idiom.

Note for v5: item 3 is a delta this port has that v5 HEAD does not β€” v5's saveLeasesLocked still uses the fixed-name tmp + rename idiom and should get the same #5625 upgrade as a follow-up.

Coverage

pkg/dashboard with go test -short -race -count=1 -coverprofile: 92.0% of statements β€” above the package's documented 84% floor and the 90% default.

πŸ€– Generated with Claude Code

Danathar and others added 3 commits September 2, 2026 15:30
A hub restart threw away every in-flight contributor task β€” and then handed the
identical issue back seconds later.

A relay holds one task at a time and keeps working through a brief disconnect,
re-asserting the task when it reconnects. The hub honours that re-assertion only
against a server-issued lease (lookupLease), which is deliberate: C4 established
that a client must never be able to assert ownership of work the server did not
assign. But leases lived only in process memory. A restart emptied the registry,
so after an upgrade NO in-flight resume could match: the relay was told "no
active lease for this task", the revoke interrupted the agent mid-turn, and the
same issue was re-assigned as fresh work.

Observed 2026-09-02 (#5681): revoked at 14:24:40, the same issue reassigned to
the same relay at 14:24:44, discarding two and a half minutes of a turn that was
progressing normally. Thirteen shell commands and a fork/clone in, then killed.
Ownership was never in question β€” only the record of it. Self-upgrade rolls
(#5391) make this routine rather than rare, and it hits every contributor
holding a task at the moment of any restart.

The registry is now persisted to /data/contributors/task-leases.json on every
assignment, renewal and release, and restored at startup.

This does not weaken C4. The restored record is one the SERVER wrote; a resume
still has to match it exactly on {identity, task_id, repo, number, generation}
and still has to be inside the window. Nothing is reconstructed from
client-supplied fields, the file carries no credential (the scoped token is
minted per assignment and delivered separately, #2537), it is written 0600
rather than the sibling ledgers' 0644 because it is an authorization record
rather than a report, and a lease already past its expiry is dropped at load
rather than restored.

Three couplings move with it:

- The RENEWED window is what gets persisted. Persisting only the assignment-time
  window would bring a task that had been progressing for longer than leaseTTL
  back already expired β€” precisely the defect #4260 fixed in memory.
- taskGen is advanced past every restored generation at boot. It is an in-memory
  counter that restarts at zero, so persisting leases without this would let a
  post-restart assignment mint a fencing token that ALIASES a restored one, and
  the #2568 Gate would accept a pre-restart straggler against a brand-new task.
  src/docs/design/agent-state-inventory.md residual 2 called this exact hazard
  out in advance ("any change that persists one without the other silently
  breaks the fence").
- For leaseHoldGraceAfterStart (2 min) after boot, a RESTORED lease also holds
  its work item in the double-assignment guard, which is otherwise built purely
  from live connections and is empty right after a restart. Without it, making
  the resume work would convert "lose the task" into two relays on one issue.
  Deliberately scoped: a lease is not a hold in steady state β€” a dropped socket
  keeps its lease so the relay can resume (#4260) while its item merely cools
  down (#2356), and honouring leases as holds for the full 30-minute TTL would
  silently replace that hedge with a long park on every disconnect. Leases
  minted by this process never act as holds; their holders have live
  connections, which the existing guard already covers.

The lease also now carries the item's canonical worksource key, so the guard
recognises external work (Linear/Jira items carry Number == 0 and put their
identity in Key, #4245) instead of colliding every such item as "repo#0" (#5120).

#4260's contribute_reconnect_resume_test.go pins the resume contract across a
SOCKET drop and passed throughout β€” that reconnect is to a live hub. The new
contribute_lease_restart_test.go exercises the same contract across a PROCESS
boundary, which had no coverage: it drives the real protocol through a hub
replaced by a freshly constructed one over the same /data, and fails on the
incident's own string ("no active lease for this task") when the restore is
removed.

Refs #5681

Signed-off-by: Doug Baggett <doug.baggett@gmail.com>
(cherry picked from commit 2714b1c)
Signed-off-by: Andrew Anderson <andy@clubanderson.com>
- contributor-relay.md: the resume section already documented the disconnect
  case and the exact four-line revoke/reassign symptom. Adds the restart case
  beside it, including that the restored record is still hub-written and still
  matched exactly, and the two-minute post-restart hold.
- changelog.d/fixed-5681-task-lease-restart.md: user-visible β€” in-flight
  contributor work now survives an upgrade roll.

v4 adaptations of the v5 original: the changelog entry lands as a changelog.d
fragment rather than a CHANGELOG.md edit (#5675), and the
agent-state-inventory.md rows are dropped because that design document does not
exist on v4 β€” its residual-2 warning about persisting leases without the
generation counter is carried in loadLeases' comments and pinned by
TestLeaseRestart_GenerationAdvancesPastRestoredLeases instead.

Refs #5681

Signed-off-by: Doug Baggett <doug.baggett@gmail.com>
(cherry picked from commit bb3812a)
Signed-off-by: Andrew Anderson <andy@clubanderson.com>
#5625 idiom)

The lease registry write used the older fixed-name tmp + rename idiom. This
upgrades it to the #5625 standard the mutation claim ledger set: a UNIQUE
os.CreateTemp name (a fixed name lets a non-cooperating process clobber a
commit in flight), an explicit chmod pinning the 0600 owner-only invariant
rather than inheriting it from CreateTemp, an fsync of the bytes before the
rename β€” the whole point of this file is that the next process boots from it,
so the record must be durable, not merely renamed β€” and an fsync of the
directory so the rename itself survives a crash. A failed attempt removes its
unique temp file instead of leaving a stale fixed-name .tmp beside the
registry.

The sibling contributor ledgers (0644 reports, not authorization records)
keep their existing idiom; upgrading them is out of scope for this port.

Refs #5681

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
@kubestellar-prow kubestellar-prow Bot added dco-signoff: yes Indicates the PR's author has signed the DCO. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Sep 2, 2026
@clubanderson clubanderson added lgtm Indicates that a PR is ready to be merged. approved Indicates a PR has been approved by an approver from all required OWNERS files. labels Sep 2, 2026
@kubestellar-prow

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

Approval requirements bypassed by manually added approval.

This pull-request has been approved by:

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubestellar-prow
kubestellar-prow Bot merged commit 83a9cfc into v4 Sep 2, 2026
74 of 79 checks passed
@kubestellar-prow
kubestellar-prow Bot deleted the fix/5681-v4-task-lease-persistence branch September 2, 2026 19:57
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Thank you for your contribution! Your PR has been merged.

Check out what's new:

Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. dco-signoff: yes Indicates the PR's author has signed the DCO. lgtm Indicates that a PR is ready to be merged. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

2 participants