Skip to content

fix(rivetkit): defer initialized persist until state exists#4974

Merged
NathanFlurry merged 1 commit intomainfrom
rivetkit-state/defer-initialized-persist
May 5, 2026
Merged

fix(rivetkit): defer initialized persist until state exists#4974
NathanFlurry merged 1 commit intomainfrom
rivetkit-state/defer-initialized-persist

Conversation

@NathanFlurry
Copy link
Copy Markdown
Member

Description

Please include a summary of the changes and the related issue. Please also include relevant motivation and context.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Copy link
Copy Markdown
Member Author

NathanFlurry commented May 5, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 5, 2026

Code Review

Overview

This PR fixes a race condition where has_initialized was being persisted before a new actor's state was ready, specifically when the factory uses requires_manual_startup_ready(). The fix defers the initialization persist until after spawn_run_handle completes for new actors that gate on startup_ready. A companion change in the NAPI layer normalizes empty-byte snapshots to None for stateful actors (those with a create_state callback), treating them as new.


Issues

_ => fall-through in test enum match (tests/task.rs)

The test callback's event loop uses a wildcard arm:

_ => {}

Per CLAUDE.md conventions, _ => fall-throughs on Rust enums are prohibited so that adding a new variant forces a compile error. Either enumerate every ActorEvent variant explicitly, or use _ => unreachable!() if unhandled events are genuinely impossible in the test scenario.


Redundant PERSIST_DATA_KEY redefinition (rivetkit-napi/tests/napi_actor_events.rs)

const PERSIST_DATA_KEY: &[u8] = &[1];

This replaces an import that was removed. If the real key ever changes, this local copy will silently diverge and tests will pass against a stale key. Either keep the import from rivetkit_core::actor::state (even if it requires a pub(crate) visibility bump), or add a comment explaining why re-definition is intentional.


Suggestions

Missing comment on the deferred-init condition (task.rs, line ~1208)

if !is_new || !self.factory.requires_manual_startup_ready() {

The invariant — that initialization must not be persisted until the runtime's startup_ready handshake completes — is non-obvious. Per CLAUDE.md, this is exactly the case that warrants a short explanatory comment.


Consider documenting the fallback path after spawn_run_handle

if !self.ctx.persisted_actor().has_initialized {
    self.ctx.set_has_initialized(true);
}

The guard exists to avoid double-setting when the runtime callback already called set_has_initialized(true). A brief comment documenting this as the guaranteed fallback (not the expected path) would help future readers understand why the condition exists rather than an unconditional set.


normalize_startup_snapshot lacks a WHY comment

The logic of treating empty bytes as None only when has_create_state is true is subtle. An empty-bytes snapshot is not an obvious sentinel value. A one-line comment explaining the invariant (e.g. "an empty snapshot produced before create_state runs indicates the actor was interrupted mid-initialization") would satisfy the CLAUDE.md standard for non-obvious behavior.


Positives

  • The test manual_startup_does_not_mark_initialized_before_runtime_preamble is well-structured and directly exercises the observable invariant (runtime sees has_initialized == false, then task persists it after startup_ready).
  • startup_snapshot_recovery_only_treats_empty_stateful_snapshot_as_new covers all four cases cleanly as a pure unit test.
  • Using oneshot::channel to observe the in-callback state is the right approach — no polling, deterministic ordering.

@NathanFlurry NathanFlurry changed the base branch from conn-preflight/no-disconnect-on-failed-preflight to graphite-base/4974 May 5, 2026 13:09
@NathanFlurry NathanFlurry force-pushed the rivetkit-state/defer-initialized-persist branch from de6ffdd to b8cd998 Compare May 5, 2026 13:15
@NathanFlurry NathanFlurry force-pushed the graphite-base/4974 branch from e987ebe to 342f41d Compare May 5, 2026 13:15
@NathanFlurry NathanFlurry changed the base branch from graphite-base/4974 to conn-preflight/no-disconnect-on-failed-preflight May 5, 2026 13:15
@NathanFlurry NathanFlurry marked this pull request as ready for review May 5, 2026 13:21
Base automatically changed from conn-preflight/no-disconnect-on-failed-preflight to main May 5, 2026 14:58
@NathanFlurry NathanFlurry merged commit b8cd998 into main May 5, 2026
6 of 22 checks passed
@NathanFlurry NathanFlurry deleted the rivetkit-state/defer-initialized-persist branch May 5, 2026 14:58
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