Prevent silent data: null responses when Fusion diagnostics throw - #10299
Open
rstaib wants to merge 7 commits into
Open
Prevent silent data: null responses when Fusion diagnostics throw#10299rstaib wants to merge 7 commits into
data: null responses when Fusion diagnostics throw#10299rstaib wants to merge 7 commits into
Conversation
A diagnostic listener that threw while opening an execution node's scope silently killed the node: the subgraph fetch never ran and the operation returned data: null with an empty errors list. A throwing scope dispose could additionally skip node completion and stall the executor drain. - Isolate CreateScope and guard scope disposal and OnError in ExecutionNode.ExecuteAsync so diagnostics can never affect execution or node completion. - Track exceptions that escaped a node's own error handling and surface them as GraphQL errors at completion, so missing data always comes with an explaining error, in Release too. - Add the missing EventStream step kind to ExecutePlanNodeSpan and fall back to an untagged span for unmapped execution node types. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CrEmm3srU8R6WuGEGzZmos
data: null responses when Fusion diagnostics throw
…ections The introspection node built its result document over the full root selection set, but only populated the introspection selections. Property slots belonging to data fields stayed unassigned, and building the document crashed with a NullReferenceException that the engine swallowed silently until the previous commit made such failures visible. The document is now built over exactly the selections the node resolves, and the build step moved inside the node's error handling so a failure there surfaces as a proper GraphQL error. The Fetch_User_With_Invalid_Node_Field snapshot now expects the error that the previously silent node-id coercion failure produces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CrEmm3srU8R6WuGEGzZmos
…st/fusion-diagnostics-non-fatal
Contributor
Patch coverage85.4% of changed lines covered (76/89)
Uncovered changed lines (JSON){
"sha": "212ca78496ff27be6d7f6871f601708ef0be2bcf",
"files": [
{ "path": "src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanContext.cs", "ranges": [[784, 790]] },
{ "path": "src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Nodes/ExecutionNode.cs", "ranges": [[118, 119], [123, 123], [173, 174], [178, 178]] }
]
}Project coverage: 56.3% (265163/470766 lines) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a Fusion gateway runs
.AddInstrumentation()and an OTelActivityListeneris active, an exception thrown while opening an execution node's diagnostic scope silently killed the node: the subgraph fetch never ran and the operation returneddata: nullwith an emptyerrorslist.We hit this through a package version skew (
HotChocolate.Fusion.Diagnostics16.5.0 next toFusion.Execution16.6.x throwsMissingMethodExceptionforOperationSourceText.get_Hash()insideExecutePlanNodeSpan.SetSourceSchemaTags, reachable onceStartActivityreturns non-null). The skew is a packaging issue, but the engine turning any diagnostics exception into silent data loss is the defect fixed here:ExecutionNode.ExecuteAsyncranCreateScope(context)inside the sametryasOnExecuteAsync, so a throwing listener skipped the fetch entirely.ExecutionNodeErrordiagnostic event — no GraphQL error was ever produced, and the failed node cascadedSkippedto all dependents.Debug.Assert— a no-op in Release — so the malformeddata: null, errors: []response shipped.scope?.Dispose()in thefinallywas unguarded and ran beforecontext.CompleteNode(result); a throwing span dispose would skip node completion, unbalancing the active-node count and stalling the executor drain forever.Fixes
ExecutionNode.ExecuteAsync):CreateScopeis isolated in its own try/catch — on failure the node executes without a scope.scope?.Dispose()is guarded soCompleteNodealways runs and the active-node count stays balanced. Failures are reported through a self-guardedExecutionNodeErrorevent, and theOnErrorcall in the main catch is guarded as well.ExecutionState.CompleteNodetracks those exceptions (engine-cancellationOperationCanceledExceptions excluded; pending-merge failures are markedErrorReportedsinceApplyMergealready added their errors) andOperationPlanContext.Complete()surfaces each as anUnexpected Execution ErrorviaErrorBuilder.FromException+IErrorHandler.Handle. Surfacing is unconditional, so an unrelated node's handled error cannot mask another node's silent crash. If data is invalidated and no error was collected from any source, a generic error is added as a last resort —data: nullis never paired with emptyerrors, in Release too. TheDebug.Assertstays.EventStreamstep kind (ExecutePlanNodeSpan): theKindValuesdictionary lackedExecutionNodeType.EventStream(reachable viaExecuteSubscriptionNodefor event-stream subscription events, whereKindValues[node.Type]threwKeyNotFoundException). Added anevent_streamkind value to the semantic conventions and the dictionary, and switched the lookup toTryGetValueso future enum members degrade to an untagged span instead of throwing.Tests
DiagnosticEventListenerResilienceTests(new): a registered listener that throws inExecuteOperationNode(scope creation) or from the returned scope'sDispose— the subgraph fetch still executes and the operation returns data.ExecutionNodeCompletionTests(extended): a throwingCreateScopedoesn't prevent execution; a throwing scope dispose doesn't swallow node completion; an unhandled node failure yields anUnexpected Execution ErrorfromComplete(); an unrelated already-collected error does not suppress that surfacing;ErrorReportedresults are not double-surfaced.FusionActivityExecutionDiagnosticListenerTests(extended): everyExecutionNodeTypemember maps to a step kind value, includingEventStream→event_stream.The failure-mode tests fail against the previous code (the silent case fails with exactly the empty-
errorssymptom).Fusion.Execution.Tests: 1076 passed, 0 failed.Follow-ups (flagged, not implemented)
HotChocolate.Fusion.Diagnosticsships with an open version floor on its Fusion dependencies, so NuGet resolves mismatched pairs (16.5.0 diagnostics next to 16.6.x execution — the skew that triggered this). Consider an upper-bounded dependency range or a startup version check inAddInstrumentation.AggregateFusionExecutionDiagnosticEventsinvokes listeners in plain loops — with multiple listeners registered, one throwing during scope creation/dispose leaks the other listeners' already-created scopes (an Activity-backed span is never stopped andActivity.Currentstays pinned).path: errors surfaced by fix 2 are root-level; the baseExecutionNodehas no result-selection-set to derive paths from.