Skip to content

fix: restore volatile reads/writes downgraded by the VarHandle migration#3008

Merged
He-Pin merged 1 commit into
mainfrom
fix/jdk25-restore-volatile-reads
May 29, 2026
Merged

fix: restore volatile reads/writes downgraded by the VarHandle migration#3008
He-Pin merged 1 commit into
mainfrom
fix/jdk25-restore-volatile-reads

Conversation

@He-Pin

@He-Pin He-Pin commented May 29, 2026

Copy link
Copy Markdown
Member

Motivation

The migration from sun.misc.Unsafe to VarHandle (#1990, and #1894 for Mailbox) replaced the producer writes correctly (compareAndSwapObjectcompareAndSet, putOrderedObjectsetRelease) but silently downgraded the corresponding reads (and two writes) from volatile to plain access:

  • Unsafe.getObjectVolatile / getIntVolatile / getLongVolatileVarHandle.get (plain)
  • putObjectVolatile / putIntVolatileVarHandle.set (plain)

VarHandle.get / set have plain memory semantics even when the field is declared volatile, so these accesses lost their happens-before guarantees with the concurrent compareAndSet / setRelease publications. On weakly-ordered hardware (AArch64) a reader can observe a stale value, and inside tight loops the plain read may be hoisted by the JIT. On x86-64 plain loads happen to have acquire semantics, which is why this largely went unnoticed.

Modification

Restore the original ordered semantics on every downgraded access:

  • Mailbox: currentStatus / setStatus and systemQueueGet (mailbox status and system-message queue head).
  • ActorCell: mailbox (Dispatch), childrenRefs / functionRefs reads and the setTerminated write (Children).
  • RepointableActorRef: underlying / lookup cell reads.
  • CircuitBreaker: currentState / currentResetTimeout reads.
  • PromiseActorRef (AskSupport): state / watchedBy reads and the setState write.
  • MessageDispatcher: inhabitants / shutdownSchedule reads.
  • Artery Association: associationState read.

CAS-published fields use getVolatile / setVolatile, restoring the exact getObjectVolatile / putObjectVolatile semantics. For a load, getVolatile compiles to the same instruction as a plain load on x86-64 (MOV) and to LDAR on AArch64 — i.e. exactly what the original code emitted — so this restores the prior semantics at no extra cost on the read side. The two restored setVolatile writes carry a StoreLoad fence as they did before the migration.

The lock-free node-queue spin reads (AbstractNodeQueue / AbstractBoundedNodeQueue) are handled in a separate PR, as they pair with release stores and use getAcquire.

Result

All concurrently-accessed fields that were volatile before the migration are volatile again, closing latent visibility races — most importantly the mailbox status / system-message queue and the actor cell/mailbox references. Method signatures are unchanged, so MiMa is unaffected.

Tests

  • sbt actor/compile remote/compile succeed.

References

Motivation:
The migration from sun.misc.Unsafe to VarHandle (#1990, and #1894 for
Mailbox) replaced the producer writes correctly (compareAndSwapObject
-> compareAndSet, putOrderedObject -> setRelease) but silently
downgraded the corresponding reads and a couple of writes from
volatile to plain access: `Unsafe.getObjectVolatile`/`getIntVolatile`/
`getLongVolatile` became `VarHandle.get` (plain) and two
`putObjectVolatile`/`putIntVolatile` became `VarHandle.set` (plain).

`VarHandle.get`/`set` have plain memory semantics even when the field
is declared `volatile`, so these accesses lost their happens-before
guarantees with the concurrent compareAndSet/setRelease publications.
On weakly-ordered hardware (AArch64) a reader can observe a stale value,
and inside tight loops the plain read may be hoisted by the JIT. (On
x86-64 plain loads happen to have acquire semantics, which is why this
mostly went unnoticed.)

Modification — restore the original ordered semantics on every
downgraded access:
- Mailbox: `currentStatus`/`setStatus` and `systemQueueGet` (status and
  system-message queue head).
- ActorCell: `mailbox` (Dispatch), `childrenRefs`/`functionRefs` reads
  and the `setTerminated` write (Children).
- RepointableActorRef: `underlying`/`lookup` cell reads.
- CircuitBreaker: `currentState`/`currentResetTimeout` reads.
- PromiseActorRef (AskSupport): `state`/`watchedBy` reads and the
  `setState` write.
- MessageDispatcher: `inhabitants`/`shutdownSchedule` reads.
- Artery Association: `associationState` read.

CAS-published fields use `getVolatile`/`setVolatile` (restoring the
exact getObjectVolatile/putObjectVolatile semantics). For a load,
`getVolatile` compiles to the same instruction as a plain load on
x86-64 (MOV) and to LDAR on AArch64 — i.e. exactly what the original
Unsafe code emitted — so this restores the prior semantics at no extra
cost on the read side. The two writes restored to `setVolatile` carry a
StoreLoad fence as they did before the migration.

The node-queue spin reads (AbstractNodeQueue/AbstractBoundedNodeQueue)
are addressed separately, as they pair with release stores and use
getAcquire.

Result:
All concurrently-accessed fields that were volatile before the
Unsafe->VarHandle migration are volatile again, closing latent
visibility races (most importantly the mailbox status / system-message
queue and the actor cell/mailbox references). Method signatures are
unchanged, so there is no binary-compatibility impact.

References: #2870
@He-Pin He-Pin requested review from Copilot and pjfanning May 29, 2026 11:05
@He-Pin He-Pin added this to the 2.0.0-M3 milestone May 29, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR restores the original volatile (JMM-ordered) read/write semantics that were unintentionally downgraded to plain accesses during the UnsafeVarHandle migrations. This closes latent visibility races on weakly-ordered hardware by switching affected reads to getVolatile and two writes to setVolatile, matching the pre-migration get*Volatile / put*Volatile behavior.

Changes:

  • Replace several VarHandle.get reads with VarHandle.getVolatile for fields published via compareAndSet / getAndAdd.
  • Replace two VarHandle.set writes with VarHandle.setVolatile to restore prior volatile-store ordering.
  • Add inline comments documenting the publication mechanism and why volatile access is required.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.

Show a summary per file
File Description
remote/src/main/scala/org/apache/pekko/remote/artery/Association.scala Restore volatile read of association shared state via getVolatile.
actor/src/main/scala/org/apache/pekko/pattern/CircuitBreaker.scala Restore volatile reads of breaker state and reset timeout.
actor/src/main/scala/org/apache/pekko/pattern/AskSupport.scala Restore volatile reads of PromiseActorRef state/watchers and a volatile state write.
actor/src/main/scala/org/apache/pekko/dispatch/Mailbox.scala Restore volatile mailbox status read/write and system-message queue head read.
actor/src/main/scala/org/apache/pekko/dispatch/AbstractDispatcher.scala Restore volatile reads of dispatcher inhabitants and shutdown schedule.
actor/src/main/scala/org/apache/pekko/actor/RepointableActorRef.scala Restore volatile reads of cell / lookup references.
actor/src/main/scala/org/apache/pekko/actor/dungeon/Dispatch.scala Restore volatile read of ActorCell mailbox reference.
actor/src/main/scala/org/apache/pekko/actor/dungeon/Children.scala Restore volatile reads of children/function refs and a volatile terminal-state write.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@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

@pjfanning pjfanning added the bug Something isn't working label May 29, 2026
@He-Pin He-Pin merged commit c1910b2 into main May 29, 2026
10 checks passed
@He-Pin He-Pin deleted the fix/jdk25-restore-volatile-reads branch May 29, 2026 11:39
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.

3 participants