Skip to content

Commit f72be1e

Browse files
authored
Merge pull request #8812 from apache/jcqueue-weak-ref-batcher-leak
utils: use WeakReference in BatchInserter to fix ThreadLocal retention of JCQueue
2 parents a18676b + 6413f22 commit f72be1e

2 files changed

Lines changed: 65 additions & 2 deletions

File tree

storm-client/src/jvm/org/apache/storm/utils/JCQueue.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package org.apache.storm.utils;
2020

2121
import java.io.Closeable;
22+
import java.lang.ref.WeakReference;
2223
import java.util.ArrayList;
2324
import java.util.List;
2425
import org.apache.storm.metrics2.StormMetricRegistry;
@@ -345,11 +346,16 @@ public boolean tryFlush() {
345346
/* Not thread safe. Have one instance per producer thread or synchronize externally */
346347
private static class BatchInserter implements Inserter {
347348
private final int batchSz;
348-
private final JCQueue queue;
349+
// WeakReference breaks the ThreadLocal retention cycle: thdLocalBatcher is an instance field
350+
// of JCQueue, so the ThreadLocalMap key (the ThreadLocal object) is kept strongly reachable
351+
// via value(BatchInserter) -> queue(JCQueue) -> field. A WeakReference here cuts that path,
352+
// allowing the key to become weakly-reachable and the entry to be expunged once the JCQueue
353+
// is no longer externally referenced.
354+
private final WeakReference<JCQueue> queueRef;
349355
private final ArrayList<Object> currentBatch;
350356

351357
BatchInserter(JCQueue queue, int batchSz) {
352-
this.queue = queue;
358+
this.queueRef = new WeakReference<>(queue);
353359
this.batchSz = batchSz;
354360
this.currentBatch = new ArrayList<>(batchSz + 1);
355361
}
@@ -402,6 +408,13 @@ public void flush() throws InterruptedException {
402408
if (currentBatch.isEmpty()) {
403409
return;
404410
}
411+
JCQueue queue = queueRef.get();
412+
if (queue == null) {
413+
// The JCQueue was GC'd (topology stopped on a long-lived thread, e.g. LocalCluster).
414+
// Nothing to flush; discard the buffered batch and return cleanly.
415+
currentBatch.clear();
416+
return;
417+
}
405418
boolean wasFull = currentBatch.size() >= batchSize();
406419
int publishCount = queue.tryPublishInternal(currentBatch);
407420
int retryCount = 0;
@@ -432,6 +445,13 @@ public boolean tryFlush() {
432445
if (currentBatch.isEmpty()) {
433446
return true;
434447
}
448+
JCQueue queue = queueRef.get();
449+
if (queue == null) {
450+
// The JCQueue was GC'd (topology stopped on a long-lived thread, e.g. LocalCluster).
451+
// Nothing to flush; discard the buffered batch and report success.
452+
currentBatch.clear();
453+
return true;
454+
}
435455
boolean wasFull = currentBatch.size() >= batchSize();
436456
int publishCount = queue.tryPublishInternal(currentBatch);
437457
if (publishCount == 0) {

storm-client/test/jvm/org/apache/storm/utils/JCQueueTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
import static org.junit.jupiter.api.Assertions.assertFalse;
1616
import static org.junit.jupiter.api.Assertions.assertTrue;
1717

18+
import java.lang.ref.WeakReference;
1819
import java.time.Duration;
1920
import java.util.Collections;
21+
import java.util.concurrent.ExecutorService;
22+
import java.util.concurrent.Executors;
2023
import java.util.concurrent.atomic.AtomicBoolean;
2124
import java.util.concurrent.atomic.AtomicReference;
2225
import org.apache.storm.metrics2.StormMetricRegistry;
@@ -230,6 +233,46 @@ public void testDynamicBatchGrowsNotShrinksUnderBackpressure() throws Exception
230233
assertEquals(3, inserter.batchSize());
231234
}
232235

236+
@Test
237+
public void testQueueIsCollectedAfterLongLivedProducerPublishes() {
238+
Assertions.assertTimeoutPreemptively(Duration.ofSeconds(30), () -> {
239+
ExecutorService producerPool = Executors.newSingleThreadExecutor();
240+
try {
241+
WeakReference<JCQueue> ref = publishFromLongLivedThread(producerPool);
242+
assertTrue(awaitGarbageCollection(ref),
243+
"JCQueue was not garbage collected — BatchInserter may still hold a strong reference to it");
244+
} finally {
245+
producerPool.shutdownNow();
246+
}
247+
});
248+
}
249+
250+
// Publishes a few tuples from a pooled thread so that the BatchInserter's ThreadLocal entry is
251+
// created on that thread. Once .get() returns the lambda is done and its closure ref is gone;
252+
// when the method returns the local `queue` variable goes out of scope. The only remaining
253+
// reference is the WeakReference — if it is not cleared after GC, the ThreadLocal cycle is still live.
254+
private WeakReference<JCQueue> publishFromLongLivedThread(ExecutorService pool) throws Exception {
255+
JCQueue queue = createQueue("leak", 100, 1024);
256+
pool.submit(() -> {
257+
try {
258+
for (long i = 0; i < 10; i++) {
259+
queue.publish(i);
260+
}
261+
} catch (InterruptedException e) {
262+
Thread.currentThread().interrupt();
263+
}
264+
}).get();
265+
return new WeakReference<>(queue);
266+
}
267+
268+
private boolean awaitGarbageCollection(WeakReference<JCQueue> ref) throws InterruptedException {
269+
for (int i = 0; i < 50 && ref.get() != null; i++) {
270+
System.gc();
271+
Thread.sleep(100);
272+
}
273+
return ref.get() == null;
274+
}
275+
233276
/** Drive the inserter with full flushes until the effective batch size reaches the target. */
234277
private void growEffectiveTo(JCQueue.DynamicBatchInserter inserter, int target) throws InterruptedException {
235278
long val = 0;

0 commit comments

Comments
 (0)