Skip to content

refactor(rivetkit): expose schedule intervals as durations#5452

Merged
NathanFlurry merged 13 commits into
mainfrom
stack/refactor-rivetkit-expose-schedule-intervals-as-durations-youlrotr
Jul 20, 2026
Merged

refactor(rivetkit): expose schedule intervals as durations#5452
NathanFlurry merged 13 commits into
mainfrom
stack/refactor-rivetkit-expose-schedule-intervals-as-durations-youlrotr

Conversation

@NathanFlurry

Copy link
Copy Markdown
Member

No description provided.

@railway-app
railway-app Bot temporarily deployed to rivet-frontend / production July 20, 2026 08:39 Inactive
@railway-app
railway-app Bot temporarily deployed to rivet-frontend / preview July 20, 2026 08:39 Inactive
@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review: actor scheduling (rivetkit) — #5452

Large stacked PR (+12.8k/-1.8k across ~130 files) adding cron/interval scheduling to actors: rivetkit-core scheduling engine + SQLite storage, an envoy protocol bump (v5 to v6) for batched remote SQLite execution, an inspector-protocol bump for the new Schedules data, NAPI/Rust-wrapper bindings, a new dashboard "Schedules" tab, and doc/example updates. Reviewed by area (core engine, envoy/pegboard protocol, inspector/NAPI/Rust wrapper, frontend, docs/examples). Overall the core scheduling engine and protocol versioning are carefully done (see "Notably good" below) — the following are the concrete issues found, verified against the PR head (8b9b7ad).

Correctness / bugs

  • rivetkit-rust/packages/rivetkit/src/start.rs, handle_actor_event — the new scheduled_fire field on ActorEvent::Action is discarded (..) before dispatching through the action-set dispatcher, and Ctx<A> has no field to carry it. Meanwhile event.rs's RuntimeEvent::from(...) does thread scheduled_fire through, with a passing test (action_call_exposes_scheduled_fire_metadata). So schedule-fire metadata (which action fired due to a cron/interval/at-schedule) is only reachable via the low-level Event/on_event handler, never via the primary #[action] dispatch path most users will use. Worth confirming whether this is intentional preview-API lag or a gap.

  • examples/multiplayer-game-patterns/src/actors/idle/world.ts:148 — still uses the pre-rename field name:

    await c.cron.every({
        name: `collect-production:${buildingId}`,
        intervalMs,   // should be `interval: intervalMs`
        ...
    });

    ActorCronEveryOptions (rivetkit-typescript/packages/rivetkit/src/actor/config.ts:132) requires interval: number with no index signature, per this PR's own "expose schedule intervals as durations" rename. This example is missing the required interval field and has an excess intervalMs — it will fail to type-check as-is.

Security / robustness (untrusted boundary)

  • engine/packages/pegboard-envoy/src/ws_to_tunnel_task.rs — an earlier commit in the stack added MAX_REMOTE_SQL_BATCH_STATEMENTS = 256, but a later commit removes the statement-count cap entirely; validate_remote_sqlite_batch now only re-checks per-statement bind bytes, not the number of statements. The accompanying test explicitly asserts 257 statements should pass ("batch statement count should not have an envoy-only limit"). Per CLAUDE.md, envoy-to-pegboard-envoy is an explicitly untrusted boundary — an envoy-originated batch of many zero-bind-byte statements (e.g. repeated SELECT 1) can drive unbounded sequential work inside a single transaction per request. Recommend reinstating a bounded cap (or documenting why unbounded is acceptable, e.g. if depot enforces a batch limit downstream).

  • rivetkit-rust/packages/rivetkit-core/src/error.rspublic_error_status_code blanket-matches the "schedule" error group to status 400, marking every ScheduleRuntimeError code as client-public, so client_error_message/client_error_metadata pass the raw message straight through. Most schedule codes are genuine user-input errors and fine to expose, but invalid_schedule_row describes storage-corruption/decode failures (e.g. "MIN(trigger_at) was not an integer", "unknown kind {other}") and isn't user-triggerable — internal storage-shape details shouldn't leak to clients. Other groups (actor_runtime_socket, queue) enumerate specific public codes rather than blanket-matching the group; suggest the same here, letting invalid_schedule_row fall through to the sanitized INTERNAL_ERROR path.

