Skip to content

Add trace spans to the Rust io-context crate - #8034

Draft
bartlomieju wants to merge 4 commits into
biwanczuk/rust-io-context-await-iofrom
biwanczuk/rust-io-context-trace-spans
Draft

bartlomieju wants to merge 4 commits into
biwanczuk/rust-io-context-await-iofrom
biwanczuk/rust-io-context-trace-spans

Conversation

@bartlomieju

@bartlomieju bartlomieju commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #8030; only the last two commits are new here.

This gives Rust APIs the trace spans that porting MemoryCache.read()
to Rust needs, so the Rust port records the same spans and tags as
the C++ implementation.

API

let mut span = ctx.make_trace_span(c"memory_cache_read")?;
span.set_tag(c"cache_hit", false);
let mut child = span.new_child(c"memory_cache_fallback");
let user_span = ctx.make_user_trace_span(c"memory_cache_read")?;
  • IoContext::make_trace_span() and make_user_trace_span() call
    C++ makeTraceSpan() and makeUserTraceSpan(), so spans are
    children of the span current in the async context frame.
  • SpanBuilder and TraceContext hold the C++ objects and end the
    spans when dropped. Like the C++ types, they stay on the
    IoContext's thread: neither is Send or Sync.
  • SpanBuilder::set_tag(), SpanBuilder::new_child() and
    TraceContext::set_tag() mirror their C++ counterparts.

Strings

C++ expects span names and tag keys to be string literals, which it
does not copy. The Rust API takes them as &'static CStr (c"..."
literals), which cross the bridge with their NUL, so C++ wraps them
in a kj::ConstString with a null disposer, as it does _kjc
literals. A tag value can be a bool, an integer, a float, a literal
(not copied either), or a borrowed string, which is copied only if
the span is observed.

Async tracing

As in C++, a user trace span started just before await_io() is an
operation that the wait is reported under, instead of a bridge of its
own. This works because await_io() shares awaitIo()'s
implementation (#8030).

Tests

io-context-test.c++ records spans through a test SpanObserver:

  • spans and children are recorded under the request's span, with
    tags of each type, and a tag set twice keeps its last value
  • a span ends when its SpanBuilder is dropped
  • spans and tags do nothing when the request is not traced
  • await_io() reports its wait under the operation of a user trace
    span started before it

C++ MemoryCache: fallback_success overwrite

A separate commit fixes a span tag bug in the C++ MemoryCache.read(), which the Rust port has to reproduce tag for tag. The memory_cache_fallback span was tagged fallback_success=true before the fallback's result was validated; when validation then threw (a NaN expiration, or a value that cannot be serialized), the catch handler overwrote the tag with false. The final tags were right, but trace.c++ treats overwriting a tag as a programming error and logs it at ERROR in predictable mode. The tag is now set once the result is accepted. A new test in memory-cache-test.c++ records the spans of two rejected results and checks their tags; without the fix it fails on the overwrite's log.

Rust APIs need it for failures that only a bug causes, such as an
IoContext that is no longer current. Throwing the error gives
JavaScript a generic "internal error; reference = <id>" Error and
logs the message with that ID. A new test checks the redaction.
IoContext::await_io() waits for a Rust future as part of the request,
then runs a callback with its output in the IoContext. It goes
through IoContext::awaitIoAndRun(), a new public variant of awaitIo()
for callers that settle no jsg::Promise. Both now share the I/O half
of awaitIoImpl(), split out as resumeAfterIo(), so the callback runs
like awaitIo()'s does: holding a pending event, in the critical
section and async context frame current when the wait began, and
recorded for async tracing. If the request ends first, the future and
callback are dropped without the callback running.

IoContext::add_object() gives the IoContext ownership of a value, as
C++ addObject() does. It returns IoOwned<T>, from which the value can
only be taken out, rather than used in place as IoOwn<T> is.
IoContext::make_trace_span() and make_user_trace_span() start spans
as C++ makeTraceSpan() and makeUserTraceSpan() do, returning a
SpanBuilder or TraceContext that holds the C++ object and ends the
span when dropped. SpanBuilder::set_tag(), SpanBuilder::new_child()
and TraceContext::set_tag() mirror their C++ counterparts.

Span names and tag keys are &'static CStr, written as c"..."
literals. C++ wraps them without copying, as it does its own string
literals. A tag value can be a bool, an integer, a float, a literal,
or a borrowed string, which is copied only if the span is observed.

A user trace span started before await_io() is adopted by it for
async tracing, as for awaitIo(), and a test checks this.
The memory_cache_fallback span was tagged fallback_success=true
before the fallback's result was validated. If validation then
threw, as for a NaN expiration or a value that cannot be serialized,
the catch handler overwrote the tag with false. The final tags were
right, but overwriting a tag is treated as a programming error,
logged at ERROR in predictable mode.

The tag is now set once the result has been accepted. A test records
the spans of two rejected results and checks their tags, failing on
the overwrite's log without the fix.
@github-actions

Copy link
Copy Markdown

The generated output of @cloudflare/workers-types has been changed by this PR. If this is intentional, run just generate-types to update the snapshot. Alternatively, you can download the full generated types:

Full Type Diff

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