This is an automated email from the ASF dual-hosted git repository.

He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git


The following commit(s) were added to refs/heads/main by this push:
     new b2ae2fafa7 fix: deliver stashed timer messages for UnboundedStash and 
UnrestrictedStash (#3264)
b2ae2fafa7 is described below

commit b2ae2fafa71b0f1f604d21d21f9009f62d7f2819
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Sun Jul 5 16:59:14 2026 +0800

    fix: deliver stashed timer messages for UnboundedStash and 
UnrestrictedStash (#3264)
    
    Motivation:
    Timers rewrites currentMessage to the unwrapped timer message before 
invoking
    the user's receive, so that stash() captures the actual message rather than 
the
    internal TimerMsg wrapper. This rewrite was guarded by isInstanceOf[Stash],
    which is false for actors mixing in UnboundedStash or UnrestrictedStash 
directly
    - they are siblings of Stash (all extend UnrestrictedStash), not subtypes. 
As a
    result stash() captured the TimerMsg wrapper, and on unstashAll() the 
wrapper was
    intercepted again and discarded (single-shot timer already removed, or stale
    generation for a periodic timer), losing the message.
    
    Modification:
    - Guard the currentMessage rewrite with isInstanceOf[StashSupport] instead 
of
      isInstanceOf[Stash], so all three public stash traits (Stash, 
UnboundedStash,
      UnrestrictedStash - which all extend StashSupport) receive the unwrapped
      message.
    
    Result:
    Timer messages stashed with UnboundedStash or UnrestrictedStash are no 
longer
    lost on unstash; behavior for Stash is unchanged and non-stashing actors are
    unaffected.
    
    Tests:
    - actor-tests/testOnly org.apache.pekko.actor.TimerSpec
      org.apache.pekko.actor.TimersAndStashSpec - all passed. Added directional
      tests for UnboundedStash and UnrestrictedStash that fail before the fix 
(the
      timer message is lost on unstash).
    - actor/mimaReportBinaryIssues - no binary issues.
    
    References:
    Fixes #3258
---
 .../scala/org/apache/pekko/actor/TimerSpec.scala   | 56 ++++++++++++++++++++++
 .../main/scala/org/apache/pekko/actor/Timers.scala |  8 +++-
 2 files changed, 62 insertions(+), 2 deletions(-)

diff --git a/actor-tests/src/test/scala/org/apache/pekko/actor/TimerSpec.scala 
b/actor-tests/src/test/scala/org/apache/pekko/actor/TimerSpec.scala
index 2d51cf27ee..85e67b77ec 100644
--- a/actor-tests/src/test/scala/org/apache/pekko/actor/TimerSpec.scala
+++ b/actor-tests/src/test/scala/org/apache/pekko/actor/TimerSpec.scala
@@ -341,6 +341,44 @@ class TimersAndStashSpec extends PekkoSpec {
     }
   }
 
+  // Same scenario as ActorWithTimerAndStash but mixing in UnboundedStash, 
which is a sibling of
+  // Stash (both extend UnrestrictedStash), to cover the timer/stash 
interaction for it too (#3258).
+  class ActorWithTimerAndUnboundedStash(probe: ActorRef) extends Actor with 
Timers with UnboundedStash {
+    timers.startSingleTimer("key", "scheduled", 50.millis)
+    def receive: Receive = stashing
+    def notStashing: Receive = {
+      case msg => probe ! msg
+    }
+
+    def stashing: Receive = {
+      case StopStashing =>
+        context.become(notStashing)
+        unstashAll()
+      case "scheduled" =>
+        probe ! "saw-scheduled"
+        stash()
+    }
+  }
+
+  // Same scenario mixing in UnrestrictedStash directly (needs an explicitly 
configured
+  // deque-based mailbox, as it does not declare a RequiresMessageQueue) 
(#3258).
+  class ActorWithTimerAndUnrestrictedStash(probe: ActorRef) extends Actor with 
Timers with UnrestrictedStash {
+    timers.startSingleTimer("key", "scheduled", 50.millis)
+    def receive: Receive = stashing
+    def notStashing: Receive = {
+      case msg => probe ! msg
+    }
+
+    def stashing: Receive = {
+      case StopStashing =>
+        context.become(notStashing)
+        unstashAll()
+      case "scheduled" =>
+        probe ! "saw-scheduled"
+        stash()
+    }
+  }
+
   "Timers combined with stashing" should {
 
     "work" in {
@@ -350,6 +388,24 @@ class TimersAndStashSpec extends PekkoSpec {
       actor ! StopStashing
       probe.expectMsg("scheduled")
     }
+
+    "work with UnboundedStash (#3258)" in {
+      val probe = TestProbe()
+      val actor = system.actorOf(Props(new 
ActorWithTimerAndUnboundedStash(probe.ref)))
+      probe.expectMsg("saw-scheduled")
+      actor ! StopStashing
+      probe.expectMsg("scheduled")
+    }
+
+    "work with UnrestrictedStash (#3258)" in {
+      val probe = TestProbe()
+      val actor = system.actorOf(
+        Props(new ActorWithTimerAndUnrestrictedStash(probe.ref))
+          .withMailbox("pekko.actor.mailbox.unbounded-deque-based"))
+      probe.expectMsg("saw-scheduled")
+      actor ! StopStashing
+      probe.expectMsg("scheduled")
+    }
   }
 
 }
diff --git a/actor/src/main/scala/org/apache/pekko/actor/Timers.scala 
b/actor/src/main/scala/org/apache/pekko/actor/Timers.scala
index 5b264ba4ac..9c57c6100b 100644
--- a/actor/src/main/scala/org/apache/pekko/actor/Timers.scala
+++ b/actor/src/main/scala/org/apache/pekko/actor/Timers.scala
@@ -55,8 +55,12 @@ trait Timers extends Actor {
           case OptionVal.Some(m: AutoReceivedMessage) =>
             context.asInstanceOf[ActorCell].autoReceiveMessage(Envelope(m, 
self, context.system))
           case OptionVal.Some(m) =>
-            if (this.isInstanceOf[Stash]) {
-              // this is important for stash interaction, as stash will look 
directly at currentMessage #24557
+            if (this.isInstanceOf[StashSupport]) {
+              // This is important for stash interaction, as stash reads the 
message directly from
+              // currentMessage (StashSupport) #24557. We match StashSupport 
rather than Stash so that
+              // actors mixing in UnboundedStash or UnrestrictedStash directly 
- which are siblings of
+              // Stash, not subtypes - also rewrite the unwrapped timer 
message; otherwise stash()
+              // would re-stash the TimerMsg wrapper and the message would be 
lost on unstash (#3258).
               actorCell.currentMessage = actorCell.currentMessage.copy(message 
= m)
             }
             super.aroundReceive(receive, m)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to