Skip to content

fix: mint a call-unique callKey for RequestContext and ExchangeContext#125

Merged
OmarAlJarrah merged 1 commit into
mainfrom
fix/request-exchange-context-callkey
Jun 16, 2026
Merged

fix: mint a call-unique callKey for RequestContext and ExchangeContext#125
OmarAlJarrah merged 1 commit into
mainfrom
fix/request-exchange-context-callkey

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Jun 16, 2026

Copy link
Copy Markdown
Member

Closes #124

Problem

A CallContext's callKey is the slot under which a call is tracked in ContextStore, and it must be unique per call: ContextStore.put rejects a duplicate key, and set (used on the promotion path) silently overwrites — and so evicts — a live entry if two calls collide. traceId:spanId is not a safe key on its own: the no-op instrumentation context shares constant ids across untraced calls, an inbound W3C trace shares one trace id across its spans, and some tracers reuse a span id across sibling client calls.

DispatchContext already defends against this — its default callKey appends a process-unique counter (traceId:spanId:n). RequestContext and ExchangeContext, however, still defaulted to the plain traceId:spanId derivation.

On the normal promotion path that was harmless: toRequestContext / toExchangeContext pass the head's minted key forward explicitly, so these defaults were never exercised. But a RequestContext or ExchangeContext constructed directly off-chain from instrumentation that shares a trace/span id received a non-unique key, so two such instances collided in ContextStoreput rejects the second, or set clobbers the first and evicts a live entry. The result was an inconsistency: the head of the chain was collision-safe by default while the other two links were not.

Change

Default RequestContext and ExchangeContext to the same minted, call-unique key DispatchContext uses, so every link in the chain is collision-safe by default regardless of how it is constructed. mintCallKey is shared as the common derivation; deriveCallKey is now a private implementation detail of it.

As with DispatchContext, minting a unique default makes two default-constructed instances structurally unequal. The data-class equality tests pin an explicit callKey to construct identical instances, and new tests assert that two directly-constructed contexts sharing a trace and span id get distinct keys and both register in the store without evicting each other.

No public API signature changes (apiCheck clean); the constructors' default-argument values change but their types do not.

DispatchContext's default callKey appends a process-unique counter
(traceId:spanId:n) so it stays unique even when the trace/span pair is
not — the no-op instrumentation context shares constant ids, an inbound
W3C trace shares one trace id across spans, and some tracers reuse a span
id across sibling calls. RequestContext and ExchangeContext, however,
still defaulted to the plain traceId:spanId derivation.

On the normal promotion path this was harmless: toRequestContext and
toExchangeContext pass the head's minted key forward explicitly. But a
RequestContext or ExchangeContext constructed directly off-chain from
instrumentation that shares a trace/span id received a non-unique key,
so two such instances collided in ContextStore — put rejects the
duplicate, and set silently clobbers and evicts the live entry.

Default both to the same minted, call-unique key DispatchContext uses, so
every link in the chain is collision-safe by default. As with
DispatchContext, this makes two default-constructed instances structurally
unequal; the equality tests pin an explicit key, and new tests cover the
shared-trace/span collision case.
@OmarAlJarrah OmarAlJarrah merged commit 53b850c into main Jun 16, 2026
1 check passed
@OmarAlJarrah OmarAlJarrah deleted the fix/request-exchange-context-callkey branch June 16, 2026 22:36
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.

RequestContext and ExchangeContext default to a non-call-unique callKey

1 participant