Skip to content

Commit e29b9c9

Browse files
authored
Remove indirection in SubscribableListener (#96642)
There's no need for a separate `AtomicReference` here, we can hold our current state directly and manipulate it with a `VarHandle`.
1 parent 1bb0fa9 commit e29b9c9

File tree

2 files changed

+65
-36
lines changed

2 files changed

+65
-36
lines changed

server/src/main/java/org/elasticsearch/action/support/SubscribableListener.java

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
import org.elasticsearch.core.TimeValue;
2222
import org.elasticsearch.threadpool.ThreadPool;
2323

24+
import java.lang.invoke.MethodHandles;
25+
import java.lang.invoke.VarHandle;
2426
import java.util.concurrent.ExecutionException;
2527
import java.util.concurrent.Executor;
26-
import java.util.concurrent.atomic.AtomicReference;
2728

2829
/**
2930
* An {@link ActionListener} to which other {@link ActionListener} instances can subscribe, such that when this listener is completed it
@@ -38,16 +39,18 @@ public class SubscribableListener<T> implements ActionListener<T> {
3839
private static final Object EMPTY = new Object();
3940

4041
/**
41-
* If we are incomplete, {@code ref} may refer to one of the following depending on how many waiting subscribers there are:
42+
* If we are incomplete, {@code state} may be one of the following depending on how many waiting subscribers there are:
4243
* <ul>
43-
* <li>If there are no subscribers yet, {@code ref} refers to {@link #EMPTY}.
44-
* <li>If there is one subscriber, {@code ref} refers to it directly.
45-
* <li>If there are more than one subscriber, {@code ref} refers to the head of a linked list of subscribers in reverse order of their
44+
* <li>If there are no subscribers yet, {@code state} is {@link #EMPTY}.
45+
* <li>If there is one subscriber, {@code state} is that subscriber.
46+
* <li>If there are multiple subscribers, {@code state} is the head of a linked list of subscribers in reverse order of their
4647
* subscriptions.
4748
* </ul>
48-
* If we are complete, {@code ref} refers to a {@code Result<T>} which will be used to complete any subsequent subscribers.
49+
* If we are complete, {@code state} is the {@code SuccessResult<T>} or {@code FailureResult} which will be used to complete any
50+
* subsequent subscribers.
4951
*/
50-
private final AtomicReference<Object> ref = new AtomicReference<>(EMPTY);
52+
@SuppressWarnings("FieldMayBeFinal") // updated via VH_STATE_FIELD (and _only_ via VH_STATE_FIELD)
53+
private volatile Object state = EMPTY;
5154

5255
/**
5356
* Add a listener to this listener's collection of subscribers. If this listener is complete, this method completes the subscribing
@@ -90,12 +93,12 @@ public final void addListener(ActionListener<T> listener) {
9093
*/
9194
@SuppressWarnings({ "rawtypes" })
9295
public final void addListener(ActionListener<T> listener, Executor executor, @Nullable ThreadContext threadContext) {
93-
if (tryComplete(ref.get(), listener)) {
96+
if (tryComplete(state, listener)) {
9497
return;
9598
}
9699

97100
final ActionListener<T> wrappedListener = fork(executor, preserveContext(threadContext, listener));
98-
Object currentValue = ref.compareAndExchange(EMPTY, wrappedListener);
101+
Object currentValue = compareAndExchangeState(EMPTY, wrappedListener);
99102
if (currentValue == EMPTY) {
100103
return;
101104
}
@@ -106,7 +109,7 @@ public final void addListener(ActionListener<T> listener, Executor executor, @Nu
106109
}
107110
if (currentValue instanceof ActionListener firstListener) {
108111
final Cell tail = new Cell(firstListener, null);
109-
currentValue = ref.compareAndExchange(firstListener, tail);
112+
currentValue = compareAndExchangeState(firstListener, tail);
110113
if (currentValue == firstListener) {
111114
currentValue = tail;
112115
}
@@ -118,7 +121,7 @@ public final void addListener(ActionListener<T> listener, Executor executor, @Nu
118121
} else {
119122
newCell.next = headCell;
120123
}
121-
currentValue = ref.compareAndExchange(headCell, newCell);
124+
currentValue = compareAndExchangeState(headCell, newCell);
122125
if (currentValue == headCell) {
123126
return;
124127
}
@@ -146,7 +149,7 @@ protected Exception wrapException(Exception exception) {
146149
* @return {@code true} if and only if this listener has been completed (either successfully or exceptionally).
147150
*/
148151
public final boolean isDone() {
149-
return isDone(ref.get());
152+
return isDone(state);
150153
}
151154

152155
/**
@@ -157,10 +160,10 @@ public final boolean isDone() {
157160
*/
158161
@SuppressWarnings({ "unchecked", "rawtypes" })
159162
protected final T rawResult() throws Exception {
160-
final Object refValue = ref.get();
161-
if (refValue instanceof SuccessResult result) {
163+
final Object currentState = state;
164+
if (currentState instanceof SuccessResult result) {
162165
return (T) result.result();
163-
} else if (refValue instanceof FailureResult result) {
166+
} else if (currentState instanceof FailureResult result) {
164167
throw result.exception();
165168
} else {
166169
assert false : "not done";
@@ -185,40 +188,40 @@ private static <T> ActionListener<T> fork(Executor executor, ActionListener<T> l
185188
}
186189

187190
@SuppressWarnings({ "unchecked", "rawtypes" })
188-
private static <T> boolean tryComplete(Object refValue, ActionListener<T> listener) {
189-
if (refValue instanceof SuccessResult successResult) {
191+
private static <T> boolean tryComplete(Object currentState, ActionListener<T> listener) {
192+
if (currentState instanceof SuccessResult successResult) {
190193
successResult.complete(listener);
191194
return true;
192195
}
193-
if (refValue instanceof FailureResult failureResult) {
196+
if (currentState instanceof FailureResult failureResult) {
194197
failureResult.complete(listener);
195198
return true;
196199
}
197200
return false;
198201
}
199202

200203
/**
201-
* If incomplete, atomically update {@link #ref} with the given result and use it to complete any pending listeners.
204+
* If incomplete, atomically update {@link #state} with the given result and use it to complete any pending listeners.
202205
*/
203206
@SuppressWarnings("unchecked")
204207
private void setResult(Object result) {
205208
assert isDone(result);
206209

207-
Object currentValue = ref.get();
210+
Object currentState = state;
208211
while (true) {
209-
if (isDone(currentValue)) {
212+
if (isDone(currentState)) {
210213
// already complete - nothing to do
211214
return;
212215
}
213216

214-
final Object witness = ref.compareAndExchange(currentValue, result);
215-
if (witness == currentValue) {
217+
final Object witness = compareAndExchangeState(currentState, result);
218+
if (witness == currentState) {
216219
// we won the race to complete the listener
217-
if (currentValue instanceof ActionListener<?> listener) {
220+
if (currentState instanceof ActionListener<?> listener) {
218221
// unique subscriber - complete it
219222
boolean completed = tryComplete(result, listener);
220223
assert completed;
221-
} else if (currentValue instanceof Cell currCell) {
224+
} else if (currentState instanceof Cell currCell) {
222225
// multiple subscribers, but they are currently in reverse order of subscription so reverse them back
223226
Cell prevCell = null;
224227
while (true) {
@@ -237,18 +240,18 @@ private void setResult(Object result) {
237240
currCell = currCell.next;
238241
}
239242
} else {
240-
assert currentValue == EMPTY : "unexpected witness: " + currentValue;
243+
assert currentState == EMPTY : "unexpected witness: " + currentState;
241244
}
242245
return;
243246
}
244247

245248
// we lost a race with another setResult or addListener call - retry
246-
currentValue = witness;
249+
currentState = witness;
247250
}
248251
}
249252

250-
private static boolean isDone(Object refValue) {
251-
return refValue instanceof SubscribableListener.SuccessResult<?> || refValue instanceof SubscribableListener.FailureResult;
253+
private static boolean isDone(Object currentState) {
254+
return currentState instanceof SuccessResult<?> || currentState instanceof FailureResult;
252255
}
253256

254257
/**
@@ -322,4 +325,20 @@ private Runnable scheduleTimeout(TimeValue timeout, ThreadPool threadPool, Strin
322325
return () -> {};
323326
}
324327
}
328+
329+
private static final VarHandle VH_STATE_FIELD;
330+
331+
static {
332+
try {
333+
VH_STATE_FIELD = MethodHandles.lookup()
334+
.in(SubscribableListener.class)
335+
.findVarHandle(SubscribableListener.class, "state", Object.class);
336+
} catch (NoSuchFieldException | IllegalAccessException e) {
337+
throw new RuntimeException(e);
338+
}
339+
}
340+
341+
private Object compareAndExchangeState(Object expectedValue, Object newValue) {
342+
return VH_STATE_FIELD.compareAndExchange(this, expectedValue, newValue);
343+
}
325344
}

server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.apache.lucene.util.StringHelper;
1717
import org.elasticsearch.ElasticsearchException;
1818
import org.elasticsearch.Version;
19+
import org.elasticsearch.action.support.SubscribableListener;
1920
import org.elasticsearch.common.ReferenceDocs;
2021
import org.elasticsearch.common.filesystem.FileSystemNatives;
2122
import org.elasticsearch.common.io.stream.InputStreamStreamInput;
@@ -179,14 +180,13 @@ private static void initPhase2(Bootstrap bootstrap) throws IOException {
179180
// Log ifconfig output before SecurityManager is installed
180181
IfConfig.logIfNecessary();
181182

182-
try {
183+
ensureInitialized(
183184
// ReferenceDocs class does nontrivial static initialization which should always succeed but load it now (before SM) to be sure
184-
MethodHandles.publicLookup().ensureInitialized(ReferenceDocs.class);
185-
// AbstractRefCounted class uses MethodHandles.lookup during initialization, load it now (before SM) to be sure it succeeds
186-
MethodHandles.publicLookup().ensureInitialized(AbstractRefCounted.class);
187-
} catch (IllegalAccessException unexpected) {
188-
throw new AssertionError(unexpected);
189-
}
185+
ReferenceDocs.class,
186+
// The following classes use MethodHandles.lookup during initialization, load them now (before SM) to be sure they succeed
187+
AbstractRefCounted.class,
188+
SubscribableListener.class
189+
);
190190

191191
// install SM after natives, shutdown hooks, etc.
192192
org.elasticsearch.bootstrap.Security.configure(
@@ -196,6 +196,16 @@ private static void initPhase2(Bootstrap bootstrap) throws IOException {
196196
);
197197
}
198198

199+
private static void ensureInitialized(Class<?>... classes) {
200+
for (final var clazz : classes) {
201+
try {
202+
MethodHandles.publicLookup().ensureInitialized(clazz);
203+
} catch (IllegalAccessException unexpected) {
204+
throw new AssertionError(unexpected);
205+
}
206+
}
207+
}
208+
199209
/**
200210
* Third phase of initialization.
201211
*

0 commit comments

Comments
 (0)