NEXUS-504: Refactor Nexus frontend interceptors - #11464
Conversation
de43b8c to
3c4f3d5
Compare
bergundy
left a comment
There was a problem hiding this comment.
I have submitted the WIP review so you have something actionable until I complete the full review.
fc8921b to
b1c2817
Compare
bergundy
left a comment
There was a problem hiding this comment.
Not blocking: You could try to consolidate some of the interceptor logic for nexus and grpc because there's a high risk now that the behaviors will drift.
f825f4a to
5d5775f
Compare
| // redirectionWrapper is one chain position for both transports: gRPC DC redirection | ||
| // and Nexus HTTP forwarding. The implementations stay separate but are wrapped together | ||
| // for canonical ordering of interceptors for both gRPC and Nexus | ||
| type redirectionWrapper struct { |
There was a problem hiding this comment.
Being a bit nitpicky here but I wonder if it wouldn't be simpler to not have this separate wrapper and move the nexus logic into interceptor.Redirection
There was a problem hiding this comment.
I did consider this but that would create an import cycle between as the nexus forwarder depends on the frontend and it already imports rpc/interceptor - breaking that up seemed like a larger effort than using this small wrapper
| return next(ctx, in) | ||
| } | ||
|
|
||
| func (ti *TelemetryInterceptor) InterceptNexusOutermost( |
There was a problem hiding this comment.
Remind me again what were the considerations to make this an exception in the interceptor flow?
There was a problem hiding this comment.
The issue is that currently Nexus has telemetry outermost in chain but gRPC has it after auth+fwding. imo telemetry as outermost is the right way forward for unified chain to continue capturing auth failures/redirects etc in same path and not have different paths for those. However, making that change for gRPC looks risky as it would change the current reporting - like forwarded requests being capture both at source and target clusters, authz failures etc - those should be relatively small overall but its still a change and didnt want to introduce any such behavior changes through this refactor
3054e12 to
b6c8293
Compare
b6c8293 to
f4b241c
Compare
|
Note: Cancelled in favor of analysis below |
d604bc8 to
113f9c9
Compare
|
Claude code review and analysis Raw summaryHolistic review —
|
| # | Severity | Disposition |
|---|---|---|
| F1 | med (was high, see C1) |
Fixed. ExposeDetails now asserted both exposed and masked (nexus_interceptor_chain_test.go:171-195, :250). |
| F2 | — | Accepted as-is. Confirmed collateral of unifying the chains; old gRPC had the same order (before/service_frontend_fx.go:306 redirection → :310 state validation). Not a regression anyone chose. |
| F3 | med |
Fixed exactly as suggested — outcome derived from the converted error (nexus_completion_http_handler.go:281-285). |
| F4 | med |
Fixed. recordPreInterceptorFailure emits all four metrics across all four pre-chain paths; auth path retains error_internal. |
| F5 | med |
Fixed. TestInterceptorsProviderOrder pins all 25 chain positions by type, plus telemetry-outermost and the len(nexus) == len(grpc)+1 invariant. |
| F6 | med |
Fixed. capturePanicHandlerNexus builds Operation / Namespace / Endpoint / NexusOperation / NexusStage / RequestID tags. |
| F7 | med |
Fixed. baseLogger falls back to rCtx.logger / oc.logger. |
| F8 | small |
Fixed via api.MethodName(in.APIName()), which restores the per-route split. Tag value still changes vs. main — release note still needed. |
| F9 | small |
Fixed. Flag now honored on the start/cancel path. |
| F10 | small |
Fixed. Dead field and params removed; exported constructor deliberately retained (public API) and now carries a Deprecated: marker. |
| F11 | small |
Mostly fixed. Renamed to NamespaceLengthValidatorInterceptor — better than the suggested swap, since the name now states behavior. One residual: see N1. |
| F12 | small |
Fixed. Explicit no-op with an explanatory comment. |
| F13 | small |
Fixed. Both godocs document the exclusion; the error names both options. |
| F14 | small |
Mostly fixed. TestOperationInputOutcomes covers all three input types incl. the error_ lowercase format, and the NewCompleteOpInput(nil) guard. Residuals: see N2. |
| F15 | small/nit |
Mostly fixed — aliases, Error() → %v, Get→Value, TODO resolved, attributeFailureToWorker, testOperationContext removed, auth api.MethodName. One bullet withdrawn (C2). |
Also fixed during remediation: a dead clusterMetadata cluster.Metadata parameter on
NewNexusOperationHTTPHandler that became unused once the field was dropped from newNexusHandler — the
parameter and the now-unneeded cluster import are both gone.
Open items
N1 — nit — Stale doc comment names two methods that no longer exist
nit — namespace_validator.go:25 still documents NamespaceValidateIntercept and StateValidationIntercept, neither of which is in the tree.
// common/rpc/interceptor/namespace_validator.go:25
// NamespaceValidatorInterceptor contains NamespaceValidateIntercept and StateValidationIntercept
NamespaceValidatorInterceptor struct {A repo-wide grep finds those two identifiers only in this comment and in test names. More conspicuous now
that the sibling type immediately below is correctly named NamespaceLengthValidatorInterceptor.
Suggestion: Replace with what the type actually does:
// NamespaceValidatorInterceptor validates that the resolved namespace is in a state
// that permits the requested API.N2 — small — Forwarding-interceptor test remains Start-only, with one unsynchronized shared variable
small — Cancel, Complete and unknown-type forwarding branches are unexercised, and receivedHeaders is shared between the httptest goroutine and the test goroutine.
service/frontend/nexus_forward_interceptor_test.go:163 is the only input construction — every table row
builds NewStartOpInput. Unreached: forwardCancelOperation (nexus_forward_interceptor.go:175),
forwardCompleteOperation (:205), completeOperationOptions (:240) including its
HandlerErrorTypeBadRequest arm, and the default: unknown-operation-type branch (:122-127).
Note forwardCancelOperation's error_bad_request return (:194) is the one forwarding error that does
not set SkipServiceErrorReporting, unlike all ten of its siblings — untested, and inconsistent either
way.
receivedHeaders (:43) is written from the httptest handler goroutine (:46), reset from the test
goroutine (:130), and read from the test goroutine (:193-195) with no synchronization. This blocks
t.Parallel() on the subtests, which the repo guidelines ask for in plain t.Run tests. Whether -race
reports it depends on whether net/http's loopback path establishes a happens-before edge —
UNVERIFIABLE-WITHOUT-RUN.
Suggestion: Add a Cancel row and a Complete row to the existing table (the httptest server already accepts
any method), assert the redirection headers per input type, and replace the shared variable with a
per-subtest server or a mutex-guarded value so t.Parallel() can be added.
N3 — nit — Bare blocking channel receives in a test
nit — concurrent_request_limit_test.go:180,195 use bare channel receives where the guidelines ask for await.Rcv.
180: <-blockUntilFirstReqStarted
195: require.NoError(t, <-firstReqErrorCh)Guideline §3: "Prefer await.Rcv and await.Snd for blocking channel operations so tests fail on timeout
instead of hanging indefinitely." If InterceptNexus ever stops calling next, this test hangs to the
package deadline rather than failing with a useful message.
Suggestion: Swap both for await.Rcv.
Revised readiness assessment
Ready for review. The six med findings that mattered — the collapsed completion outcome vocabulary, the
missing ServiceRequests/latency emissions, the unpinned chain order, the untagged panic logs, the
correlation-free forwarding logs, and the ignored SkipServiceErrorReporting — are all closed, and the
remediation went beyond the suggestions in two places worth noting: NamespaceLengthValidatorInterceptor is a
better rename than the type-swap the report proposed, and the golden-order test also pins the
telemetry-outermost and chain-length invariants rather than just the sequence.
Residual risk is now concentrated in one place: the test suite has never been run in this review. That is
the same caveat as the original pass, but it carries more weight now, because remediation added a substantial
amount of new test code — TestInterceptorsProviderOrder, TestOperationInputOutcomes,
TestServerOptionsRejectsBothFrontendInterceptorOptions, new chain cases. Static reading cannot distinguish a green golden-order
test from one that was written against a stale expected list.
Before merge, in priority order:
- Release notes for the metric and log-string changes that are intentional but breaking for existing
dashboards and saved searches: the DC-redirectionoperationtag value (F8),"Slow gRPC call"→
"Slow request", and the forwarding log-message punctuation changes. - N2 and N3 — test-quality items; reasonable to defer to a follow-up, though N2's Cancel/Complete rows
are the cheapest remaining coverage win in the branch.
One thing deliberately not on this list: F2. Confirmed as accepted collateral of unifying the two chains,
with old gRPC exhibiting the same ordering. No action.
Prompt for reference
Do a full holistic code review of this branch.## Target construction
- Find the branch point with `git merge-base origin/main HEAD` and build ONE unified diff
covering **all commits on the branch PLUS all uncommitted working-tree changes**, reviewed
together as a single final state.
- **DO NOT do a commit-by-commit review.** I deliberately split and reordered the commits after
the fact to make them easier to read, so intermediate commits contain known inconsistencies
that are NOT findings. Only the final state matters.
- Confirm every finding against the **current file on disk**, never against the diff alone. If it
is not still wrong in the working tree, it is not a finding.
- Save pre-refactor snapshots (`git show <branch-point>:<path>`) of any heavily rewritten file so
behavior parity can be compared directly rather than guessed at.
## Constraints
- **Do not run tests, `go test`, builds, `go vet`, lint, or CI.** This is a read-only review.
- **Do not edit, write, or fix any repo file.** Report only.
- If you want to make a claim that only a test run could settle, say so explicitly and let me decide.
## Process
1. Scope the change first, then **give me a plan and wait for my go-ahead** before executing.
Ask me for effort level and where I want the output.
2. Load the repo's own review skill / guidelines (e.g. `.claude/skills/review`,
`.github/copilot-instructions.md`, `CLAUDE.md`) and pass them to every subagent so findings come
back in the repo's severity vocabulary and comment format.
3. Fan out **8 parallel subagents**, one per dimension, scoped to specific files rather than the
whole diff. Typical dimensions — adapt to the change:
- Ordering/sequencing parity of whatever pipeline the change touches
- The core new abstraction or framework
- Any logic extracted or moved between components
- **Behavior parity of rewritten code — what was LOST, not what moved** (usually the highest value)
- Security-adjacent: auth, rate limits, admission control, fail-closed behavior
- Metrics / tracing / logging parity
- Wiring, DI, configuration, defaults
- Conventions, API surface, structural simplicity, test quality
4. Run an **adversarial verification pass** on every candidate finding, especially every `high`.
The verifier's job is to REFUTE the claim by reading the actual code, and it must return
CONFIRMED / PLAUSIBLE / REFUTED with the decisive evidence and any corrections to the reasoning.
Drop what cannot be verified rather than softening it.
5. Deduplicate and consolidate related symptoms into root-cause findings, rank most-severe first.
## What to look for — regressions of ANY size
Treat "this used to happen and no longer does" as the core question, and report it **however minor**:
- **Behavior parity:** validation checks, early returns, guard clauses, defers, cleanup, panic
recovery, request/response size limits, timeouts, request ID handling.
- **Error handling:** error → status code / error type mapping, exact failure messages, whether
detail is exposed or masked, retryable vs non-retryable classification, error wrapping that
breaks `errors.As` / `errors.AsType`.
- **Metrics:** renamed metrics, changed or dropped tag sets, changed tag VALUE formats, lost
outcome/classification vocabularies, unbounded cardinality, measurement windows that shrank or
now double-count, emissions that are now dead writes.
- **Logging — treat these as real findings, not nits:** lost log lines, lost structured tags,
lost correlation fields (request/trace/workflow/run IDs), a richly tagged logger replaced by a
bare one, interpolated messages where static messages are required, messages that now name the
wrong transport/component, changed log levels, duplicated tags.
- **Tracing:** lost spans, lost span attributes, broken propagation across hops.
- **Ordering:** anything whose position in a chain/pipeline moved, and what that breaks.
- **Concurrency & resources:** data races, shared mutable state, cross-request leakage, unreleased
slots/counters on error or panic paths, unclosed bodies/handles, IO under locks.
- **Uninitialized or dead state:** fields declared but never assigned, writes never read, nil
interface dereferences, half-finished removals where the producer went but the consumer stayed.
- **Config & options:** new options whose default changes existing behavior, undocumented
mutual exclusions, validation that fails late (at graph construction) instead of early.
- **Dead code:** functions, types, fields, constants, imports left behind. Grep to confirm zero
callers before reporting.
- **Test coverage:** deleted tests whose behavior is not re-covered elsewhere; new tests that only
assert the happy path; tests that would not catch the regressions above.
## Output
- Use the repo's finding format and severity levels. Every finding needs:
`file:line` against the current tree, a one-sentence summary that stands alone, why it matters,
a **concrete failure scenario** (specific inputs/state → specific wrong behavior), and a concrete
suggested fix with code where the fix is an edit.
- Prefer a small number of high-confidence findings over a long list.
- Also report what you **verified clean** — the things that could have broken and didn't.
- Finish with an honest readiness assessment: what's solid, what the real remaining risk is, and
what you'd do before it goes up for review, in priority order.
- Write the full report to a file **and always give me the full absolute path** to anything you write.
## Reporting standards
- Report faithfully. Don't soften findings, don't inflate them, and don't claim verification you
didn't do. If you only read code and didn't run anything, say that and treat your list as a
lower bound.
- Don't take subagent results at face value — they are sometimes wrong or overstated. Verify the
consequential ones yourself where a cheap grep settles it.
- If subagents stall or die (e.g. the machine sleeps mid-run), just relaunch the dead dimensions.
What changed?
Refactoring Nexus frontend interceptors
Why?
https://temporalio.atlassian.net/browse/NEXUS-504
How did you test it?
Potential risks