He-Pin opened a new issue, #3235:
URL: https://github.com/apache/pekko/issues/3235
### Motivation
`ManualTime.expectNoMessageFor` uses `Duration.Zero` when checking test
probes for unexpected messages after advancing the clock. Because scheduled
tasks run synchronously on the calling thread inside `timePasses`, but messages
they send to test probe actors are delivered asynchronously through the actor
system's dispatcher/mailbox, the zero-timeout poll can miss messages that are
still in flight. This produces false-negative test results: assertions that "no
messages were received" pass when they should fail.
### Current behavior
In
`actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/scaladsl/ManualTime.scala:76-79`:
```scala
def expectNoMessageFor(duration: FiniteDuration, on: TestProbe[?]*): Unit = {
delegate.timePasses(duration)
on.foreach(_.expectNoMessage(Duration.Zero))
}
```
The call chain is:
1. `delegate.timePasses(duration)` runs scheduled `Runnable`s synchronously
on the calling thread (`ExplicitlyTriggeredScheduler.scala:98`).
2. Those runnables may send messages to test probe actors via `!`, enqueuing
them in the actor's mailbox.
3. The test probe actor processes messages on a separate dispatcher thread,
calling `queue.offerLast(other)` (`TestProbeImpl.scala:66`).
4. `expectNoMessage(Duration.Zero)` calls
`receiveOne_internal(Duration.Zero)`, which does a non-blocking
`queue.pollFirst()` (`TestProbeImpl.scala:202-203`) with zero wait.
Between steps 2 and 4, there is no synchronization or delay. Messages
dispatched in step 2 may not yet be in the probe's `LinkedBlockingDeque` when
step 4 polls.
The same issue exists in the Java DSL at
`actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/javadsl/ManualTime.scala:76-79`.
Notably, `ExplicitlyTriggeredScheduler.timePasses` itself includes
`Thread.sleep(100)` at its start (`ExplicitlyTriggeredScheduler.scala:72`) with
a comment about giving "dispatchers time to clear," acknowledging that async
message delivery races exist in this context. However, no equivalent delay
exists between task execution and the `Duration.Zero` check.
### Expected behavior
After `timePasses` completes, the `expectNoMessage` check should allow
sufficient time for any messages dispatched by the scheduled tasks to be
delivered to the test probe actors. The idiomatic fix is to use the probe's
default no-message timeout via `expectNoMessage()` (no arguments), which uses
`settings.ExpectNoMessageDefaultTimeout` (defined in
`TestKitSettings.scala:77`). This timeout is dilated by the test time factor
and is specifically designed for "expect no message" assertions.
### Code evidence
-
`actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/scaladsl/ManualTime.scala:78`
— `Duration.Zero` in Scala DSL
-
`actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/javadsl/ManualTime.scala:78`
— `Duration.ZERO` in Java DSL
-
`actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/internal/TestProbeImpl.scala:202-203`
— zero duration triggers non-blocking `pollFirst`
-
`actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/internal/TestProbeImpl.scala:217-218`
— `expectNoMessage()` (no args) uses `ExpectNoMessageDefaultTimeout`
-
`testkit/src/main/scala/org/apache/pekko/testkit/ExplicitlyTriggeredScheduler.scala:72`
— existing `Thread.sleep(100)` acknowledging the race pattern
### Reproduction
Create a test using `ManualTime` where a scheduled task sends a message to a
test probe actor. Call `expectNoMessageFor` with a duration that triggers the
scheduled task. Due to the race, the test may intermittently pass (no message
detected) even though the probe should have received a message during the time
advance.
A deterministic reproduction would require a slow dispatcher or deliberate
scheduling to widen the race window, but the fundamental issue is that the
zero-timeout check is unsound.
### Impact
- **Modules affected:** `pekko-actor-testkit-typed` (both Scala and Java
DSLs)
- **Use cases affected:** Any test using `ManualTime.expectNoMessageFor` to
verify that no messages are delivered during a time advance. This is a core
testing utility documented in the official Pekko testing examples
(`ManualTimerExampleSpec.scala`).
- **Severity:** Intermittent false-negative test results. Tests may
incorrectly pass when a bug exists, reducing confidence in the test suite.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]