Skip to content

[Testing][Backend] WorkerResilienceTests polling assertion uses a fixed 300ms delay and fails on contended Windows runners #2572

Description

@Chris0Jeky

What

WorkerResilienceTests.ProposalHousekeepingWorker_WhenDbThrows_LogsErrorAndContinuesPolling asserts a background worker has polled at least once after a fixed 300 ms wall-clock delay. On a contended Windows runner the worker has not reached its first iteration inside that window, and the test fails.

Observed on PR #2522, run 33922229492, job 101182893700, API Integration / API Integration (windows-latest):

[xUnit.net 00:11:12.36]  Taskdeck.Api.Tests.Resilience.WorkerResilienceTests
                         .ProposalHousekeepingWorker_WhenDbThrows_LogsErrorAndContinuesPolling [FAIL]
  Failed  ... [894 ms]
  Error Message:
   Expected callCount to be greater than 0, but found 0.
     at FluentAssertions.Numeric.NumericAssertions`2.BeGreaterThan(...)

Failed!  - Failed: 1, Passed: 2829, Skipped: 4, Total: 2834, Duration: 20 m 33 s

One failure in 2 834. The suite took 20 m 33 s on that leg, which is the corroborating detail: the runner was heavily contended, which is exactly when a fixed 300 ms budget stops being enough.

Mechanism

backend/tests/Taskdeck.Api.Tests/Resilience/WorkerResilienceTests.cs:75-103:

var runTask = worker.StartAsync(cts.Token);
await Task.Delay(300);          // <- fixed wall-clock window
cts.Cancel();
...
callCount.Should().BeGreaterThan(0);

callCount is incremented from inside the scope factory the worker calls on its first iteration. The test grants exactly 300 ms for thread-pool scheduling, host startup and that first iteration to all happen. Nothing waits for the condition; the delay is the synchronisation. When the runner is slow the worker simply has not run yet, and the assertion reports 0 — a scheduling artifact, not a resilience defect.

Note the failure was reported at 894 ms, i.e. the test itself took longer than its own 300 ms budget to get through the delay-and-cancel sequence, which is consistent with the runner being the bottleneck rather than the worker being broken.

Suggested fix

Replace the fixed delay with a bounded wait on the condition — poll callCount > 0 (and the heartbeat) up to a generous ceiling, e.g. 5 s, returning as soon as it is satisfied. That keeps the test fast on a healthy runner, removes the wall-clock assumption, and preserves exactly what it means to assert. The same pattern is worth auditing across WorkerResilienceTests — any other await Task.Delay(...) followed by a "did it happen yet" assertion has the identical shape.

Why this is filed separately

It shares a theme with the Windows-runner-speed cohort but is a different suite, language and mechanism, and folding it in would let a fix for one be miscredited as closing the other:

Not caused by the PR it was seen on

PR #2522 changes only scripts/ci/dev-up.test.mjs, scripts/dev-up.ps1 and scripts/dev-up.sh. It touches no C#, no worker, and nothing this test exercises — git diff origin/main...HEAD | grep -ci 'ProposalHousekeeping\|WorkerResilience' returns 0.

Refs #2378, #2161, #2561

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendPrimary implementation impact in .NET API/domain/application/infrastructure.ciCI/CD pipeline, workflow, and build infrastructure changes.testingTest coverage, harnesses, regression prevention, and verification pipeline work.

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions