Skip to content

fix(ribbon): register the engine-toggle prime marker before the prime starts - #954

Merged
drmoisan merged 11 commits into
mainfrom
bug/engine-toggle-prime-marker-registration-races-removal-944
Sep 30, 2026
Merged

drmoisan merged 11 commits into
mainfrom
bug/engine-toggle-prime-marker-registration-races-removal-944

Conversation

@drmoisan

Copy link
Copy Markdown
Owner

Summary

  • Fixes a race in EngineToggleStateCoordinator in which a prime that completed before its marker was registered left a stale marker in _primeTasks, blocking every later re-prime of that engine for the session.
  • StartPrimeIfNeeded now registers a TaskCompletionSource<bool> marker under _primeGate before the prime starts. StartObservedPrime completes the marker with SetResult(true) in a finally after CompletePrime returns, so the marker never faults or cancels.
  • CompletePrime is unchanged. It still reports a fault before removing the marker, and it now always finds the marker it is meant to remove.
  • Adds three regression tests in a new partial, EngineToggleStateCoordinatorTests.PrimeRegistration.cs. They cover registration order, re-prime after a synchronous fault, and re-prime after a synchronous cancellation.
  • The code footprint is three files: one production file (+34/-12), one new test partial (175 lines), and one csproj compile entry.

Why

StartPrimeIfNeeded stored the prime's continuation task in _primeTasks only after StartObservedPrime returned. A prime that faulted or was canceled synchronously, or quickly enough on another thread, ran CompletePrime's removal before that store. The later store then left a marker for a prime that had already finished, and the at-most-one-prime guard treated it as in flight. Registering the marker before the start removes that ordering dependency. The root-cause analysis is in spec.md and the research record in the feature folder.

What Changed

Production (TaskMaster/Ribbon/EngineToggleStateCoordinator.cs)

  • StartPrimeIfNeeded: creates the marker with TaskCreationOptions.RunContinuationsAsynchronously, stores it as _primeTasks[engineName] = marker.Task inside the existing lock, then calls StartObservedPrime(..., marker).
  • StartObservedPrime: now returns void and takes the marker. The continuation wraps CompletePrime in try / finally { marker.SetResult(true); }, and the continuation task is discarded. The continuation options and scheduler are unchanged.
  • The XML documentation for _primeGate, _primeTasks and the StartObservedPrime remarks was updated to describe the registration marker.
  • No new catch, lock, dependency or public API. GetPrimeTask and CompletePrime are unchanged.

