Skip to content

De-flake ReceiveTimeoutSpec: measure the two timeout cycles with dilated probe waits instead of one flat latch - #8541

Merged
Aaronontheweb merged 3 commits into
devfrom
fix/receive-timeout-spec-probe
Sep 10, 2026
Merged

Aaronontheweb merged 3 commits into
devfrom
fix/receive-timeout-spec-probe

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Sep 9, 2026 •

Copy link
Copy Markdown
Member

What changes

ReceiveTimeoutSpec stops waiting on latches built with new TestLatch(...) and waits on TestProbes instead. The first commit rewrote the two tests that failed; the second migrates the remaining seven synchronous facts the same way, so no blocking wait is left in the file.

  • An_actor_with_receive_timeout_must_get_timeout_while_receiving_only_NotInfluenceReceiveTimeout_messages now awaits three probe messages in order: "timeout", then "tick", then "timeout". Each wait has its own dilated budget: 1 s plus 4 s of slack for a timeout cycle, 1 s for the tick, which is a local self-send. It also now proves the transparent tick was delivered between the two timeouts, which the old latch could not.
  • An_actor_with_receive_timeout_must_not_receive_timeout_message_when_not_specified now asserts with ExpectNoMsgAsync instead of catching a TimeoutException from a blocking 5 s latch wait.

Why

The first test failed on CI build 131165 after exactly 5 s. The product is correct: a message marked INotInfluenceReceiveTimeout cannot cancel or re-arm the pending timer, and the code matches Pekko line for line. Two things in the test made it fragile:

  • A latch built with new TestLatch(n) never dilates its timeout, unlike one built with CreateTestLatch(), so its 5 s wall ignores akka.test.timefactor. The earlier fix in Fix flaky ReceiveTimeoutSpec CI timeout #8153 helped the sibling test that uses the helper and could not help this one. About 83 call sites in the repo build latches this way.
  • The test parked a thread-pool worker in a blocking wait while it measured two consecutive 1 s scheduler cycles. On the net10.0 lane the scheduler's clock loop, the actor's mailbox, and the test all share the .NET thread pool, whose floor is the processor count because xUnit v3 only raises it on its parallel path and Akka.Tests disables parallelization. On a 2-vCPU agent one worker ran everything, and any stall landed on the timer. Because the receive timeout is re-armed after the handler returns, lateness in the first cycle was added to the second.

Second commit: the remaining facts

The three test actors take a probe instead of a latch and send "timeout" when the receive timeout fires. The seven remaining facts become async and await the probe with the same budget the latch had, or a receive-timeout plus 4 s where the fact measures a 1 s cycle, as the first commit does. The turn-off fact asserts with ExpectNoMsgAsync instead of catching a TimeoutException from a 1 s latch wait. The issue-469 fact keeps its probe apart from the test actor so the dead-letter check stays separate. A grep for synchronous TestKit calls and Ready( on the file returns nothing. The class run three times: 10 of 10 each time.

How it was checked

dotnet build src/core/Akka.Tests -c Release -warnaserror clean. The rewritten test run 25 times in a row: 25 passes, about 2 s each. The whole ReceiveTimeoutSpec class: 10 passed.

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

…ted probe waits instead of one flat latch

The latch built with `new TestLatch(...)` does not dilate with akka.test.timefactor,
and the test parked a pool worker in CountdownEvent.Wait while the scheduler's clock
loop and the mailbox shared the same thread pool. Rewrote the test to await a
TestProbe through three per-phase, dilated ExpectMsgAsync budgets instead of one flat
5s wall, stopping the actor in a finally. The probe sequence ("timeout", "tick",
"timeout") also proves the transparent tick was actually delivered, rather than just
inferring it from a latch count.

Also converts the companion negative-assertion test (no receive-timeout ever set)
from a blocking TestLatch wait to ExpectNoMsgAsync on a probe, so it stops parking a
pool worker for the duration of the wait.
@Aaronontheweb
Aaronontheweb force-pushed the fix/receive-timeout-spec-probe branch from 3c749c9 to 371c0f8 Compare September 9, 2026 18:18
@Aaronontheweb
Aaronontheweb merged commit 86e479d into dev Sep 10, 2026
15 checks passed
@Aaronontheweb
Aaronontheweb deleted the fix/receive-timeout-spec-probe branch September 10, 2026 16:11
Aaronontheweb added a commit that referenced this pull request Sep 12, 2026
…ted probe waits instead of one flat latch (#8541)

The latch built with `new TestLatch(...)` does not dilate with akka.test.timefactor,
and the test parked a pool worker in CountdownEvent.Wait while the scheduler's clock
loop and the mailbox shared the same thread pool. Rewrote the test to await a
TestProbe through three per-phase, dilated ExpectMsgAsync budgets instead of one flat
5s wall, stopping the actor in a finally. The probe sequence ("timeout", "tick",
"timeout") also proves the transparent tick was actually delivered, rather than just
inferring it from a latch count.

Also converts the companion negative-assertion test (no receive-timeout ever set)
from a blocking TestLatch wait to ExpectNoMsgAsync on a probe, so it stops parking a
pool worker for the duration of the wait.

(cherry picked from commit 86e479d)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant