|
15 | 15 | import static org.junit.jupiter.api.Assertions.assertFalse; |
16 | 16 | import static org.junit.jupiter.api.Assertions.assertTrue; |
17 | 17 |
|
| 18 | +import java.lang.ref.WeakReference; |
18 | 19 | import java.time.Duration; |
19 | 20 | import java.util.Collections; |
| 21 | +import java.util.concurrent.ExecutorService; |
| 22 | +import java.util.concurrent.Executors; |
20 | 23 | import java.util.concurrent.atomic.AtomicBoolean; |
21 | 24 | import java.util.concurrent.atomic.AtomicReference; |
22 | 25 | import org.apache.storm.metrics2.StormMetricRegistry; |
@@ -230,6 +233,46 @@ public void testDynamicBatchGrowsNotShrinksUnderBackpressure() throws Exception |
230 | 233 | assertEquals(3, inserter.batchSize()); |
231 | 234 | } |
232 | 235 |
|
| 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 | + |
233 | 276 | /** Drive the inserter with full flushes until the effective batch size reaches the target. */ |
234 | 277 | private void growEffectiveTo(JCQueue.DynamicBatchInserter inserter, int target) throws InterruptedException { |
235 | 278 | long val = 0; |
|
0 commit comments