Skip to content

fix(rivetkit): log and surface actor startup failures - #5528

Open
abcxff wants to merge 1 commit into
stack/fix-rivetkit-treat-empty-actor-input-bytes-as-absent-ouvoqtnsfrom
stack/fix-rivetkit-log-and-surface-actor-startup-failures-wyxwxpum
Open

fix(rivetkit): log and surface actor startup failures#5528
abcxff wants to merge 1 commit into
stack/fix-rivetkit-treat-empty-actor-input-bytes-as-absent-ouvoqtnsfrom
stack/fix-rivetkit-log-and-surface-actor-startup-failures-wyxwxpum

Conversation

@abcxff

@abcxff abcxff commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@abcxff

abcxff commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review (updated)

Correcting my earlier pass on this PR after tracing the full call path through rivetkit-core, since I missed something material the first time.

The startup_ready forwarding fix is correct and valuable. Before this change, a startup failure (bad input CBOR, create_state/create/on_create/on_start erroring) hit ? and dropped startup_ready without sending, so the spawn_run_handle await on startup_ready_rx (rivetkit-core/src/actor/task.rs:1345) only ever saw a generic "receive runtime startup ready reply" RecvError, losing the real cause. Wrapping the phase in the startup block and explicitly replying Err(anyhow::Error::new(RivetError::extract(&error))) on failure (start.rs:238-240) fixes that correctly and matches the existing RivetError::extract pattern already used in registry.rs, in build_factory.

However, the new tracing::error! call at start.rs:237 duplicates an existing, more detailed log. run_actor still returns the same Err(error) after replying to startup_ready (start.rs:241). That error is what run_actor returns from factory.start(start), which spawn_run_handle spawns as self.run_handle (task.rs:1330) before it awaits startup_ready_rx. Since build_factory uses new_with_manual_startup_ready (registry.rs:192), requires_manual_startup_ready() returns true for every actor registered through the typed API, so this path is not an edge case.

Once that spawned task completes (immediately, since run_actor returns right after the startup_ready reply), the ActorTask::run_live select! loop picks it up via wait_for_run_handle on the very next iteration; nothing about the failed startup_ready reply causes early termination or skips this poll (lifecycle is already Started, and should_terminate() only checks for LifecycleState::Terminated). handle_run_handle_outcome then matches Ok(Err(error)) => { log_actor_error(&error, "actor run handler failed"); ... } (task.rs:1422-1425), which already logs the same underlying error with group/code/message/metadata extracted via RivetError::extract, a strictly more structured log than the new plain ?error debug dump.

So in the real production path (anything driven through ActorTask, not just the unit test added here, which calls run_actor directly and never touches task.rs), a single startup failure now produces two error-level log lines for the same event: "actor failed to start" (new, unstructured) and "actor run handler failed" (pre-existing, structured). That is redundant log volume and confusing for anyone correlating errors, and it also cuts against the "rivetkit (Rust) is a thin wrapper, no load-bearing logic" layering rule in CLAUDE.md, since logging and observability for actor run failures is already centrally owned by rivetkit-core, via log_actor_error.

Suggest dropping the explicit tracing::error! in start.rs and relying on the existing core-level log for this case, since the actual gap being fixed is the silent drop of startup_ready, not the absence of logging. If distinguishing "failed during startup" from "failed during run" in the log message has real diagnostic value, that is better done inside log_actor_error or handle_run_handle_outcome in core (for example by tagging the error or checking lifecycle phase), so there is one log site instead of two.

Everything else from the earlier pass still holds:

  • The success path is unchanged and behaviorally identical.
  • RivetError::extract reuse (not re-coercion) of already-structured errors is correct.
  • Minor, non-blocking: the test hand-rolled BufMakeWriter/BufWriter pair duplicates the LogCapture/LogWriter pair already in event.rs, in its test module (private to that file, so not directly reusable today). Worth hoisting to a shared test-utils module eventually, not blocking here.

No security concerns. The main actionable item is the duplicate logging described above.

@abcxff
abcxff force-pushed the stack/fix-rivetkit-treat-empty-actor-input-bytes-as-absent-ouvoqtns branch from e9dadf6 to 0283e79 Compare July 31, 2026 06:03
@abcxff
abcxff force-pushed the stack/fix-rivetkit-log-and-surface-actor-startup-failures-wyxwxpum branch from efaf873 to 11b03c3 Compare July 31, 2026 06:03
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