Skip to content

Commit ce514a8

Browse files
committed
better comments
1 parent 5cef81f commit ce514a8

1 file changed

Lines changed: 34 additions & 60 deletions

File tree

  • packages/server-utils/src/integrations/tracing-channel

packages/server-utils/src/integrations/tracing-channel/mysql.ts

Lines changed: 34 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,10 @@ const ATTR_NET_PEER_PORT = 'net.peer.port';
4040
* `context` object. Documented here rather than imported because orchestrion's
4141
* runtime doesn't export it — see `node_modules/@apm-js-collab/code-transformer/lib/transforms.js`.
4242
*
43-
* `arguments` is the *live* args array the wrapper passes to the wrapped function:
44-
* orchestrion splices the user's callback out and inserts its own wrapper at
45-
* the same index before publishing `start`. We mutate that last entry again in
46-
* our `start` hook so the callback (and any nested `connection.query(...)`)
47-
* runs inside `withActiveSpan(parent, …)` — mysql v2 loses ALS state when it
48-
* dispatches callbacks from its socket handler, which would otherwise cause
49-
* nested queries to begin a fresh root trace.
43+
* `arguments` is the *live* args array passed to the wrapped function: orchestrion
44+
* splices the user's callback out and inserts its own wrapper at the same index
45+
* before publishing `start`. The `start` hook re-wraps that entry to restore the
46+
* caller's scope across mysql's async callback dispatch (see below).
5047
*/
5148
interface MysqlQueryChannelContext {
5249
arguments: unknown[];
@@ -76,16 +73,12 @@ const _mysqlChannelIntegration = (() => {
7673
DEBUG_BUILD && debug.log(`[orchestrion:mysql] subscribing to channel "${CHANNELS.MYSQL_QUERY}"`);
7774
const queryCh = diagnosticsChannel.tracingChannel(CHANNELS.MYSQL_QUERY);
7875

79-
// Each `context` object is shared across start/end/asyncStart/asyncEnd/error
80-
// for one call (orchestrion creates one per invocation). We key the span
81-
// off the same identity. WeakMap so we don't leak if a path never reaches
82-
// asyncEnd for some reason.
76+
// Orchestrion creates one `context` object per call, shared across all
77+
// lifecycle hooks. We key both maps off that identity; `WeakMap` so an
78+
// unfinished path can't leak its entries.
8379
const spans = new WeakMap<object, Span>();
84-
85-
// The scope active when the query was issued, keyed by the same per-call
86-
// `context` object. The streamable no-callback path needs it in `end`
87-
// (where `ctx.result` first becomes available) to bind the returned
88-
// `Query` emitter's listeners to it — see the `end` hook.
80+
// The scope active when the query was issued, consumed in `end` to bind
81+
// the streamed `Query` emitter's listeners to it.
8982
const parentScopes = new WeakMap<object, Scope>();
9083

9184
// `subscribe()` requires all five lifecycle hooks. The orchestrion
@@ -101,18 +94,10 @@ const _mysqlChannelIntegration = (() => {
10194
// (ctx.result is the Query
10295
// emitter, no async events)
10396
//
104-
// We end the span on `asyncEnd` for the two callback paths (so the span
105-
// covers the full network round-trip + callback duration). For the
106-
// sync-throw path, `end` finishes the span because `ctx.error` is set
107-
// there. For the streamable no-callback path, `end` finishes by
108-
// attaching `'end'`/`'error'` listeners to `ctx.result` (the returned
109-
// `Query` emitter).
110-
//
111-
// The discriminator between "end fired before any error" and "end fired
112-
// after a sync throw" is whether `ctx.error` is set when `end` runs —
113-
// orchestrion populates it before publishing `error`. The discriminator
114-
// between callback and no-callback is whether `ctx.result` is set — only
115-
// the `wrapPromise` (no-callback) path stores it.
97+
// Where the span closes depends on the path: `asyncEnd` for callbacks (so
98+
// it spans the full round-trip + callback), or `end` for the sync-throw
99+
// and streamable paths. The `end` hook tells those apart via `ctx.error`
100+
// / `ctx.result` — see there.
116101
queryCh.subscribe({
117102
start(rawCtx) {
118103
const ctx = rawCtx as MysqlQueryChannelContext;
@@ -137,25 +122,19 @@ const _mysqlChannelIntegration = (() => {
137122
});
138123
spans.set(rawCtx, span);
139124

140-
// Restore the Sentry/OTel context across mysql's internal callback
141-
// dispatch. The orchestrion transform has already spliced the user's
142-
// callback out of `ctx.arguments` and put its own wrapper
143-
// (`__apm$wrappedCb`) at the same index. mysql v2 drains callbacks
144-
// from a socket data handler — by the time the response arrives, the
145-
// AsyncLocalStorage store backing `getActiveSpan()` no longer
146-
// reflects the caller's context. We re-wrap orchestrion's wrapper so
147-
// the user's callback (and any nested `connection.query(...)` inside
148-
// it) runs with the parent span active again.
149-
//
150-
// This must happen at `start` (we're synchronously inside the
151-
// caller's `connection.query` call, so OTel context is still
152-
// correct). `asyncStart`/`asyncEnd` fire from the same lost context
153-
// as the callback itself, so they're too late.
125+
// Capture the scope while we're still synchronously inside the
126+
// caller's `connection.query` call. mysql v2 drains callbacks and
127+
// emits streamed-query events from its socket data handler, where the
128+
// AsyncLocalStorage store backing the active span no longer reflects
129+
// the caller's context — and `asyncStart`/`asyncEnd` fire from that
130+
// same lost context, so capturing has to happen now.
154131
const scope = getCurrentScope();
155-
156-
// Keep track of the scope that was active when the query was issued.
157132
parentScopes.set(rawCtx, scope);
158133

134+
// Callback path: orchestrion has spliced the user's callback out of
135+
// `ctx.arguments` and put its own wrapper (`__apm$wrappedCb`) at the
136+
// same index. Re-wrap it so the callback — and any nested
137+
// `connection.query(...)` — runs with the captured scope active.
159138
if (ctx.arguments.length > 0) {
160139
const cbIdx = ctx.arguments.length - 1;
161140
const orchestrionWrappedCb = ctx.arguments[cbIdx];
@@ -184,27 +163,22 @@ const _mysqlChannelIntegration = (() => {
184163
// fires `asyncStart`/`asyncEnd`. The returned `Query` is an
185164
// `EventEmitter` that emits `'end'` on success and `'error'` on
186165
// failure — hook those to close the span.
187-
// TODO: streaming spans aren't finished on `connection.destroy()` —
188-
// mysql guarantees no further events/callbacks for a destroyed
189-
// connection, so neither `'end'` nor `'error'` fires and the span
190-
// never ends (it's dropped, never reported). Closing this gap needs
191-
// connection-level lifecycle hooks, which the per-query channel
192-
// context doesn't expose here. The `WeakMap` still prevents a leak.
166+
// Note: a streamed span never finishes if the connection is destroyed
167+
// mid-flight — mysql then emits neither `'end'` nor `'error'`, so the
168+
// span is dropped (the `WeakMap` still prevents a leak). Closing this
169+
// needs connection-level hooks the per-query context doesn't expose.
193170
const result = ctx.result;
194171
if (result && typeof result === 'object' && hasOnMethod(result)) {
195172
const span = spans.get(rawCtx);
196173
if (!span) return;
197174

198-
// Bind the streamed `Query`'s listeners to the scope that was active
199-
// when the query was issued. mysql v2 emits `'end'`/`'error'`/
200-
// `'fields'`/… from its socket data handler, where the
201-
// AsyncLocalStorage store backing `getActiveSpan()` no longer
202-
// reflects the caller's context. Without this, a span started inside
203-
// a user's stream listener would begin a fresh root trace instead of
204-
// nesting under the active parent. `bindScopeToEmitter` patches the
205-
// emitter's `on`/`addListener`/… so listeners the user adds after
206-
// `query()` returns inherit the scope — mirroring what
207-
// `@opentelemetry/instrumentation-mysql` does via `context.bind`.
175+
// Bind the captured scope to the streamed `Query` emitter: its
176+
// `'end'`/`'error'`/`'fields'`/… events fire from mysql's socket
177+
// handler with the caller's context lost, so without this a span
178+
// started in a user's stream listener would begin a fresh root trace
179+
// instead of nesting under the parent. `bindScopeToEmitter` patches
180+
// `on`/`addListener`/… so listeners added after `query()` returns
181+
// inherit the scope (like OTel's `context.bind`).
208182
const parentScope = parentScopes.get(rawCtx);
209183
if (parentScope) {
210184
bindScopeToEmitter(result, parentScope);

0 commit comments

Comments
 (0)