postgresql-jdbc.git / summary / log / commit / refs
commit 301cfb7a5a1950eb9d545dcb4b8d13162bd4e855
Author: Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
Date: Sun May 31 18:22:04 2026 +0000
test: fix LazyCleanerTest timeouts for the lingering Java 8 cleanup thread
LazyCleanerImpl now keeps its Java 8 cleanup thread alive for up to one
threadTtl after the reference queue empties (05166d0fd, #4037), so the
worst case to observe !isThreadRunning() is roughly 2 x threadTtl. The
tests used threadTtl = 5s with an Await budget of exactly 10s = 2 x ttl
and no slack, so under -XX:ActiveProcessorCount=1 (e06c64995) the extra
GC, reference-handler, and ForkJoinPool scheduling latency pushed them
past the deadline:
cleanupCompletesAfterManualClean - waited for !isThreadRunning()
exceptionsDuringCleanupAreHandled - waited for both cleanups to run
Derive the thread-stop budget from threadTtl (2 x ttl + 5s) instead of
hard-coding it, and raise the cleanup-completion budget to 30s since it
is bound by GC/enqueue latency rather than ttl. Production thread TTL is
unchanged; the tests keep the 5s value.
Branch the thread-lifecycle assertions on the observed behaviour rather
than on the runtime Java version. The active LazyCleanerImpl is chosen by
multi-release packaging, not by the JDK: the Maven source-distribution
build runs the PhantomReference-based Java 8 variant on any JDK, so a
version-based "Java 11+ has no thread" assertion fails there. Detect the
variant once via isThreadRunning() after register(): the dedicated-thread
variant must stop after the queue empties, the native-Cleaner variant
never starts a thread.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
.../java/org/postgresql/util/LazyCleanerTest.java | 77 +++++++++++++++++-----
1 file changed, 59 insertions(+), 18 deletions(-)
diff --git a/pgjdbc/src/test/java/org/postgresql/util/LazyCleanerTest.java b/pgjdbc/src/test/java/org/postgresql/util/LazyCleanerTest.java
index 9ba18506..c1f1a7a7 100644
--- a/pgjdbc/src/test/java/org/postgresql/util/LazyCleanerTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/util/LazyCleanerTest.java
@@ -24,6 +24,7 @@ package org.postgresql.util;
import static java.time.Duration.ofSeconds;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -33,6 +34,7 @@ import org.postgresql.test.annotations.DisableLogger;
import org.junit.jupiter.api.Test;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -46,7 +48,8 @@ public class LazyCleanerTest {
List<Object> list = new ArrayList<>(Arrays.asList(
new Object(), new Object(), new Object()));
- LazyCleanerImpl t = new LazyCleanerImpl("Cleaner", ofSeconds(5));
+ Duration ttl = ofSeconds(5);
+ LazyCleanerImpl t = new LazyCleanerImpl("Cleaner", ttl);
String[] collected = new String[list.size()];
List<LazyCleaner.Cleanable<RuntimeException>> cleaners = new ArrayList<>();
@@ -67,9 +70,13 @@ public class LazyCleanerTest {
);
}
+ // The active LazyCleanerImpl is selected by multi-release packaging, not by the runtime JDK
+ // (the Maven source-distribution build runs the Java 8 variant on any JDK), so branch on the
+ // observed behaviour rather than on JavaVersion.
+ boolean dedicatedThread = t.isThreadRunning();
if (JavaVersion.getRuntimeVersion() == JavaVersion.v1_8) {
- assertTrue(t.isThreadRunning(),
- "cleanup thread should be running, and it should wait for the leaks");
+ assertTrue(dedicatedThread,
+ "Java 8 always loads the PhantomReference-based cleaner, which runs a dedicated thread");
}
cleaners.get(1).clean();
@@ -82,14 +89,14 @@ public class LazyCleanerTest {
System.gc();
System.gc();
- if (JavaVersion.getRuntimeVersion() == JavaVersion.v1_8) {
+ if (dedicatedThread) {
Await.until(
- "The cleanup thread should detect leaks and terminate within 5-10 seconds after GC",
- ofSeconds(10),
+ "The cleanup thread should detect leaks and terminate after GC",
+ threadStopBudget(ttl),
() -> !t.isThreadRunning()
);
} else {
- // Allow some room for Java's Cleaner to clean the refs
+ // The native Cleaner has no dedicated thread to stop; give it room to process the refs
Thread.sleep(1000);
}
@@ -103,7 +110,8 @@ public class LazyCleanerTest {
@Test
void cleanupCompletesAfterManualClean() throws InterruptedException {
String threadName = UUID.randomUUID().toString();
- LazyCleanerImpl t = new LazyCleanerImpl(threadName, ofSeconds(5));
+ Duration ttl = ofSeconds(5);
+ LazyCleanerImpl t = new LazyCleanerImpl(threadName, ttl);
AtomicBoolean cleaned = new AtomicBoolean();
List<Object> list = new ArrayList<>();
@@ -115,9 +123,13 @@ public class LazyCleanerTest {
leak -> cleaned.set(true)
);
+ // The active LazyCleanerImpl is selected by multi-release packaging, not by the runtime JDK
+ // (the Maven source-distribution build runs the Java 8 variant on any JDK), so branch on the
+ // observed behaviour rather than on JavaVersion.
+ boolean dedicatedThread = t.isThreadRunning();
if (JavaVersion.getRuntimeVersion() == JavaVersion.v1_8) {
- assertTrue(t.isThreadRunning(),
- "cleanup thread should be running when there are objects to monitor");
+ assertTrue(dedicatedThread,
+ "Java 8 always loads the PhantomReference-based cleaner, which runs a dedicated thread");
}
// Manually clean the object
@@ -131,19 +143,23 @@ public class LazyCleanerTest {
System.gc();
System.gc();
- if (JavaVersion.getRuntimeVersion() == JavaVersion.v1_8) {
+ if (dedicatedThread) {
Await.until(
"Cleanup thread should stop when no objects remain",
- ofSeconds(10),
+ threadStopBudget(ttl),
() -> !t.isThreadRunning()
);
+ } else {
+ assertFalse(t.isThreadRunning(),
+ "Native Cleaner variant: there is no dedicated thread to stop");
}
}
@Test
@DisableLogger(LazyCleanerImpl.class)
void exceptionsDuringCleanupAreHandled() throws InterruptedException {
- LazyCleanerImpl t = new LazyCleanerImpl("test-cleaner", ofSeconds(5));
+ Duration ttl = ofSeconds(5);
+ LazyCleanerImpl t = new LazyCleanerImpl("test-cleaner", ttl);
java.util.concurrent.atomic.AtomicInteger cleanupCount = new java.util.concurrent.atomic.AtomicInteger(0);
List<Object> list = new ArrayList<>();
@@ -166,9 +182,13 @@ public class LazyCleanerTest {
leak -> secondCleaned.set(true)
);
+ // The active LazyCleanerImpl is selected by multi-release packaging, not by the runtime JDK
+ // (the Maven source-distribution build runs the Java 8 variant on any JDK), so branch on the
+ // observed behaviour rather than on JavaVersion.
+ boolean dedicatedThread = t.isThreadRunning();
if (JavaVersion.getRuntimeVersion() == JavaVersion.v1_8) {
- assertTrue(t.isThreadRunning(),
- "cleanup thread should be running when there are objects to monitor");
+ assertTrue(dedicatedThread,
+ "Java 8 always loads the PhantomReference-based cleaner, which runs a dedicated thread");
}
// Trigger cleanup
@@ -176,18 +196,25 @@ public class LazyCleanerTest {
System.gc();
System.gc();
+ // Cleanup completion is bound by GC and reference-enqueue latency, not by threadTtl. Allow a
+ // generous budget so the assertion holds even when the JVM runs on a single CPU
+ // (-XX:ActiveProcessorCount=1), where GC, the reference handler, and the ForkJoinPool worker
+ // all share one core.
Await.until(
"Both cleanups should complete despite exception",
- ofSeconds(10),
+ ofSeconds(30),
() -> cleanupCount.get() == 1 && secondCleaned.get()
);
- if (JavaVersion.getRuntimeVersion() == JavaVersion.v1_8) {
+ if (dedicatedThread) {
Await.until(
"Cleanup thread should stop after all objects are cleaned",
- ofSeconds(10),
+ threadStopBudget(ttl),
() -> !t.isThreadRunning()
);
+ } else {
+ assertFalse(t.isThreadRunning(),
+ "Native Cleaner variant: there is no dedicated thread to stop");
}
}
@@ -204,4 +231,18 @@ public class LazyCleanerTest {
RuntimeException thrownException = assertThrows(RuntimeException.class, cleanable::clean);
assertSame(expectedException, thrownException, "Expected same exception instance to be thrown");
}
+
+ /**
+ * Worst-case time for the Java 8 cleanup thread to stop once its registry empties. The thread
+ * waits up to one {@code threadTtl} window to observe the final GC'd reference, then must time
+ * out a second full {@code threadTtl} window before the loop breaks (see {@code LazyCleanerImpl}).
+ * The extra seconds absorb scheduling slack when the JVM runs on a single CPU
+ * (-XX:ActiveProcessorCount=1).
+ *
+ * @param threadTtl the time-to-live configured for the cleaner
+ * @return the await budget to use for {@code !isThreadRunning()}
+ */
+ private static Duration threadStopBudget(Duration threadTtl) {
+ return threadTtl.multipliedBy(2).plus(ofSeconds(5));
+ }
}
[parent: 472632250c06]