Test coverage

  • frontend/src/components/actors/actor-inspector-protocol.test.ts:13vi.mock("reconnectingwebsocket", () => ({ default: class {} })). CLAUDE.md explicitly forbids vi.mock/jest.mock/module-level mocking ("write tests against real infrastructure... vi.fn() for simple callback tracking is acceptable"). The rest of the file's negotiation/version/round-trip assertions are meaningful — only the mock needs removing, likely by restructuring so the test doesn't need to import the module that pulls in reconnectingwebsocket at runtime.

  • frontend/src/components/actors/actor-schedules-tab.test.ts — despite the name, doesn't exercise ActorSchedulesTab/ScheduleDetails at all (no render, no expand/collapse, no delete-confirmation flow). It only re-tests the pure functions already covered by actor-schedules-format.ts's own test file. Row selection, the two-step delete confirmation, and history loading are untested.

  • rivetkit-rust/packages/rivetkit/tests/client.rs, sibling_action — the assertion was loosened from a single expected value to accepting either json!({"from": "from-caller"}) or json!("from-caller") as the args. Accepting two structurally different arg shapes instead of pinning the one correct encoding either masks nondeterministic behavior or an unresolved args-normalization question (relevant given CLAUDE.md's explicit rule that action/event args must always be array-shaped and normalized at the server/source side) — should be tightened to the single correct shape.

Minor / nits

  • rivetkit-rust/packages/rivetkit-core/src/actor/schedule.rs, read_stored_schedule — the match on event.kind ends in wildcard arms (unavoidable given the guarded earlier arms, but as written a future ScheduleKind variant would silently fall through rather than fail to compile). Consider a nested match keyed on event.kind per variant to keep the "no wildcard on enum matches" guarantee.
  • rivetkit-rust/packages/rivetkit-core/src/registry/inspector_ws.rs (inspector_schedules) — reuses timestamp_to_uint (clamp-negative-to-0-and-cast) to encode Schedule.max_history, which is a count, not a timestamp. Works, but the function name is misleading at that call site.
  • frontend/src/components/actors/actor-schedules-tab.tsxuseQuery for schedules/history doesn't destructure isError/error; a failed fetch renders identically to a genuinely empty list (no retry affordance). Also, deleteSchedule resolving false (e.g. already fired/removed) silently closes the panel with no toast, unlike the success path.
  • internal_storage/mod.rs::import_legacy_actor_snapshot inserts one statement per legacy scheduled event with no chunking, unlike the batched pattern used elsewhere in schedule.rs. Low severity since it's a one-time legacy-import path.
  • website/src/content/docs/actors/schedule.mdx mixes tabs into otherwise 2-space code samples (two lines) — cosmetic.

Notably good

  • tests/sql_efficiency.rs (new, 457 lines) covers essentially every new/changed query from internal_storage/queries.rs and schedule.rs with semantic plan assertions (forbidding full scans, temp sorts, automatic indexes) rather than exact EXPLAIN QUERY PLAN text — a careful, correct implementation of the project's SQL-efficiency policy.
  • The envoy-protocol v5-to-v6 bump is done correctly end-to-end: v5.bare is never touched (only v6.bare added), the two schema copies (rust sdk / sdks/schemas) are byte-identical, and the version converters do genuine field-by-field manual conversion with no round-trip shortcuts. New v6-only batch-execution variants correctly fail closed going backward, with direct test coverage.
  • Inspector-protocol v6 similarly downgrades new schedule messages to a structured "inspector.schedules_dropped" error for old clients (matching the existing dropped-message convention) rather than dropping silently, and a signal-ordering fix addresses a real bug where a slow schedule-snapshot query could overwrite a newer state update.
  • rivetkit-napi's new Schedule methods stay pure thin delegations to core — no layering violations found.
  • The deleted examples/docs/actors-schedule/full-example.ts is not an orphaned reference; the docs page was rewritten in the same PR to use inline snippets instead.

Reviewed via multi-agent pass across core engine, protocol/envoy, inspector/NAPI, frontend, and docs/examples; all findings above were independently verified against the PR head commit.

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