Skip to content

fix: deliver stashed timer messages for UnboundedStash and UnrestrictedStash (#3258)#3264

Open
He-Pin wants to merge 1 commit into
mainfrom
fix/3258-timers-stashsupport
Open

fix: deliver stashed timer messages for UnboundedStash and UnrestrictedStash (#3258)#3264
He-Pin wants to merge 1 commit into
mainfrom
fix/3258-timers-stashsupport

Conversation

@He-Pin

@He-Pin He-Pin commented Jun 28, 2026

Copy link
Copy Markdown
Member

Motivation

Timers rewrites actorCell.currentMessage to the unwrapped timer message
before invoking the user's receive, so that stash() (which reads
currentMessage directly) captures the actual message rather than the internal
TimerMsg wrapper.

That rewrite was guarded by this.isInstanceOf[Stash]. But Stash,
UnboundedStash and UnrestrictedStash are siblings — they all extend
UnrestrictedStash / StashSupport, and UnboundedStash/UnrestrictedStash
are not subtypes of Stash. So for an actor mixing in UnboundedStash or
UnrestrictedStash directly, the guard was false, the rewrite was skipped,
and stash() captured the TimerMsg wrapper. On unstashAll() the wrapper was
intercepted again and discarded (the single-shot timer was already removed, or
the generation was stale for a periodic timer), so the message was lost.

The same guard also excluded the Java‑API stash actors (AbstractActorWithStash
and friends, via AbstractActorStashSupport), which have the same problem.

Fixes #3258.

Modification

  • Guard the currentMessage rewrite with this.isInstanceOf[StashSupport]
    instead of this.isInstanceOf[Stash]. StashSupport is the common base of
    all stash‑capable actors (Stash, UnboundedStash, UnrestrictedStash, and
    the Java‑API stash classes), and it is exactly the set of actors whose
    stash() reads currentMessage — so it is the precise, correct boundary.
  • Expanded the explanatory comment so the sibling relationship and the
    re‑stash/lost‑message mechanism are clear (keeping the original #24557
    reference).

Result

  • Timer messages stashed with UnboundedStash or UnrestrictedStash (and the
    Java‑API stash actors) are no longer lost on unstashAll().
  • Behavior for Stash is unchanged, and non‑stashing actors are unaffected
    (the guard is still false for them).
  • The change is a method‑body change in Timers.aroundReceive; no public API
    or binary‑compatibility change
    (actor / mimaReportBinaryIssues is clean).

Tests

  • sbt "actor-tests/testOnly org.apache.pekko.actor.TimerSpec org.apache.pekko.actor.TimersAndStashSpec"
    → all passed.
  • Added directional tests mirroring the existing Stash test, for
    UnboundedStash and UnrestrictedStash (the latter with an explicit
    deque‑based mailbox, since UnrestrictedStash does not declare
    RequiresMessageQueue). Both fail before the fix (the stashed timer
    message is lost: expectMsg("scheduled") times out) and pass after.
  • sbt "actor/mimaReportBinaryIssues" → no binary issues.

References

Fixes #3258


This is an original contribution to Apache Pekko, made under the Apache License
2.0 (i.e. the changes are now Apache licensed). No third-party or Akka-derived
code is included.

…edStash

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
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]) {

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.

@He-Pin He-Pin requested a review from pjfanning June 28, 2026 16:41
@He-Pin He-Pin added the bug Something isn't working label Jun 28, 2026
@He-Pin He-Pin added this to the 2.0.0-M4 milestone Jun 28, 2026
@He-Pin He-Pin marked this pull request as ready for review June 28, 2026 16:42
@He-Pin

He-Pin commented Jun 28, 2026

Copy link
Copy Markdown
Member Author

@pjfanning I think we can backport this to 1.7.0 ,wdyt

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Timer message lost when stashed with UnboundedStash or UnrestrictedStash

1 participant