fix: restore volatile reads/writes downgraded by the VarHandle migration#3008
Merged
Conversation
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
Contributor
There was a problem hiding this comment.
Pull request overview
This PR restores the original volatile (JMM-ordered) read/write semantics that were unintentionally downgraded to plain accesses during the Unsafe → VarHandle 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.getreads withVarHandle.getVolatilefor fields published viacompareAndSet/getAndAdd. - Replace two
VarHandle.setwrites withVarHandle.setVolatileto 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The migration from
sun.misc.UnsafetoVarHandle(#1990, and #1894 forMailbox) replaced the producer writes correctly (compareAndSwapObject→compareAndSet,putOrderedObject→setRelease) but silently downgraded the corresponding reads (and two writes) from volatile to plain access:Unsafe.getObjectVolatile/getIntVolatile/getLongVolatile→VarHandle.get(plain)putObjectVolatile/putIntVolatile→VarHandle.set(plain)VarHandle.get/sethave plain memory semantics even when the field is declaredvolatile, so these accesses lost their happens-before guarantees with the concurrentcompareAndSet/setReleasepublications. 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:
currentStatus/setStatusandsystemQueueGet(mailbox status and system-message queue head).mailbox(Dispatch),childrenRefs/functionRefsreads and thesetTerminatedwrite (Children).underlying/lookupcell reads.currentState/currentResetTimeoutreads.state/watchedByreads and thesetStatewrite.inhabitants/shutdownSchedulereads.associationStateread.CAS-published fields use
getVolatile/setVolatile, restoring the exactgetObjectVolatile/putObjectVolatilesemantics. For a load,getVolatilecompiles to the same instruction as a plain load on x86-64 (MOV) and toLDARon 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 restoredsetVolatilewrites 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 usegetAcquire.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/compilesucceed.References