Skip to content

fix(actor-typed): discard stale ReceiveTimeout after cancelReceiveTimeout to avoid NPE#3086

Merged
He-Pin merged 2 commits into
mainfrom
fix/cancel-receive-timeout-npe
Jun 19, 2026
Merged

fix(actor-typed): discard stale ReceiveTimeout after cancelReceiveTimeout to avoid NPE#3086
He-Pin merged 2 commits into
mainfrom
fix/cancel-receive-timeout-npe

Conversation

@He-Pin

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

Copy link
Copy Markdown
Member

Motivation

When context.cancelReceiveTimeout() is called while a classic ReceiveTimeout message has already been enqueued in the mailbox by the scheduler, the typed actor crashes with a NullPointerException inside InterceptorImpl.receive.

Root cause trace:

  1. setReceiveTimeout(d, msg) → sets receiveTimeoutMsg = msg, schedules classic.ReceiveTimeout via scheduleOnce
  2. Before the actor processes the enqueued ReceiveTimeout, another message triggers cancelReceiveTimeout()
  3. cancelReceiveTimeout() sets receiveTimeoutMsg = null and calls classicActorContext.setReceiveTimeout(Duration.Undefined)
  4. The already-enqueued classic.ReceiveTimeout cannot be retracted from the mailbox (the Cancellable.cancel() only prevents future scheduling, not already-delivered messages)
  5. ActorAdapter.aroundReceive matches classic.ReceiveTimeout and calls handleMessage(ctx.receiveTimeoutMsg)with null
  6. The null message propagates through Behavior.interpretMessageInterceptorImpl.receivemsg.getClass throws NPE

This bug was observed in both 2.8.3 and 2.6.21 of the upstream project. A reproduction is available at https://github.com/lolboxen/akka-receive-timeout-bug.

Modification

  • ActorAdapter.aroundReceive: null-check ctx.receiveTimeoutMsg before forwarding to handleMessage. If null (timeout was cancelled), the stale classic.ReceiveTimeout is silently discarded.
  • Added CancelReceiveTimeoutSpec: a deterministic regression test that directly sends classic.ReceiveTimeout to a typed actor after cancelReceiveTimeout() has been called, verifying the actor survives without NPE. The test uses an interceptor in the behavior stack to exercise the exact InterceptorImpl.receive code path where the NPE manifests.

Result

Typed actors no longer crash when cancelReceiveTimeout() races with an already-enqueued stale ReceiveTimeout. The fix is a single null-guard at the adapter layer — the typed behavior stack never sees a null message.

Tests

sbt "actor-typed-tests / Test / testOnly org.apache.pekko.actor.typed.CancelReceiveTimeoutSpec"
→ All tests passed

sbt "actor-typed-tests / Test / testOnly org.apache.pekko.actor.typed.ActorContextSpec org.apache.pekko.actor.typed.TimerSpec org.apache.pekko.actor.typed.CancelReceiveTimeoutSpec"
→ 14 tests passed, 0 failed

The regression test was verified to fail without the fix (NPE at InterceptorImpl.scala:94) and pass with the fix.

References

Motivation:
cancelReceiveTimeout() 将 receiveTimeoutMsg 置 null,
但已通过 scheduleOnce 入队邮箱的 classic ReceiveTimeout 无法撤回;
stale 消息以 null 传入 typed 行为栈, 导致 InterceptorImpl.receive
中 msg.getClass 抛出 NullPointerException。

Modification:
ActorAdapter.aroundReceive 在转发 receiveTimeoutMsg 前做 null 检查,
stale timeout 直接丢弃; 新增 CancelReceiveTimeoutSpec 回归测试。

Result:
typed actor 在 cancelReceiveTimeout 与 stale ReceiveTimeout 竞态下
不再崩溃, 行为栈对调用方透明。
@He-Pin He-Pin added this to the 2.0.0-M4 milestone Jun 18, 2026
@He-Pin He-Pin added the bug Something isn't working label Jun 18, 2026
@He-Pin He-Pin requested a review from pjfanning June 18, 2026 22:28

@pjfanning pjfanning left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@He-Pin He-Pin merged commit 005e242 into main Jun 19, 2026
9 checks passed
@He-Pin He-Pin deleted the fix/cancel-receive-timeout-npe branch June 19, 2026 02:17
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.

2 participants