tracing span context for may coroutines — the
may equivalent of Tokio's Instrument.
tracing's "current span" is a per-OS-thread stack. A may coroutine can yield on one
thread and resume on another, so a span entered across a yield is exited on the wrong
thread: the exit finds nothing to pop there, and a closed span stays on the first thread's
stack. The next span created on that thread is parented on it, tracing-subscriber
asserts "tried to clone a span that already closed", and the coroutine unwinds. Under
load this poisons worker threads for the life of the process (PriceWhisperer,
20 Sep 2026: every push stream dropping; the market pod killed by its liveness probe).
Tokio solves the same problem by entering a task's span only while the task is polled. may has no such hook, so this crate supplies the discipline in three layers:
| Layer | What | Where |
|---|---|---|
| 1 | The rule: never hold an Entered guard across a yield; parent spans and events explicitly |
every microscaler crate on may — ADR-0001 |
| 2 | Coroutine-scoped span context: with_span, current, spawn_in_current_span, child_span! — the span travels with the coroutine, not the thread |
this crate — ADR-0002, Design |
| 3 | Scheduler hooks in the may fork: enter on resume, exit on park, so vanilla Span::current() and #[instrument] work too |
microscaler/may — ADR-0003 |
Layer 1 is in force now (lifeguard c9673d7 / 9e5f12d). Layer 2 is this crate. Layer 3 is optional polish once 2 is everywhere.
0.1.1 — Epic 01 delivered (crate, tests, otel feature) and Epic 02 adopted in
BRRTRouter (12581dd), lifeguard (62cfb69), pw_telemetry and pricewhisperer_push; see
docs/Design.md for the API,
Epic 01 and
Epic 02 for the stories.
[dependencies]
may_tracing = { git = "https://github.com/microscaler/may_tracing.git", tag = "v0.1.1" }The request-span shape — set, spawn with context, child span, event:
use may_tracing::BuilderExt;
// at the request boundary (BRRTRouter's service.rs): the request span becomes the
// coroutine's context. NOT entered.
let request = tracing::info_span!(parent: None, "http_request", path = %path);
let _ctx = may_tracing::set_current(request.clone());
// anywhere below: spans and events parented on it
let q = may_tracing::child_span!(tracing::Level::INFO, "execute_query", sql = %sql);
let rows = may_tracing::with_span(q, || run_query(sql)); // q is the context while it runs
may_tracing::info_in_current!(rows, "query done");
// work that belongs to the request inherits the context at spawn; work that does not is
// detached explicitly
let stream = unsafe {
may_tracing::Builder::new().name("orders-stream".into()).stack_size(256 * 1024)
.spawn_with_context(move || stream_loop.run())
}?;
let worker = unsafe { may_tracing::Builder::new().spawn_detached(pool_worker) }?;Outside a coroutine (a Tokio worker, a test on the main thread) the same calls use a
thread-local slot, so shared code needs no cfg.
A context never crosses a channel by itself. Work handed to a worker thread or another
coroutine through a queue must carry may_tracing::current() along and set_current it
on the other side (lifeguard's pool does this in its job envelope); only spawn_with_context
does it for you, and only at spawn.
Tests that spawn coroutines must install their subscriber with
tracing::subscriber::set_global_default: set_default is thread-scoped and may's worker
threads never see it.
- Never hold an
Entered/EnteredSpanguard across a point that can yield. On may, assume every call into lifeguard, may_minihttp,may::syncoryield_nowcan. - Create spans with an explicit parent:
child_span!(parented on the coroutine context) orspan!(parent: None, ..)for a deliberate root. - Record events with an explicit parent:
info_in_current!and friends. - Choose inheritance at every spawn:
spawn_with_contextorspawn_detached. A rawmay::coroutine::spawnstarts with no context. - The same for OS locks (ADR-0004):
never hold a
std::sync::Mutex/RwLockor block on an OS primitive across a yield; usemay::sync. The failure is a deadlock, not a panic.
scripts/check-span-discipline.sh [--strict] <path>… greps for violations; the four repos run it in CI.
| Feature | Default | What |
|---|---|---|
otel |
off | inject_current, traceparent_of_current, span_with_remote_parent[_str], install_w3c_propagator — W3C trace context against the coroutine context (tracing-opentelemetry 0.32 / opentelemetry 0.31) |
scheduler-hooks |
off | Reserved for ADR-0003; declared, empty |
docs/ADR/ ADR-0001..0004
docs/Design.md the crate design (API, storage, macros, consumers, tests, rollout)
docs/Epics/ 01-span-context (this crate), 02-adoption (the consumers)
src/ context.rs (slot, with_span), spawn.rs, macros.rs, otel.rs
tests/ support/ (recording layer, global subscriber), slot, with_span, spawn,
macros, regression_adr0001 (the ADR-0001 reproduction), soak (nightly), otel
examples/ request_context, otel_propagation (--features otel)
scripts/ check-span-discipline.sh (02.5)