Repository navigation
fix(825): close the residual ETL deadline mechanics left by #811 - #834
Merged
drmoisan merged 3 commits intoSep 9, 2026
Conversation
Six residual items from issue #825, none a regression introduced by #811. Item 1 records why the 250 ms per-row ETL budget is unchanged. A comment at the expression states that the value rests on no recorded measurement, that no measurement is obtainable in this environment, and names the live-Outlook capture route by LogTableTiming payload and log4net root level. The value itself is untouched. Item 2 puts the table-acquisition deadline under caller control. GetTableInViewAsync gains a trailing optional TimeProvider parameter, resolves its deadline source once so an explicitly supplied timeoutSourceFactory still wins, and DfDeedle.GetEmailDataInViewAsync now forwards its provider. The TimeoutException retry propagates the caller's timeoutMs instead of the literal 2000, so both attempts are governed by the same caller-visible value on the same caller-supplied clock. Proven at build time rather than assumed: a settling call site was compiled and produced no CS1061, so TimeProviderTaskExtensions.CreateCancellationTokenSource is available. Item 3 changes EtlAsync's declared return type so its first tuple element is object[,]?, and deletes the null-forgiving suppression on the return. The TimeoutException swallow, the tokenSource.Cancel call and the DfDeedle null guard are all unchanged, so no consumer observes a behavioural difference. Item 4 deletes the two inert (int, int) TimeoutAfter overloads, EtlAsyncOld, and their three tests. All three exits of the inner provider overload return a Task and none throws synchronously, so the catch clauses in the deleted overloads could never execute. This takes TimeOutTask.cs from 1011 lines to 966. That is a reduction of the 500-line cap violation, not a resolution of it; the file remains 466 lines above the cap and reducing it further is a separate change. Item 5 corrects the stale DfDeedle.QfcColumns.cs doc comment, which named a TableEtlInvoker seam that no longer exists, to name DefaultTableEtl. The CS1769 rationale sentence is retained. Item 6 removes [DoNotParallelize] from OlTableExtensions_Tests and replaces its class comment, which had claimed a population of tests the class does not contain. The justification is the item 2 change rather than observed repeated runs: the one test that armed a real timed CancellationTokenSource now supplies a FakeTimeProvider, so no wall-clock deadline governs the class. TimeOutTask_Tests keeps its attribute and gains the verified reason it lacked, naming the two real wall-clock races in that class. Four regression tests are added in a new file with one compile item added to the test project. DfDeedleEtlTimeoutTests.cs receives a bounded timer-ordering update, permitted by the 2026-09-09 AC6 amendment: threading the provider inserts the acquisition timer as the first arming signal on a latch-based barrier, so each timer now arms inside its own await window. No assertion in that class was added or removed. Toolchain: csharpier format and check clean at 1623 files; both MSBuild gates pass with /t:Rebuild at 0 warnings and 0 errors, with zero CoreCompile skips and four csc invocations for the Write Set projects; 7213 tests pass with coverage. Repository first-party line coverage moves from 85.62% to 85.67%, and changed-line coverage is 90.9%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URyGon15RvDXcUEo96tvyh
Two artifacts that could not be inside the preceding commit, because each records the state that commit produced. evidence/qa-gates/ac18-commit-language.md records the commit sha, the zero-line porcelain result, the commit-language checks, both directions of the Write Set accounting, and the confirming check that none of the five committed MSBuild logs contains the host account name. evidence/other/review-handoff.md indexes every artifact this plan produced with a one-line purpose, and names the adjudicated design conflict section of the plan as the first item a reviewer must read. It also states, for sibling feature 826, the end state of the exception that escapes TimeOutTask.RunWithTimeout. The plan checklist is updated to reflect the tasks these two artifacts complete. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URyGon15RvDXcUEo96tvyh
Records the policy audit, code review and feature audit for the ETL deadline mechanics follow-ups. blocking_count is 0 and all 35 acceptance criteria in spec.md evaluate PASS, so no remediation cycle is opened. Also carries the P9-T6 plan check-off, which the plan itself records as the single permitted residual because it is written after the commit the task makes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URyGon15RvDXcUEo96tvyh
drmoisan
added a commit
that referenced
this pull request
Sep 9, 2026
Feature 825 merged as PR #834 (merge commit 049c142), verified against gh pr view and the remote integration tip; the fan-in introduced zero deletions. That completes wave 0: 813, 815, 817, 821, 823, 824 and 825 are all merged and durably confirmed. Six dispatched integration CI runs, six successes. Also records a redundant resume of feature 825. The first orchestrator for that feature notified with a narrative progress report rather than the agreed bounded return shape, and the parent's idleness probes -- no live test process, a coverage file identical across two samples -- measured the workload rather than the agent. The original merged twenty-three seconds after that read. The double-delegation is bounded because a merge cannot be repeated, and wave 1 was held until the redundant agent exited. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URyGon15RvDXcUEo96tvyh
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.
fix(825): close the residual ETL deadline mechanics left by #811
Summary
TimeProviderthroughGetTableInViewAsync, so the table-acquisition deadline and both of its retry attempts are armed on the caller's clock instead of the system clock.2000ms argument in theTimeoutExceptionretry recursion with the caller'stimeoutMs, so both attempts are governed by the same caller-visible deadline. This is proven by a red-before / green-after regression test.EtlAsync's tuple return type admit the null it can actually return (object[,]? data) and removes the null-forgiving suppression on that return, so the null-snapshot path is expressed in the type system rather than suppressed.TimeoutAfteroverloads and the deadEtlAsyncOldmethod, together with their three test callers.UtilitiesCS/Threading/TimeOutTask.csdrops 45 lines.250 * rowCountETL budget in place, without changing the expression or its literal, and names the live-Outlook capture route that would produce a real measurement.[DoNotParallelize]fromOlTableExtensions_Tests.csonly after the production change eliminated the wall-clock hazard that justified it, and adds the missing verified-reason comment to the one class that keeps it.All 35 acceptance criteria in
spec.mdare delivered and independently reviewed: 35 PASS, 0 PARTIAL, 0 FAIL, 0 UNVERIFIED, blocking findings 0.Why
Issue #811 placed the 250 ms per-row ETL deadline and the 1000 ms dataframe-transform deadline under an injectable
TimeProviderand added a descriptive guard for the null-snapshot path. Six residual items were deliberately excluded from that fix to keep its blast radius to a bugfix. None of them is a regression introduced by #811; this change closes them as a set.The central defect was that
GetTableInViewAsyncacquired its table under a deadline armed on the system clock, so no test could govern it deterministically, and itsTimeoutExceptionretry silently substituted a literal2000ms for whatever the caller asked for. A caller that requested 750 ms got 750 ms on the first attempt and 2000 ms on the second, with no way to observe the substitution.The invariant this change establishes
Every wall-clock deadline reachable from
DfDeedle.GetEmailDataInViewAsync— including the table-acquisition deadline inGetTableInViewAsyncand both of its retry attempts — is now armed on theTimeProviderthe caller supplied, is measured against the caller'stimeoutMsrather than any literal, andEtlAsync's declared return type admits the null it can return, so no null-forgiving suppression remains anywhere on that path.What Changed
Core production change (5 files)
UtilitiesCS/OutlookObjects/Table/OlTableExtensions.TableAccess.csTimeProvider? timeProvider = null; resolves the deadline factory once fromtimeProvider ?? TimeProvider.System; propagates the provider through both retry recursions; replaces the literal2000withtimeoutMs.UtilitiesCS/OutlookObjects/Table/OlTableExtensions.Etl.csEtlAsyncreturn type becomes(object[,]? data, ...); the null-forgiving suppression on its return is removed; the budget-rationale comment is added;EtlAsyncOldis deleted.UtilitiesCS/Threading/TimeOutTask.csTimeoutAfteroverloads deleted (−45 lines).UtilitiesCS/Extensions/DfDeedle.cstimeProvider:toGetTableInViewAsync; reads the tuple through itsdataname so the existing null guard's flow state carries to both consumers.UtilitiesCS/Extensions/DfDeedle.QfcColumns.csTableEtlInvokertoDefaultTableEtl.The two deleted overloads were inert: their inner
TimeoutAfter(int)returns a faultedTaskrather than throwing synchronously, so theircatch (TimeoutException)could never run and therepeatAttemptsparameter never had any effect.Tests (6 files)
UtilitiesCS.Test/OutlookObjects/Table/GetTableInViewAsyncClockTests.cs(+289) — four tests: the retry-literal regression, the injected-clock arming proof, explicit-factory precedence, and the default-path guard proving a null provider still resolves to the system clock.OlTableExtensions_Tests.cs— four reflection binding sites gain aTimeProviderentry; theEtlAsyncOldtest is deleted;[DoNotParallelize]and the stale class comment are removed.DfDeedleEtlTimeoutTests.cs— a third test-owned gate on table acquisition, so each production timer arms inside its own await/re-arm window on the latch-based barrier. No assertion added or removed.TimeOutTask_Tests.cs— the twoWithRepeatAttemptstests are deleted; a verified-reason comment is added above the retained[DoNotParallelize].OlTableExtensionsEtlClockTests.cs— doc comment only.UtilitiesCS.Test.csproj— exactly one addedCompile Includeline, nothing else.Architecture / How It Fits Together
GetTableInViewAsyncpreviously passed the caller'stimeoutSourceFactorystraight toTimeOutTask.RunWithTimeout, andRunWithTimeoutfell back tonew CancellationTokenSource(ms)— a system-clock timer — when that factory was null.The change inserts one resolution step in the caller. A local
resolvedTimeoutSourceFactoryprefers an explicitly supplied factory, and otherwise builds one from(timeProvider ?? TimeProvider.System).CreateCancellationTokenSource(TimeSpan.FromMilliseconds(ms)). That resolved local is what reachesRunWithTimeout. Precedence is therefore explicit and testable: an explicit factory still wins, a supplied provider governs when it does not, and a null provider reproduces the previous production behaviour exactly.DfDeedle.GetEmailDataInViewAsyncalready threaded itstimeProviderintoAddQfcColumnsAsyncandEtlAsync; it now does the same for the table-acquisition call, completing the pattern.A deliberate consequence, recorded because it drove a spec amendment: threading the provider inserts the table-acquisition timer as the first arming signal on the
ArmingBarrierTimeProviderthat one existing test consumes in a fixed order. The barrier's latch drops a signal whenever two timers arm inside one await window, so that test needed a third gate. The alternative — not forwarding the provider — was rejected because it leaves the production deadline on the system clock, which is the defect this item exists to close.Verification
Completed
Full C# toolchain, run in order, with one loop restart when the formatter reflowed this change's own new code:
dotnet tool run csharpier format .ChangedFileCount: 0on the final passdotnet tool run csharpier check ./t:Rebuild)/t:Rebuild,TreatWarningsAsErrors)Gate non-vacuity measured, not assumed:
SkippingCoreCompileCount: 0andCscInvocationsForWriteSetProjects: 4on both MSBuild logs./t:Rebuildis used deliberately — MSBuild's up-to-date check does not invalidate on a command-line/p:change, so a warm/t:Buildreturns exit 0 withCoreCompileskipped and the gate cannot fail.Red-before / green-after:
GetTableInViewAsync_TimeoutRetry_UsesCallerTimeoutMsNotLiteral2000was captured failing against the pre-change tree (EXIT_CODE: 1,ExpectedExitCode: 1, recording that the second value the factory received was 2000 rather than 750) and passes after.Coverage:
UtilitiesCSpackageLinesValid43287 → 43246 (−41),LinesCovered39010 ≥ the 38973 floor.No threshold, exclusion list or analyzer severity was lowered, weakened or deleted. No production file was added to a coverage exclusion list, and no
[ExcludeFromCodeCoverage]attribute was added.Determinism: no
Thread.Sleep,Task.Delay,Stopwatch,DateTime.Nowor timing tolerance was added to any test. Deadline behaviour is tested only throughFakeTimeProviderand the latch-basedArmingBarrierTimeProvider. TheTask.Delay(200)andTask.Delay(50)calls inTimeOutTask_Tests.csare pre-existing; this change only added a comment naming them as the reason that class keeps[DoNotParallelize].Recommended
Backward Compatibility / Migration Notes
GetTableInViewAsyncgains a trailing optional parameter. Existing call sites compile unchanged, and a null provider resolves toTimeProvider.System, so production timing is unchanged.EtlAsync's return type changes from(object[,] data, ...)to(object[,]? data, ...). This is a nullability annotation change, not a shape change. Every in-repo consumer is updated; the nullable gate passes at zero warnings, which is the evidence that the annotation propagated correctly.TimeoutAfteroverloads. They were inert (see above) and had no production caller besidesEtlAsyncOld, which is also removed. The proxy-returningTimeProvideroverloads are retained untouched.EtlAsyncOldis removed. It had one production caller, inside the code also deleted.Risks and Mitigations
timeoutMs[DoNotParallelize]fromOlTableExtensions_Tests.csreintroduces flakinessspec.mdRisks mandates. The justification is the code change, not observed green runs —RunsObserved: 0is recorded deliberately, and the green full-suite run explicitly declines to be cited as the justification.TimeProvider.System.CreateCancellationTokenSourcebehaves differently fromnew CancellationTokenSource(ms)cscinvocation (CS1061Count: 0, with both confounders excluded). Behavioural equivalence rests on the default-path guard test rather than on IL inspection of the BCL shim; residual risk is low and is recorded in the code review.CancelAftercall is later added to a provider-created sourceCancelAfter(TimeSpan)does not terminate the original delay timer.Review Guide
Suggested order:
docs/features/active/2026-09-08-etl-deadline-mechanics-follow-ups-825/plan.2026-09-08T23-51.md— read the adjudicated design conflict section first. It records a measured conflict between two acceptance criteria and the decision that closed it.GetTableInViewAsyncClockTests.cs— the new test file.feature-audit.2026-09-10T00-45.md— all 35 criteria evaluated row by row.evidence/other/plan-deviations.md— five recorded deviations, each adjudicated.Diff noise warning. 64 files change, but seven of them are machine artifacts totalling roughly 740,000 lines: two Cobertura XML documents and five MSBuild logs. The reviewable surface is 11 source files plus about 60 small Markdown artifacts. Automated PR-context classification reports "Core logic changes: 0 files" for this branch; that is a misclassification caused by top-N-churn truncation, not an accurate description.
All five committed MSBuild logs were sanitised of absolute host paths before commit, and carry zero occurrences of the host-profile path token.
Follow-ups
Four follow-ups are recorded in
evidence/other/ac35-reachability-observation.mdand are deliberately deferred until after this epic merges, plus one raised by review:GetTableInViewAsyncreturns null on timeout behind a non-null contract. Traced and documented here, not fixed. On the ordinary timeout pathTimeOutTask.RunWithTimeoutabsorbs the cancellation, exhausts its single internal retry, and returnsdefault(TResult)without throwing, so neithercatchblock is entered and the method returns null throughreturn table!. This change does not alter that behaviour.UtilitiesCS/Threading/TimeOutTask.csremains 966 lines, over the 500-line file cap. This change is a reduction, not a resolution, of that cap violation.main.250 * rowCountbudget still needs a real measurement, collected fromLogTableTimingpayloads in a live Outlook session.UtilitiesCS.Test/OutlookObjects/Table/OlTableExtensions_Tests.csis about 1822 lines and no existing follow-up names it. Recommend the epic add it.Non-blocking review suggestions not applied here, because they would touch test files after the recorded final clean toolchain pass: add a
[Timeout]attribute to the three barrier-awaiting tests so a regression fails rather than hangs, and have the new class comment name the specific reason it keeps[DoNotParallelize].GitHub Auto-close
None. GitHub CLI validation was unavailable when this context was collected, so no closing keyword is emitted. This pull request also targets the epic integration branch rather than the repository default branch, where GitHub does not honour closing keywords in any case.
Issue #825 should be closed by the epic integration pull request when
epic/review-residuals-2026-09-08-integrationreachesmain. Issues #780, #798 and #811 appear in this branch's context as references only and are not resolved by this change.🤖 Generated with Claude Code
https://claude.ai/code/session_01URyGon15RvDXcUEo96tvyh