Skip to content

feat(rivetkit): trace sqlite operations - #5727

Merged
NathanFlurry merged 1 commit into
stack/feat-rivetkit-trace-actor-invocations-vmzuwnkkfrom
stack/feat-rivetkit-trace-sqlite-operations-qpnpswll
Sep 23, 2026
Merged

NathanFlurry merged 1 commit into
stack/feat-rivetkit-trace-actor-invocations-vmzuwnkkfrom
stack/feat-rivetkit-trace-sqlite-operations-qpnpswll

Conversation

@eersnington

@eersnington eersnington commented Sep 15, 2026 •

Copy link
Copy Markdown
Member
  • Adding SQLite spans under the action that runs each database operation
  • Reused database handles use the current action's trace, including transactions that update actor state
  • execute_batch gets one span for the whole batch.
  • SQL statements and parameter values are not recorded

@the-company-company the-company-company Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 2 medium-severity findings

Reviewed commit 4a9bbf0.

args,
conn,
scheduled_fire,
invocation_telemetry: _,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Medium · Preserve telemetry for Rust action SQLite calls

The Rust event adapter explicitly discards the telemetry attached by ActorTask here. Rust actors obtain their long-lived Ctx independently of RuntimeEvent, so no call path binds that invocation to Ctx::sql(); its SqliteDb consequently retains invocation_telemetry: None. A traced Rust actor action that executes SQL will emit only rivet.actor.invoke, without the SQLite child spans this change adds.

Carry the invocation telemetry through the Rust action execution boundary (or bind it task-locally while handling the event) so the action's SQLite handle is invocation-bound.