Tests

  • TaskMaster.Test/Ribbon/EngineToggleStateCoordinatorTests.PrimeRegistration.cs (new; MSTest, the fixture's strict Moq harness, FluentAssertions):
    • GetPressed_WhenPrimeStarts_RegistersPrimeHandleBeforeActivationReadRuns
    • GetPressed_AfterPrimeFaultsSynchronously_LaterReadStartsNewPrime
    • GetPressed_AfterPrimeIsCanceledSynchronously_LaterReadStartsNewPrime
  • TaskMaster.Test/TaskMaster.Test.csproj: one Compile Include entry for the new partial.

Docs

  • The feature folder docs/features/active/2026-09-30-engine-toggle-prime-marker-registration-races-removal-944/ holds the spec, research, plan, evidence and three audit artifacts. The PR also adds the promotion record under docs/features/potential/promoted/.

Architecture / How It Fits Together

The ribbon getPressed callback reads EngineToggleStateCoordinator.GetPressed. On a cache miss, StartPrimeIfNeeded takes _primeGate, checks _primeTasks for the engine, registers the marker and starts the prime. ApplyPrimeAsync performs the activation read. Its continuation runs CompletePrime on the default scheduler, which logs a fault, removes a faulted or canceled marker, and invalidates the control. The continuation then completes the marker. Tests await the marker through GetPrimeTask rather than polling.

Verification

Completed (local, recorded in the feature-folder evidence)

  • Fail-before: against the unchanged production file, GetPressed_WhenPrimeStarts_RegistersPrimeHandleBeforeActivationReadRuns failed on its registration-order assertion; 28 fixture tests were discovered and no compile or load failure occurred (evidence/regression-testing/prime-registration-fail-before.md).
  • Pass-after: coordinator fixture 28/28 passed, including all seven named tests, on the implementation build and again on the final rebuilt assembly (prime-registration-pass-after.md).
  • Final toolchain pass, in CLAUDE.md order (evidence/qa-gates/toolchain-final-pass.md):
    • dotnet tool run csharpier format . rewrote 0 files.
    • dotnet tool run csharpier check . checked 1627 files and reported no differences.
    • The analyzer rebuild (/t:Rebuild, EnableNETAnalyzers, EnforceCodeStyleInBuild) and the nullable rebuild (/t:Rebuild, TreatWarningsAsErrors) each reported 0 errors and 0 warnings.
    • The coverage-enabled run (dotnet-coverage around vstest) exited 0 with 7327 of 7327 tests passed.
  • Coverage:
    • First-party totals: lines 56098/65750 (85.32%), branches 13597/17054 (79.73%).
    • EngineToggleStateCoordinator: 157/157 lines, 37/38 branches (the same missed branch as the baseline).
    • StartPrimeIfNeeded, StartObservedPrime and CompletePrime are each at 100%.
    • All 17 changed executable lines are covered.
  • Final-pass restart: the first final coverage attempt failed two QuickFiler.Controllers.Tests.QfcDatamodelLivenessTests wall-clock tests. This PR neither changes nor covers that QuickFiler.Test project. A single full restart of the final loop was approved by the coordinator for that reason, and the restart passed with zero failures. Both attempts are recorded in evidence/qa-gates/coverage-summary.md.
  • Feature review: 18/18 acceptance criteria met, with 0 blocking, 4 non-blocking and 3 follow-up findings (policy audit, code review and feature audit dated 2026-09-30T16-00).

Recommended

  • CI on this PR is the gate for the four UtilitiesCS.Test shell-icon test classes, which the local coverage route excludes on this workstation.

Backward Compatibility / Migration Notes

None. All changed members are private, and observable behavior changes only in the failure paths the fix addresses. After a fault or cancellation, a later read now starts a new prime instead of being blocked.

Risks and Mitigations

  • Ordering of marker completion: the marker completes only in the continuation's finally, after CompletePrime has exited, so test awaits observe the logged fault and the removal. The fault-ordering tests from the upstream fix still pass unchanged.
  • Repeated re-primes after a permanent configuration fault: every cache-miss poll now re-primes and logs again, where the stale marker previously suppressed the repeats intermittently. This is recorded as follow-up FU-2.
  • Rollback: revert the production file and the new partial and its compile entry. No data or configuration is involved.

Review Guide

  1. TaskMaster/Ribbon/EngineToggleStateCoordinator.cs: the lock block in StartPrimeIfNeeded and the StartObservedPrime continuation.
  2. TaskMaster.Test/Ribbon/EngineToggleStateCoordinatorTests.PrimeRegistration.cs.
  3. code-review.2026-09-30T16-00.md in the feature folder, which includes the analysis of why the keyed TryRemove cannot remove a newer marker.
  4. The feature-folder evidence is large but mechanical, and can be sampled.

Follow-ups

Listed for the coordinator to file. This branch files no issues.

  • FU-1: a throwing logError sink skips _primeTasks.TryRemove under report-then-clear, so the marker completes but stays registered, and the discarded continuation task faults unobserved (spec Rollout item 1).
  • FU-2: after a permanently faulted configuration load, every cache-miss getPressed poll re-primes and logs again. A back-off or a reset-based recovery is a separate decision (spec Rollout item 2).
  • FU-3: the GetPrimeTask <returns> element still opens with "The prime task", although the value is now the registration marker. This is a one-sentence precision edit for the next change to that file.
  • Context: the QuickFiler QfcDatamodelLivenessTests wall-clock flakiness and the transaction timing flakiness are being filed by the coordinator separately.

GitHub Auto-close

🤖 Generated with Claude Code

@drmoisan
drmoisan merged commit 829ad41 into main Sep 30, 2026
7 checks passed
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.

Bug: engine-toggle-prime-marker-registration-races-removal

1 participant