From fcfe9080b2bd3996b426269237cc573acf885dcb Mon Sep 17 00:00:00 2001 From: LordKay-sudo Date: Mon, 29 Jun 2026 16:08:42 +0200 Subject: [PATCH] Initialize PostgreSQL SharedTimer on JUnit launcher thread Prevent the PostgreSQL JDBC driver timer thread from inheriting Spring TransactionContextHolder from transactional test threads, which can pin ApplicationContext instances for the JVM lifetime (gh-36737). Related to gh-36737 Signed-off-by: LordKay-sudo --- ...sqlSharedTimerLauncherSessionListener.java | 70 +++++++++++++++++++ ....platform.launcher.LauncherSessionListener | 1 + ...aredTimerLauncherSessionListenerTests.java | 36 ++++++++++ 3 files changed, 107 insertions(+) create mode 100644 spring-test/src/main/java/org/springframework/test/context/jdbc/PostgresqlSharedTimerLauncherSessionListener.java create mode 100644 spring-test/src/main/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener create mode 100644 spring-test/src/test/java/org/springframework/test/context/jdbc/PostgresqlSharedTimerLauncherSessionListenerTests.java diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/PostgresqlSharedTimerLauncherSessionListener.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/PostgresqlSharedTimerLauncherSessionListener.java new file mode 100644 index 000000000000..f5f934fe413f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/PostgresqlSharedTimerLauncherSessionListener.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.jdbc; + +import java.lang.reflect.Method; + +import org.jspecify.annotations.Nullable; + +import org.junit.platform.launcher.LauncherSession; +import org.junit.platform.launcher.LauncherSessionListener; + +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link LauncherSessionListener} that eagerly initializes the PostgreSQL JDBC driver's + * shared {@link java.util.Timer} on the JUnit launcher thread. + *

This prevents the driver's {@code TimerThread} from inheriting Spring's + * {@code TransactionContextHolder} from a transactional test thread, which would otherwise + * pin an {@code ApplicationContext} for the lifetime of the JVM. + * + * @since 7.0.x + * @see gh-36737 + */ +public final class PostgresqlSharedTimerLauncherSessionListener implements LauncherSessionListener { + + private static final String POSTGRESQL_DRIVER_CLASS_NAME = "org.postgresql.Driver"; + + + @Override + public void launcherSessionOpened(LauncherSession session) { + initializeSharedTimer(); + } + + private static void initializeSharedTimer() { + try { + Class driverClass = ClassUtils.forName(POSTGRESQL_DRIVER_CLASS_NAME, null); + Method getSharedTimer = ClassUtils.getMethodIfAvailable(driverClass, "getSharedTimer"); + if (getSharedTimer == null) { + return; + } + @Nullable Object sharedTimer = ReflectionUtils.invokeMethod(getSharedTimer, null); + if (sharedTimer == null) { + return; + } + Method getTimer = ClassUtils.getMethodIfAvailable(sharedTimer.getClass(), "getTimer"); + if (getTimer != null) { + ReflectionUtils.invokeMethod(getTimer, sharedTimer); + } + } + catch (ClassNotFoundException | LinkageError ex) { + // PostgreSQL JDBC driver not present + } + } + +} diff --git a/spring-test/src/main/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener b/spring-test/src/main/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener new file mode 100644 index 000000000000..a3be9a4ca7a6 --- /dev/null +++ b/spring-test/src/main/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener @@ -0,0 +1 @@ +org.springframework.test.context.jdbc.PostgresqlSharedTimerLauncherSessionListener diff --git a/spring-test/src/test/java/org/springframework/test/context/jdbc/PostgresqlSharedTimerLauncherSessionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/jdbc/PostgresqlSharedTimerLauncherSessionListenerTests.java new file mode 100644 index 000000000000..e0ef27f1f64e --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/jdbc/PostgresqlSharedTimerLauncherSessionListenerTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.jdbc; + +import org.junit.jupiter.api.Test; +import org.junit.platform.launcher.LauncherSession; + +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PostgresqlSharedTimerLauncherSessionListener}. + */ +class PostgresqlSharedTimerLauncherSessionListenerTests { + + @Test + void launcherSessionOpenedDoesNotThrowWhenPostgresqlDriverIsAbsent() { + PostgresqlSharedTimerLauncherSessionListener listener = + new PostgresqlSharedTimerLauncherSessionListener(); + listener.launcherSessionOpened(mock(LauncherSession.class)); + } + +}