Skip to content

feat(rivetkit): isolate includeState transaction reads with a committed snapshot - #5638

Open
abcxff wants to merge 1 commit into
mainfrom
stack/feat-rivetkit-isolate-includestate-transaction-reads-with-a-committed-snapshot-snxxvxko
Open

feat(rivetkit): isolate includeState transaction reads with a committed snapshot#5638
abcxff wants to merge 1 commit into
mainfrom
stack/feat-rivetkit-isolate-includestate-transaction-reads-with-a-committed-snapshot-snxxvxko

Conversation

@abcxff

@abcxff abcxff commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

An includeState state transaction mutated actor state in place, so a
concurrent action reading state mid-transaction observed the owner's
uncommitted writes (a dirty read); the writes only reverted on rollback. That
gave atomic commit but not read isolation.

Take a structured-clone snapshot of the committed state when the transaction
opens. The transaction owner keeps mutating the live state (so commit/rollback,
onStateChange, and retained-proxy semantics are unchanged), but every non-owner
context — actions, runtime save ticks, the inspector, sleep saves — reads the
snapshot instead. Concurrent readers therefore observe only committed values
until the owner commits, and a save driven from a non-owner context can no
longer serialize uncommitted state. The snapshot is torn down on transaction
exit.

Note: the driver-suite state-transaction tests require the native engine and
could not be run in this environment (they fail identically on unmodified
main); validated by typecheck, the mock-provider unit tests, and review.

@abcxff

abcxff commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Stack for rivet-dev/actors

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

change snxxvxko

@railway-app

railway-app Bot commented Sep 2, 2026

Copy link
Copy Markdown

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

Service Status Web Updated
website ❌ Build Failed (View Logs) Web Sep 3, 2026 at 4:37 pm UTC
frontend-inspector 😴 Sleeping (View Logs) Web Sep 2, 2026 at 6:01 pm UTC
kitchen-sink 😴 Sleeping (View Logs) Web Sep 2, 2026 at 5:59 pm UTC
frontend-cloud 😴 Sleeping (View Logs) Web Sep 2, 2026 at 5:59 pm UTC
ladle ✅ Success (View Logs) Web Sep 2, 2026 at 5:53 pm UTC
mcp-hub ✅ Success (View Logs) Web Sep 2, 2026 at 5:50 pm UTC

@claude

claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Review

This is a well-scoped fix: includeState transactions now snapshot committed actor state at transaction start (native.ts:3099-3113) and route non-owner reads through that snapshot (native.ts:3465-3481), while the owner keeps mutating live state so commit/rollback/onStateChange semantics are preserved. The teardown in the finally block (native.ts:3150-3152) correctly restores live reads after commit or rollback. New test coverage (actor-db.test.ts) exercises the concurrent-read-during-transaction path directly.

Bug: connection state is not covered by the isolation fix

config.ts:39-45's updated doc comment now claims includeState "atomically includes actor and hibernatable connection state" and that concurrent reads "observe the committed state ... never the transaction's uncommitted writes." That guarantee is only actually implemented for actor state.

NativeConnAdapter#readState() (native.ts:1420-1434) reads connState.state directly from the shared persistState.connStates map and has no check against committedStateSnapshot — that mechanism was wired only into ActorContextHandleAdapter#readState() (native.ts:3465-3481). So if the transaction owner writes conn.state.x = "uncommitted" inside an includeState transaction (the same pattern the existing stateTransactionRejectsRetainedConnStateAfterRollback fixture uses), a concurrent non-owner action reading conn.state.x while the transaction is open still observes the dirty, uncommitted value — the exact dirty-read bug this PR sets out to fix, just for connection state instead of actor state.

Since connectionStateBaselines already exists as a rollback baseline per connection, extending the same snapshot-on-read pattern to NativeConnAdapter#readState() (checking whether a transaction is active and the reading context isn't the owner, similar to the actor-state check) should close the gap. Worth adding a driver test parallel to the new "exposes only committed state to concurrent reads during a state transaction" test, but against conn.state instead of c.state.

Minor

  • The PR description flags that driver-suite state-transaction tests couldn't be run locally (native engine required) — worth confirming CI actually exercises them before merge, since this change touches concurrency-sensitive read paths.

Nice fix on the core actor-state race; the connection-state gap is worth closing in the same PR since the updated doc comment currently overclaims the guarantee.

…ed snapshot

An includeState state transaction mutated actor state in place, so a
concurrent action reading state mid-transaction observed the owner's
uncommitted writes (a dirty read); the writes only reverted on rollback. That
gave atomic commit but not read isolation.

Take a structured-clone snapshot of the committed state when the transaction
opens. The transaction owner keeps mutating the live state (so commit/rollback,
onStateChange, and retained-proxy semantics are unchanged), but every non-owner
context — actions, runtime save ticks, the inspector, sleep saves — reads the
snapshot instead. Concurrent readers therefore observe only committed values
until the owner commits, and a save driven from a non-owner context can no
longer serialize uncommitted state. The snapshot is torn down on transaction
exit.

Note: the driver-suite state-transaction tests require the native engine and
could not be run in this environment (they fail identically on unmodified
main); validated by typecheck, the mock-provider unit tests, and review.
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