Skip to content

fix(rivetkit): persist state across development restarts - #5614

Open
NathanFlurry wants to merge 1 commit into
mainfrom
stack/fix-rivetkit-persist-state-across-development-restarts-lnqstwpq
Open

fix(rivetkit): persist state across development restarts#5614
NathanFlurry wants to merge 1 commit into
mainfrom
stack/fix-rivetkit-persist-state-across-development-restarts-lnqstwpq

Conversation

@NathanFlurry

@NathanFlurry NathanFlurry commented Aug 28, 2026

Copy link
Copy Markdown
Member
  • Schedule Rust actor state saves when typed state is mutated, matching the TypeScript runtime.
  • Avoid scheduling redundant saves while hydrating initial or snapshot state.
  • Add regression coverage for mutation-triggered save scheduling.

@NathanFlurry

Copy link
Copy Markdown
Member Author

Stack for rivet-dev/actors

Get stack: forklift get 5614
Push local edits: forklift submit
Merge when ready: forklift merge 5614

change lnqstwpq

@railway-app

railway-app Bot commented Aug 28, 2026

Copy link
Copy Markdown

🚅 Deployed to the actors-pr-5614 environment in rivet-frontend

Service Status Web Updated (UTC)
kitchen-sink 😴 Sleeping (View Logs) Web Aug 28, 2026 at 8:19 am
frontend-inspector 😴 Sleeping (View Logs) Web Aug 28, 2026 at 8:14 am
frontend-cloud 😴 Sleeping (View Logs) Web Aug 28, 2026 at 8:13 am
ladle ✅ Success (View Logs) Web Aug 28, 2026 at 8:09 am
mcp-hub ✅ Success (View Logs) Web Aug 28, 2026 at 8:07 am
website ❌ Build Failed (View Logs) Web Aug 28, 2026 at 8:07 am

@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Review

Small, focused fix: schedules a core request_save whenever typed state is mutated via state_mut()/set_state(), and routes boot/snapshot hydration through a new set_initial_state path so the fix doesn't cause a redundant save at startup. The mechanics look correct:

  • StateMut::drop explicitly drops the write guard (drop(self.guard.take())) before calling request_save, so the core save-request path never runs while the RwLock write guard is still held — good, avoids a needless lock-hold-across-call.
  • request_save (rivetkit-core) is a cheap, self-coalescing fire-and-forget hint (atomics + an unbounded channel send, deduped when a non-immediate save is already pending), so calling it unconditionally on every state_mut() drop — even read-only uses, since state_mut() already marks dirty unconditionally today — isn't a perf concern.
  • Routing run_actor's startup and set_state_from_snapshot through set_initial_state (which skips request_save) correctly avoids the redundant-save-on-hydrate problem described in the PR body.

Two things worth addressing:

  1. Stale doc: rivetkit-rust/packages/rivetkit/CLAUDE.md still says:

    State changes through Ctx::state_mut() / Ctx::set_state() mark persisted state dirty automatically. Use request_save() only when an explicit save point is needed.

    This PR makes request_save() fire automatically from both of those paths, so "use request_save() only when an explicit save point is needed" is now inaccurate — a reader would conclude saves are still opt-in. Per this repo's CLAUDE.md conventions (keep design-constraint docs in sync with behavior), this bullet should be updated in the same change to describe the new automatic-save behavior.

  2. Test coverage gap: the new test (state_cell_reads_writes_and_tracks_dirty) covers the "mutation schedules exactly one save" case well, but nothing exercises the second half of the PR description — "avoid scheduling redundant saves while hydrating initial or snapshot state." No test wires an on_request_save counter through set_initial_state (the run_actor boot path in start.rs) or set_state_from_snapshot to assert that hydration does not trigger a save request. Right now that guarantee is only verified by reading the code, not by a test — worth adding a case analogous to the existing one but asserting save_requests == 0 after set_initial_state/set_state_from_snapshot.

Nothing else stood out — no security concerns (purely internal actor-state bookkeeping), and the drop-guard restructuring (Option<MappedRwLockWriteGuard> + .expect("state guard already dropped")) is a reasonable way to let Drop take the guard while keeping Deref/DerefMut panicking only in an unreachable state.

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