Skip to content

Lite: a cancelled analysis request stops instead of reading to the end - #4561

Merged
erikdarlingdata merged 3 commits into
devfrom
fix/lite-analysis-cancellation
Sep 28, 2026
Merged

erikdarlingdata merged 3 commits into
devfrom
fix/lite-analysis-cancellation

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Why

A cancelled Lite MCP analysis request (a client that gave up, or an abandoned tool call)
used to keep reading DuckDB to the end. Darling's twin service already honours its
caller's cancellation on the same four methods; Lite's did not.

What changes

Lite/Analysis/AnalysisService.cs:

  • CollectAndScoreFactsAsync, CollectConfigAuditFactsAsync, ComparePeriodsAsync, and the
    private LookUpDispersionAsync each take a CancellationToken cancellationToken = default,
    set onto AnalysisContext.CancellationToken (or forwarded to BaselineProvider.GetBaselineAsync),
    which the DuckDB fact collector and baseline provider already observe on every store read.
  • Each of those methods' fault-swallowing catch (Exception ex) now excludes
    OperationCanceledException (when (ex is not OperationCanceledException)), so a cancelled
    read reaches the caller as a cancellation instead of an empty result logged as an error —
    the same rule Darling's CollectConfigAuditFactsAsync already states for the same reason.
  • The internal caller inside AttributeConfigChangesAsync now passes context.CancellationToken
    to its ComparePeriodsAsync call, the same token it already checks with
    ThrowIfCancellationRequested() on the line above; a cancelled config-change attribution
    request used to run both comparison-period reads to the end anyway. Every other production
    call of the four on-demand methods under Lite/ already passed the token.

Lite/Mcp/McpAnalysisTools.cs:

  • GetAnalysisFacts (get_analysis_facts),
    CompareAnalysis (compare_analysis), and AuditConfig (audit_config) each take a
    CancellationToken cancellationToken = default parameter and pass it through to the
    AnalysisService call they make. Their outer catch (Exception ex) blocks also exclude
    OperationCanceledException now that a cancellation can actually surface there.

Test plan

Census pin (Darling.Tests, runs on macOS): a new
LiteAnalysisServiceCancellationSourceTests reads Lite/Analysis/AnalysisService.cs as text
(no project reference to Lite) and asserts each of the four methods' signature carries a
CancellationToken and that the one fault catch inside each method's body excludes
OperationCanceledException.

  • RED on origin/dev (f8508b5): Total: 5, Errors: 0, Failed: 5 (the pre-fix source has
    neither the parameter nor the filter).
  • GREEN on this branch, together with AnalysisWindowEmptyRuleParityTests,
    AnalysisFactsReadRunsDetectorParityTests, and AsOfWindowAnchorTests:
    Total: 39, Errors: 0, Failed: 0.

The same class also carries a new fact, EveryProductionCallOfTheFourOnDemandMethods_ PassesACancellationTokenArgument: it scans every .cs file under Lite/ for a call of the
four on-demand methods and asserts the call's argument text contains a CancellationToken.

  • RED with the internal caller not passing its token: 1 failed, naming the exact missed call — Lite/Analysis/AnalysisService.cs: call of ComparePeriodsAsync at offset 62746 has no CancellationToken argument.
  • GREEN on this branch: Total: 5, Errors: 0, Failed: 0.

Mutation: removed the when (ex is not OperationCanceledException) filter from
CollectAndScoreFactsAsync's catch, rebuilt — the census pin went RED
(Total: 4, Errors: 0, Failed: 1, the CollectAndScoreFactsAsync case) — then reverted and
rebuilt back to GREEN.

Lite.Tests pins (Windows-only; CI judges these): a new LiteAnalysisCancellationTests
class gives each of the three public AnalysisService methods a pre-cancelled
CancellationTokenSource and asserts Assert.ThrowsAnyAsync<OperationCanceledException>. On
the pre-fix code this file does not compile (the methods had no cancellationToken parameter
to pass), so its RED is a compile failure, not a runtime one. It builds clean (0 warnings) on
this branch; dotnet Lite.Tests.dll cannot run in-process on macOS (discovery dies on
WindowsBase), so CI is the first runtime judge of these three tests.

Builds, 0 warnings on each: Lite/PerformanceMonitorLite.csproj, Lite.Tests/Lite.Tests.csproj,
Darling/Darling.Tests/Darling.Tests.csproj (Release, -p:EnableWindowsTargeting=true), and
deprecated/Dashboard.Tests/Dashboard.Tests.csproj (build-only, untouched — its own
AnalysisService/McpAnalysisTools copies in deprecated/Dashboard are a separate, older
class hierarchy and were not touched). xUnit1051 does not fire because both test projects'
.csproj already suppress it (<NoWarn>...;xUnit1051</NoWarn>) with a standing comment
explaining why (a per-call-site style nag across hundreds of short-lived test calls).

CHANGELOG

SECTION: Fixed
ENTRY: - Lite's MCP analysis tools stop reading when the client cancels the request ([#4561]) - A cancelled get_analysis_facts, compare_analysis or audit_config request, and the config-change attribution behind it, now stops reading DuckDB and returns as cancelled, instead of reading to the end and logging an error. Darling's analysis tools already did this.
REF: [#4561]: #4561

Lite's four on-demand analysis methods (CollectAndScoreFactsAsync,
CollectConfigAuditFactsAsync, ComparePeriodsAsync, and the private
LookUpDispersionAsync) now take a CancellationToken, threaded through
AnalysisContext.CancellationToken the way the DuckDB fact collector and
baseline provider already read it. The three MCP tool callers
(analyze_server, compare_analysis, audit_config) pass their own token
through get_analysis_facts, compare_analysis, and audit_config. Each
method's fault catch now excludes OperationCanceledException, so a
cancelled read reaches the caller as a cancellation instead of an empty
result logged as a fault -- the same #4203 rule Darling's twin already
follows.
…aller

A config-change attribution request that had already been cancelled still
ran both comparison-period reads to the end, because the internal call to
ComparePeriodsAsync inside AnalysisService omitted the CancellationToken
that was already in scope on the line above it.

Extends the source-census pin covering the four on-demand analysis
methods with a fact that every production call of those methods under
Lite/ passes a CancellationToken argument, so a future caller that
forgets the token fails the same way.
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 28, 2026 07:30
@erikdarlingdata
erikdarlingdata merged commit 08f3e3c into dev Sep 28, 2026
15 of 16 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/lite-analysis-cancellation branch September 28, 2026 07:30
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.

1 participant