Skip to content

Stop saves pending trust changes and no longer waits on a hung RPC - #48

Merged
TeoSlayer merged 3 commits into
mainfrom
fix/stop-final-save
Oct 8, 2026
Merged

TeoSlayer merged 3 commits into
mainfrom
fix/stop-final-save

Conversation

@TeoSlayer

Copy link
Copy Markdown
Contributor

Problem

Manager.Stop could lose trust changes and could block shutdown.

  • Lost changes:
    • drainSaves was untracked, and Stop closed done without waiting for it or doing a final save. drainSaves picks randomly between dirty and done, so a pending save was dropped about half the time.
    • A change made by an RPC goroutine while Stop waited was never saved.
    • Probe on main: 8 relayed changes, then Stop, 200 times; trust.json was incomplete in 48–75 of 200 runs.
  • Unbounded wait: wg.Wait() had no limit. A registry RPC can take up to its 30 s read deadline, against the daemon's 8 s shutdown budget.
  • Double Stop: Stop is called twice (the daemon, then the plugin runtime), and two saves could collide on trust.json.tmp.

Fix

  • Stop waits for RPC goroutines for at most 2 s. That deadline is shared by every Stop call.
  • It then closes done once, waits for drainSaves to exit, and saves.
  • Saves are serialised (saveMu).
  • Stop saves only if a change is still unsaved. A trust file that failed to parse at startup is left exactly as it was, instead of being overwritten with an empty state on start and stop.
  • Stop()'s signature is unchanged.

Tests

  • 200 runs of changes followed by Stop: every entry is on disk.
  • A trust change made by an in-flight RPC during Stop is saved.
  • A hung RPC: Stop returns in about 2 s and still saves.
  • Concurrent Stop calls: no .tmp is left and the file is complete.
  • An unparseable trust.json is unchanged after Stop.
  • No changes: Stop doesn't write (the mtime is unchanged).
  • A failed save is retried by Stop.

The first two fail on main. go test -race ./... passes, and the new tests pass 10 of 10 runs.

🤖 Generated with Claude Code

Teo Calin and others added 2 commits October 8, 2026 11:53
Stop closed done before waiting for the background RPCs, never waited
for the drain goroutine, and never saved on its own. drainSaves picks
between a pending dirty signal and done at random, so a queued save was
dropped about half the time, and a change made by an RPC that finished
while Stop waited for it (backfillPeerKey binding or dropping a record)
was never saved. Eight relayed approvals followed by Stop left
trust.json incomplete in 48 to 58 of 200 runs.

Stop now waits for the RPCs first, then closes done, waits for
drainSaves to exit (drainDone), and saves once more itself. The RPC wait
is bounded: the first Stop sets a deadline of now + stopRPCWait (2 s),
because a registry RPC can sit in its 30 s read deadline and the
daemon's whole shutdown budget is shorter. Later calls share that
deadline, so the daemon's second Stop does not wait again, but every
call saves, so the second one picks up what a late RPC changed.

saveTrust now holds saveMu, so the drain goroutine and concurrent Stop
calls never stage their writes in trust.json.tmp at the same time.

The Stop signature is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stop's final save was unconditional, so a plain start and stop rewrote
trust.json. A trust.json that failed to parse at startup was replaced
with the empty state, where before it survived until the next real
trust change.

markDirty now also sets unsaved, an atomic.Bool meaning "changed since
the last successful save". A save clears it under saveMu and mu.RLock,
before taking its snapshot, and sets it again if the write fails. Stop
saves only if it is set, and checks it under saveMu, so a concurrent
Stop that skips has waited out the save in progress and sees whether it
failed. drainSaves and direct saveTrust calls are unchanged.

New tests: an unparseable trust.json is byte-for-byte unchanged by a
start and two Stops; with no change, Stop leaves an existing file's
mtime alone and does not create a missing one; a failed save leaves the
change unsaved and Stop writes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TeoSlayer
TeoSlayer merged commit 0da018e into main Oct 8, 2026
4 checks passed
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