Comment on lines +261 to +263
/// Returns the SQLite handle bound to this handle's invocation.
pub fn invocation_sql(&self) -> SqliteDb {
self.0.sql.clone().with_invocation_telemetry(self.1.clone())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Medium · Trace state transaction SQLite operations

begin_state_transaction still calls self.sql().begin_transaction(...), and ActorStateTransaction retains that resulting transaction. sql() returns the unbound database from the shared inner context, whereas this new helper is the only place that applies the action's telemetry. Thus TypeScript actions using c.db.transaction(..., { experimental: { includeState: true } }) do not emit spans for their begin, execute, commit, or rollback calls.

Use the invocation-bound database when opening a state transaction (and add coverage for the includeState path).

@claude

claude Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Code Review

Reviewed with the repo's /code-review skill (medium effort, correctness-focused). This is a SQLite tracing/telemetry feature for RivetKit.

Findings

1. Queue, schedule, and legacy-KV SQL paths bypass the new tracing (rivetkit-core/src/actor/queue.rs, schedule.rs, context.rs)
These still call the untraced ActorContext::sql() instead of the new invocation_sql(), so rivet.sqlite.* spans are missing for c.queue.push(...), c.schedule.at(...), and legacy KV operations even inside a traced action invocation. The one call site the PR did migrate (state.rs:318, the state-transaction save path) shows the pattern was known, it just wasn't applied to these other subsystems. Worth confirming with the author whether this is intentional initial scope or a gap to close in this PR.

2. Scheduled/cron-fired actions never get an invocation span at all (context.rs:1801, dispatch_scheduled_action at context.rs:1774)
This path hardcodes invocation_telemetry: None and sends ActorEvent::Action directly via try_send_actor_event, bypassing task.rs's DispatchCommand::Action path where ActionInvocationSpan::start is invoked. A recurring scheduled action doing heavy SQL work produces zero root rivet.actor.invoke span and zero rivet.sqlite.* child spans, while the identical handler triggered by a client call is fully traced. This makes scheduled-action performance/errors invisible.

3. The Rust flagship SDK (rivetkit-rust/packages/rivetkit/) drops telemetry entirely, not just NAPI-only in scope
event.rs:98 matches invocation_telemetry: _ when converting ActorEvent::Action to ActionCall, discarding it. Ctx::sql() (context.rs:280) just returns the untraced self.inner.sql(), and Ctx<A> is constructed once at actor startup (start.rs:602), not rebound per invocation. Native Rust actors get zero rivet.sqlite.* spans from this feature even though root rivet.actor.invoke spans work. If SQLite tracing is meant to be TypeScript/NAPI-only for now, that's a reasonable scoping choice, but it's worth being explicit about it (e.g. in the PR description or a follow-up issue) since rivetkit-core is supposed to be the source of truth shared by both SDKs.

4. Performance regression: the NAPI SQL wrapper is rebuilt on every SQL call (rivetkit-typescript/packages/rivetkit/src/registry/napi-runtime.ts:278)
actorSqlExec/Execute/ExecuteBatch/Query/Run/Metrics/TakeLastKvError all funnel through a helper that now calls .sql() fresh on every invocation instead of reusing a memoized NapiSqlDatabase (the removed WeakMap memoization appears gone with no replacement caching). Each call crosses the N-API FFI boundary to construct a new #[napi] class instance and clones SqliteDb (Arc refcount bumps plus two heap clones for actor_id/actor_key), plus two tracing::debug! events per call (construct + drop in database.rs:40-56). An action issuing many sequential c.db.query()/execute() calls turns this from O(1)-per-actor into O(calls)-per-action allocation and FFI-crossing overhead. This looks like the main actionable issue in the PR, worth restoring per-actor memoization of the wrapper (it can still carry per-invocation telemetry context without reallocating the whole wrapper).

5. execute_batch's internal transaction suppresses nested spans (rivetkit-core/src/actor/sqlite/mod.rs:341)
It starts its internal transaction with with_invocation_telemetry(None), so batch calls produce one flat rivet.sqlite.execute_batch span with no per-statement breakdown, while the equivalent db.transaction(async tx => {...}) API produces nested begin/execute/commit child spans for the same underlying operations. This makes two structurally equivalent operations look different in traces and hides which statement in a failing batch actually errored. Lower severity than #4, but a real inconsistency worth fixing or at least noting as a known limitation.

Summary
Findings 1-3 are feature-completeness gaps (silently no-op rather than erroring), worth confirming intended scope with the author. Finding 4 is a genuine performance regression on the hot SQL-dispatch path and is the one I'd block on. Finding 5 is a minor tracing-consistency gap.

No security concerns identified (SQL execution paths are unchanged; this PR only adds tracing spans around existing calls). Test coverage for the new spans wasn't visible in the diff context reviewed, consider adding an assertion that rivet.sqlite.* spans are emitted for at least one action/db call path.

🤖 Generated with Claude Code

@eersnington
eersnington force-pushed the stack/feat-rivetkit-trace-sqlite-operations-qpnpswll branch from 4a9bbf0 to eabdcd1 Compare September 16, 2026 01:18

@the-company-company the-company-company Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 1 medium-severity finding

Reviewed commit eabdcd1.

args,
conn,
scheduled_fire,
invocation_telemetry: _,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Medium · Preserve telemetry for Rust action SQLite calls

The Rust event adapter still discards the telemetry attached by ActorTask here. Rust actors obtain their long-lived Ctx independently of RuntimeEvent, so no execution path binds that invocation to Ctx::sql(); its SqliteDb retains invocation_telemetry: None. A traced Rust actor action that executes SQL therefore emits only rivet.actor.invoke, without the SQLite child spans this change adds.

Carry the invocation telemetry through the Rust action execution boundary (or bind it task-locally while handling the event) so the action's SQLite handle is invocation-bound.

@eersnington

Copy link
Copy Markdown
Member Author

Test coverage gap in telemetry.rs.

bad bot. im testing exported behavior through a real collector

napi-runtime.ts #actorSql no longer caches the DB handle per actor context.

intentional. each operation needs the current invocation's context. the underlying db is still shared

worth confirming it doesn't show up in profiling for SQL-heavy actors.

hpa go brrrrr

scheduled/alarm fires explicitly pass invocation_telemetry: None

schedules followed up in #5729, http stuff in #5731, queues in #5730

ActorContext::with_invocation_telemetry's doc comment says it attributes "schedules and SQLite work" to the invocation

followed up in#5729

worth a one-line comment explaining why

execute_batch intentionally gets one span, including its internal transaction

@eersnington
eersnington force-pushed the stack/feat-rivetkit-trace-sqlite-operations-qpnpswll branch from eabdcd1 to 5347b7a Compare September 16, 2026 18:16

@the-company-company the-company-company Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No issues found

Reviewed commit 5347b7a.

@eersnington
eersnington force-pushed the stack/feat-rivetkit-trace-sqlite-operations-qpnpswll branch from 5347b7a to 4959860 Compare September 16, 2026 18:24
@eersnington
eersnington force-pushed the stack/feat-rivetkit-trace-sqlite-operations-qpnpswll branch from 4959860 to 030a7da Compare September 16, 2026 18:34
@eersnington
eersnington force-pushed the stack/feat-rivetkit-trace-sqlite-operations-qpnpswll branch from 030a7da to 2d19537 Compare September 16, 2026 18:42
@eersnington
eersnington force-pushed the stack/feat-rivetkit-trace-sqlite-operations-qpnpswll branch from 2d19537 to 00f657d Compare September 16, 2026 19:13
@eersnington
eersnington force-pushed the stack/feat-rivetkit-trace-sqlite-operations-qpnpswll branch from 00f657d to 9f70d0d Compare September 16, 2026 20:28
@eersnington
eersnington added this pull request to stack #5746 September 17, 2026 08:36
@eersnington
eersnington force-pushed the stack/feat-rivetkit-trace-sqlite-operations-qpnpswll branch 2 times, most recently from 3ba9516 to 701bd58 Compare September 18, 2026 22:40
@eersnington
eersnington force-pushed the stack/feat-rivetkit-trace-sqlite-operations-qpnpswll branch from 701bd58 to 7493eb7 Compare September 19, 2026 01:01
@eersnington
eersnington force-pushed the stack/feat-rivetkit-trace-sqlite-operations-qpnpswll branch from 7493eb7 to 9b37630 Compare September 22, 2026 15:28

@NathanFlurry NathanFlurry left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed as part of the tracing stack.

@NathanFlurry
NathanFlurry merged commit 1619f25 into main Sep 23, 2026
9 of 22 checks passed
@NathanFlurry
NathanFlurry deleted the stack/feat-rivetkit-trace-sqlite-operations-qpnpswll branch September 23, 2026 08:04
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.

2 participants