-
Notifications
You must be signed in to change notification settings - Fork 25k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ensure one-shot wrappers release their delegates #92928
Changes from 8 commits
1e05160
2ab6ce5
13884c9
798e75f
12f285a
74627d6
a045b8a
0153d53
4747420
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
pr: 92928 | ||
summary: Ensure one-shot wrappers release their delegates | ||
area: Infra/Core | ||
type: bug | ||
issues: [] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,9 +7,11 @@ | |
*/ | ||
package org.elasticsearch.action.support; | ||
|
||
import org.elasticsearch.ElasticsearchException; | ||
import org.elasticsearch.action.ActionListener; | ||
import org.elasticsearch.common.util.concurrent.AbstractRunnable; | ||
import org.elasticsearch.test.ESTestCase; | ||
import org.elasticsearch.test.ReachabilityChecker; | ||
import org.elasticsearch.threadpool.TestThreadPool; | ||
import org.elasticsearch.threadpool.ThreadPool; | ||
import org.elasticsearch.transport.Transports; | ||
|
@@ -159,4 +161,23 @@ private static void awaitSafe(CyclicBarrier barrier) { | |
throw new AssertionError("unexpected", e); | ||
} | ||
} | ||
|
||
public void testAddedListenersReleasedOnCompletion() { | ||
final ListenableActionFuture<Void> future = new ListenableActionFuture<>(); | ||
final ReachabilityChecker reachabilityChecker = new ReachabilityChecker(); | ||
|
||
for (int i = between(1, 3); i > 0; i--) { | ||
future.addListener(reachabilityChecker.register(ActionListener.wrap(() -> {}))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above, moving as much allocation as we can ahead of time to make sure we don't hit a GC. Perhaps the most prudent thing to do in all these tests is call System.gc() before we go into any of the test code. With that we ensure that at least the new space in generational collectors (e.g. G1) will be collected. |
||
} | ||
reachabilityChecker.checkReachable(); | ||
if (randomBoolean()) { | ||
future.onResponse(null); | ||
} else { | ||
future.onFailure(new ElasticsearchException("simulated")); | ||
} | ||
reachabilityChecker.ensureUnreachable(); | ||
|
||
future.addListener(reachabilityChecker.register(ActionListener.wrap(() -> {}))); | ||
reachabilityChecker.ensureUnreachable(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
package org.elasticsearch.test; | ||
|
||
import org.elasticsearch.common.Randomness; | ||
import org.elasticsearch.common.util.concurrent.ConcurrentCollections; | ||
|
||
import java.lang.management.ManagementFactory; | ||
import java.lang.management.MemoryMXBean; | ||
import java.lang.ref.PhantomReference; | ||
import java.lang.ref.ReferenceQueue; | ||
import java.util.Objects; | ||
import java.util.Queue; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import static org.junit.Assert.assertNotNull; | ||
import static org.junit.Assert.assertNull; | ||
import static org.junit.Assert.assertTrue; | ||
|
||
/** | ||
* Utility class for checking that objects become unreachable when expected. | ||
*/ | ||
public class ReachabilityChecker { | ||
|
||
private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); | ||
private final Queue<Registered> references = ConcurrentCollections.newQueue(); | ||
|
||
public ReachabilityChecker() { | ||
memoryMXBean.gc(); | ||
} | ||
|
||
/** | ||
* Register the given target object for reachability checks. | ||
* | ||
* @return the given target object. | ||
*/ | ||
public <T> T register(T target) { | ||
var referenceQueue = new ReferenceQueue<>(); | ||
references.add( | ||
new Registered(target.toString(), new PhantomReference<>(Objects.requireNonNull(target), referenceQueue), referenceQueue) | ||
); | ||
return target; | ||
} | ||
|
||
/** | ||
* Ensure that all registered objects have become unreachable. | ||
*/ | ||
public void ensureUnreachable() { | ||
ensureUnreachable(TimeUnit.SECONDS.toMillis(10)); | ||
} | ||
|
||
void ensureUnreachable(long timeoutMillis) { | ||
Registered registered; | ||
while ((registered = references.poll()) != null) { | ||
registered.assertReferenceEnqueuedForCollection(memoryMXBean, timeoutMillis); | ||
} | ||
} | ||
|
||
/** | ||
* From the objects registered since the most recent call to {@link #ensureUnreachable()} (or since the construction of this {@link | ||
* ReachabilityChecker} if {@link #ensureUnreachable()} has not been called) this method chooses one at random and verifies that it has | ||
* not yet become unreachable. | ||
*/ | ||
public void checkReachable() { | ||
if (references.peek() == null) { | ||
throw new AssertionError("no references registered"); | ||
} | ||
|
||
var target = Randomness.get().nextInt(references.size()); | ||
var iterator = references.iterator(); | ||
for (int i = 0; i < target; i++) { | ||
assertTrue(iterator.hasNext()); | ||
assertNotNull(iterator.next()); | ||
} | ||
|
||
assertTrue(iterator.hasNext()); | ||
iterator.next().assertReferenceNotEnqueuedForCollection(memoryMXBean); | ||
} | ||
|
||
private static final class Registered { | ||
private final String description; | ||
private final PhantomReference<?> phantomReference; | ||
private final ReferenceQueue<?> referenceQueue; | ||
|
||
Registered(String description, PhantomReference<?> phantomReference, ReferenceQueue<?> referenceQueue) { | ||
this.description = description; | ||
this.phantomReference = phantomReference; | ||
this.referenceQueue = referenceQueue; | ||
} | ||
|
||
/** | ||
* Attempts to trigger the GC repeatedly until the {@link ReferenceQueue} yields a reference. | ||
*/ | ||
public void assertReferenceEnqueuedForCollection(MemoryMXBean memoryMXBean, long timeoutMillis) { | ||
try { | ||
final var timeoutAt = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis); | ||
while (true) { | ||
memoryMXBean.gc(); | ||
final var ref = referenceQueue.remove(500); | ||
if (ref != null) { | ||
ref.clear(); | ||
return; | ||
} | ||
assertTrue("still reachable: " + description, System.nanoTime() < timeoutAt); | ||
assertNull(phantomReference.get()); // always succeeds, we're just doing this to use the phantomReference for something | ||
} | ||
} catch (Exception e) { | ||
throw new AssertionError("unexpected", e); | ||
} | ||
} | ||
|
||
/** | ||
* Attempts to trigger the GC and verifies that the {@link ReferenceQueue} does not yield a reference. | ||
*/ | ||
public void assertReferenceNotEnqueuedForCollection(MemoryMXBean memoryMXBean) { | ||
try { | ||
memoryMXBean.gc(); | ||
assertNull("became unreachable: " + description, referenceQueue.remove(100)); | ||
} catch (Exception e) { | ||
throw new AssertionError("unexpected", e); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a possibility to extract the allocations of the ActionListeners into an array before we register them? That way we'll minimize the allocations between register calls and ensure that when we call GC we'll get our references cleaned up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it needs that much ceremony to be stable then this is not going to work in general. I wonder, would it work to do the same thing that the
G1OverLimitStrategy
does?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think if we can somehow reuse that, we are definitely more certain the references will be properly checked for liveness. I also think the 3 action listeners are not a lot of garbage, it won't be a problem if we called System.gc() on creation of the ReachabilityChecker. It's fine as-is with the GC-up-front change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I added a short comment to the class indicating that there is some possible flakiness here. We can revisit this if it turns out to be bad.