Trace gatekeeper session resolution and stamp identity on loopback invocations - #145
Trace gatekeeper session resolution and stamp identity on loopback invocations#145ndisidore wants to merge 3 commits into
Conversation
…vocations
Production analysis of GatekeeperLoopback hangs ("code had hung" cancellations
in bursty fan-outs correlated with Jira) showed canceled invocations carry no
vendor/caller identity and don't localize which stage stalled.
- Denormalize vendorId into GatekeeperLoopbackProps so the loopback can stamp
it on its own invocation's trace.
- Emit a zero-duration gatekeeper.loopback.identity span (vendorId, target,
callerFrom, chatId, callerGadgetId): an unended span's attributes are never
streamed — flushed only at spanClose, pinned by a new tail-recorder test —
so hung invocations need a separately-closed stamp to stay identifiable.
- Add createPipelinedTracer to backend-utils: spans around pipelined RPC calls
that return the RpcPromise unchanged (traced()'s .catch wrapper would
collapse pipelining). Used for the gatekeeper.loopback.resolve stage span.
- Wrap GatekeeperClientImpl.openSession in a gatekeeper.session.open span with
a bounded gatekeeper.session.open.failed log event, splitting "stalled
before/at session open" from "stalled in the downstream connector call".
- Add callerFrom/callerGadgetId to the Workshop observability vocabulary.
This comment was marked as outdated.
This comment was marked as outdated.
…recorder between tests
This comment was marked as outdated.
This comment was marked as outdated.
There was a problem hiding this comment.
Review: Trace gatekeeper session resolution and stamp identity on loopback invocations
Reviewed all five files and verified locally (deps installed, checks run).
Verified locally
pnpm --filter @gadgets/backend-utils types:check— passpnpm --filter @gadgets/workshop-backend types:check— pass (format-blueprints generator ran first)pnpm --filter @gadgets/backend-utils test— 30/30 passed, including thetracedPipelinedreference-identity, settle, sync-throw, reject, and the unended-span visibility pin. (TheError: reporter downline is expected test output pervitest.config.ts.)pnpm lint:check— exit 0, 0 errors; only pre-existingno-shadow/consistent-function-scopingwarnings elsewhere, none on lines this PR touches.
Correctness confirmed
createPipelinedTracerpreserves Cap'n Web pipelining.startActiveSpan+ manualend()returnsresultby reference;expect(result).toBe(original)pins it, so the loopbackProxy(overseer.ts:6887) still pipelinesstartGatekeeperSession(...). This is the right choice overtraced, whose.catchwrapper (tracing.ts:41) would replace the RpcPromise with a new promise and collapse pipelining.- Dual-span design is justified. The unended-span test (tracing.test.ts:128-154) genuinely pins that a never-settling pipelined span streams
spanOpenbut drops attributes (they flush only at close), which is exactly why the separately-endedgatekeeper.loopback.identitystamp exists to identify a hung invocation. openSessionawaitis safe.startGatekeeperSession(overseer.ts:2595) already returnsPromise<any>and loopback consumers pipeline onto its resolution; the addedawaitonly folds session-open duration into that resolution — precisely what the span measures. No caller-side pipelining is lost.- Constructor-per-invocation is correct. The identity/resolve spans live in the
GatekeeperLoopbackconstructor, which (per the comment at overseer.ts:6848) runs on each method call against the ServiceStub, matching the PR's "every loopback invocation stamps" intent. - Types read as consistent.
bindingTargetIdis aWorkpieceId(number) sosetAttributeis fine; thefieldsobject inopenSessionuses only validWorkshopObservabilityFieldskeys with correct types (callerGadgetId: number,outcome: "error",durationMs).
Non-blocking notes (all previously raised, addressed in follow-up commits; I concur)
- The
resolvespan's side observervoid result.then(...)(tracing.ts:73) subscribes to the top-level RpcPromise, materializing the session stub locally that pure-pipelining consumers would otherwise skip. The updated comment (tracing.ts:71-72) correctly notes the RPC return message flows regardless, so the observation is off the critical path — accurate. stampIdentitypasses possibly-undefinedvendorIdstraight tosetAttribute(relying on undefined-as-no-op), bypassingstampContext's primitive guard. Fine per thevalue?: Attributesignature, just asymmetric with the helper.bindingTargetId(identity span) andgatekeeperId(session.open span) name the same workpiece id — now called out in the code comment (overseer.ts:6867-6868), good for the next investigator.span-recorderclear()is now invoked at the top of the unended-span test, addressing the earlier cross-test accumulation concern.
Verdict: LGTM. Clean, well-scoped observability change; types, tests, and lint are green locally.
|
Review posted to PR #145. SummaryI independently verified this PR rather than relying on CI — installed deps and ran everything: Checks run locally:
Correctness confirmed:
Verdict: LGTM — a clean, well-scoped observability change. All four earlier non-blocking notes were addressed in follow-up commits or are defensible as-is. |
|
I'm not necessarily against adding more trace info, but if we're specifically chasing the "Worker's code had hung and would never generate a response." errors, I think it's not worth it. I'm pretty confident this is a runtime bug that has no visible impact: the error is being reported spuriously at the time the event was about to end anyway. I would guess that this error is actually being produced commonly in local testing, even, but it doesn't get reported anywhere since there's no tail worker installed. We should try installing one and see what happens, then if we can reproduce, report it to the runtime team. |
Looking at production trace data, nearly every retained
GatekeeperLoopback.jsrpcrecord was a "code had hung" cancellation, arriving in bursts tied to agent-initiated gatekeeper calls. The canceled invocations carry no vendor or caller identity, and the traces don't show which stage stalled, so the investigation had to infer both from sibling spans.This adds three small instruments to workshop-backend so the next investigation can read the answer straight off the trace:
gatekeeper.loopback.identityspan with vendorId, target, callerFrom, and chatId. Canceled invocations become directly groupable by vendor.gatekeeper.loopback.resolvespan times session resolution. It uses a newcreatePipelinedTracerhelper that returns the RpcPromise unchanged, since the existingtracedhelper would break capnweb pipelining here.gatekeeper.session.openspan (plus a failure-only log event) wrapsopenSessionin the overseer DO. Comparing it against the loopback spans localizes a hang to overseer dispatch, connector session open, or the downstream connector call.Connector-side instrumentation of the tool call itself is deferred to a follow-up.