diff --git a/blackbox-tests/pom.xml b/blackbox-tests/pom.xml new file mode 100644 index 0000000..697a332 --- /dev/null +++ b/blackbox-tests/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + io.ebean + ebean-datasource-parent + 9.6 + + + blackbox-tests + + + 21 + + + + + io.ebean + ebean-datasource + 9.6 + + + + io.avaje + junit + 1.5 + test + + + + io.ebean + ebean-test-containers + 7.8 + test + + + + org.postgresql + postgresql + 42.7.2 + test + + + + ch.qos.logback + logback-classic + 1.5.17 + test + + + + org.slf4j + slf4j-jdk-platform-logging + 2.0.17 + test + + + + io.avaje + avaje-slf4j-jpl + 1.2 + test + + + + diff --git a/blackbox-tests/src/test/java/org/example/tests/Java21TrimAndShutdownTest.java b/blackbox-tests/src/test/java/org/example/tests/Java21TrimAndShutdownTest.java new file mode 100644 index 0000000..35e2422 --- /dev/null +++ b/blackbox-tests/src/test/java/org/example/tests/Java21TrimAndShutdownTest.java @@ -0,0 +1,64 @@ +package org.example.tests; + +import io.ebean.datasource.DataSourceBuilder; +import io.ebean.datasource.DataSourcePool; +import io.ebean.test.containers.PostgresContainer; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +class Java21TrimAndShutdownTest { + + private static Logger log = LoggerFactory.getLogger(Java21TrimAndShutdownTest.class); + + @BeforeAll + static void before() { + PostgresContainer.builder("15") + .port(9999) + .containerName("pool_test") + .dbName("app") + .user("db_owner") + .build() + .startWithDropCreate(); + } + + @Test + void test() throws InterruptedException, SQLException { + Properties clientInfo = new Properties(); + clientInfo.setProperty("ApplicationName", "my-test"); + + DataSourcePool pool = DataSourceBuilder.create() + .url("jdbc:postgresql://127.0.0.1:9999/app") + .username("db_owner") + .password("test") + .clientInfo(clientInfo) + .maxInactiveTimeSecs(2) + .heartbeatFreqSecs(1) + .trimPoolFreqSecs(1) + .build(); + + List connectionList = new ArrayList<>(); + for (int i = 0; i < 50; i++) { + connectionList.add(pool.getConnection()); + } + + // close them slowly to allow multiple trims + for (Connection connection : connectionList) { + connection.rollback(); + connection.close(); + Thread.sleep(200); + } + + log.info("----------- Sleep allowing trim -------------"); + Thread.sleep(9_000); + log.info("----------- Shutdown pool -------------"); + pool.shutdown(); + } +} diff --git a/blackbox-tests/src/test/resources/logback-test.xml b/blackbox-tests/src/test/resources/logback-test.xml new file mode 100644 index 0000000..16c6e08 --- /dev/null +++ b/blackbox-tests/src/test/resources/logback-test.xml @@ -0,0 +1,19 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + diff --git a/ebean-datasource/src/main/java/io/ebean/datasource/pool/ConnectionPool.java b/ebean-datasource/src/main/java/io/ebean/datasource/pool/ConnectionPool.java index 80a4647..64a7228 100644 --- a/ebean-datasource/src/main/java/io/ebean/datasource/pool/ConnectionPool.java +++ b/ebean-datasource/src/main/java/io/ebean/datasource/pool/ConnectionPool.java @@ -26,6 +26,12 @@ */ final class ConnectionPool implements DataSourcePool { + @FunctionalInterface + interface Heartbeat { + + void stop(); + } + private static final String APPLICATION_NAME = "ApplicationName"; private final ReentrantLock heartbeatLock = new ReentrantLock(false); private final ReentrantLock notifyLock = new ReentrantLock(false); @@ -80,7 +86,7 @@ final class ConnectionPool implements DataSourcePool { private final int waitTimeoutMillis; private final int pstmtCacheSize; private final PooledConnectionQueue queue; - private Timer heartBeatTimer; + private Heartbeat heartbeat; private int heartbeatPoolExhaustedCount; private final ExecutorService executor; @@ -161,13 +167,6 @@ void pstmtCacheMetrics(PstmtCache pstmtCache) { pscRem.add(pstmtCache.removeCount()); } - final class HeartBeatRunnable extends TimerTask { - @Override - public void run() { - heartBeat(); - } - } - @Override public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException { throw new SQLFeatureNotSupportedException("We do not support java.util.logging"); @@ -387,7 +386,7 @@ private void trimIdleConnections() { * This is called by the HeartbeatRunnable which should be scheduled to * run periodically (every heartbeatFreqSecs seconds). */ - private void heartBeat() { + void heartbeat() { trimIdleConnections(); if (validateOnHeartbeat) { testConnection(); @@ -727,11 +726,10 @@ private void startHeartBeatIfStopped() { heartbeatLock.lock(); try { // only start if it is not already running - if (heartBeatTimer == null) { + if (heartbeat == null) { int freqMillis = heartbeatFreqSecs * 1000; if (freqMillis > 0) { - heartBeatTimer = new Timer(name + ".heartBeat", true); - heartBeatTimer.scheduleAtFixedRate(new HeartBeatRunnable(), freqMillis, freqMillis); + heartbeat = ExecutorFactory.newHeartBeat(this, freqMillis); } } } finally { @@ -743,9 +741,9 @@ private void stopHeartBeatIfRunning() { heartbeatLock.lock(); try { // only stop if it was running - if (heartBeatTimer != null) { - heartBeatTimer.cancel(); - heartBeatTimer = null; + if (heartbeat != null) { + heartbeat.stop(); + heartbeat = null; } } finally { heartbeatLock.unlock(); diff --git a/ebean-datasource/src/main/java/io/ebean/datasource/pool/ConnectionPoolFactory.java b/ebean-datasource/src/main/java/io/ebean/datasource/pool/ConnectionPoolFactory.java index c805171..09a2985 100644 --- a/ebean-datasource/src/main/java/io/ebean/datasource/pool/ConnectionPoolFactory.java +++ b/ebean-datasource/src/main/java/io/ebean/datasource/pool/ConnectionPoolFactory.java @@ -4,6 +4,8 @@ import io.ebean.datasource.DataSourceFactory; import io.ebean.datasource.DataSourcePool; +import static java.util.Objects.requireNonNullElse; + /** * DataSourceFactory implementation that is service loaded. */ @@ -11,6 +13,6 @@ public final class ConnectionPoolFactory implements DataSourceFactory { @Override public DataSourcePool createPool(String name, DataSourceConfig config) { - return new ConnectionPool(name, config); + return new ConnectionPool(requireNonNullElse(name, ""), config); } } diff --git a/ebean-datasource/src/main/java/io/ebean/datasource/pool/ExecutorFactory.java b/ebean-datasource/src/main/java/io/ebean/datasource/pool/ExecutorFactory.java index 8e05fc8..6cd6f1e 100644 --- a/ebean-datasource/src/main/java/io/ebean/datasource/pool/ExecutorFactory.java +++ b/ebean-datasource/src/main/java/io/ebean/datasource/pool/ExecutorFactory.java @@ -1,11 +1,65 @@ package io.ebean.datasource.pool; +import io.ebean.datasource.pool.ConnectionPool.Heartbeat; + +import java.util.Timer; +import java.util.TimerTask; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; final class ExecutorFactory { static ExecutorService newExecutor() { - return Executors.newSingleThreadExecutor(); + return Executors.newSingleThreadExecutor(factory()); + } + + private static ThreadFactory factory() { + return runnable -> { + Thread thread = new Thread(runnable); + thread.setName("datasource.reaper"); + return thread; + }; + } + + /** + * Return a new Heartbeat for the pool. + */ + static Heartbeat newHeartBeat(ConnectionPool pool, int freqMillis) { + final Timer timer = new Timer(nm(pool.name()), true); + timer.scheduleAtFixedRate(new HeartbeatTask(pool), freqMillis, freqMillis); + return new TimerHeartbeat(timer); + } + + private static String nm(String poolName) { + return poolName.isEmpty() ? "datasource.heartbeat" : "datasource." + poolName + ".heartbeat"; + } + + private static final class TimerHeartbeat implements Heartbeat { + + private final Timer timer; + + private TimerHeartbeat(Timer timer) { + this.timer = timer; + } + + @Override + public void stop() { + timer.cancel(); + } + } + + private static final class HeartbeatTask extends TimerTask { + + private final ConnectionPool pool; + + private HeartbeatTask(ConnectionPool pool) { + this.pool = pool; + } + + @Override + public void run() { + pool.heartbeat(); + } } } diff --git a/ebean-datasource/src/main/java21/io/ebean/datasource/pool/ExecutorFactory.java b/ebean-datasource/src/main/java21/io/ebean/datasource/pool/ExecutorFactory.java index 06f1d5f..066e750 100644 --- a/ebean-datasource/src/main/java21/io/ebean/datasource/pool/ExecutorFactory.java +++ b/ebean-datasource/src/main/java21/io/ebean/datasource/pool/ExecutorFactory.java @@ -1,11 +1,67 @@ package io.ebean.datasource.pool; +import io.ebean.datasource.pool.ConnectionPool.Heartbeat; + import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicBoolean; final class ExecutorFactory { static ExecutorService newExecutor() { - return Executors.newVirtualThreadPerTaskExecutor(); + ThreadFactory factory = Thread.ofVirtual().name("datasource.reaper").factory(); + return Executors.newThreadPerTaskExecutor(factory); + } + + static Heartbeat newHeartBeat(ConnectionPool pool, int freqMillis) { + return new VTHeartbeat(pool, freqMillis).start(); + } + + private static final class VTHeartbeat implements Heartbeat { + + private final AtomicBoolean running = new AtomicBoolean(false); + private final ConnectionPool pool; + private final int freqMillis; + private final Thread thread; + + private VTHeartbeat(ConnectionPool pool, int freqMillis) { + this.pool = pool; + this.freqMillis = freqMillis; + this.thread = Thread.ofVirtual() + .name(nm(pool.name())) + .unstarted(this::run); + } + + private static String nm(String poolName) { + return poolName.isEmpty() ? "datasource.heartbeat" : "datasource." + poolName + ".heartbeat"; + } + + private void run() { + while (running.get()) { + try { + Thread.sleep(freqMillis); + pool.heartbeat(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } catch (Exception e) { + // continue heartbeat + Log.warn("Error during heartbeat", e); + } + } + } + + private Heartbeat start() { + running.set(true); + thread.start(); + return this; + } + + @Override + public void stop() { + running.set(false); + thread.interrupt(); + } } } diff --git a/ebean-datasource/src/test/java/io/ebean/datasource/test/NetworkOutageTest.java b/ebean-datasource/src/test/java/io/ebean/datasource/test/NetworkOutageTest.java index 45a0dd7..b941ca4 100644 --- a/ebean-datasource/src/test/java/io/ebean/datasource/test/NetworkOutageTest.java +++ b/ebean-datasource/src/test/java/io/ebean/datasource/test/NetworkOutageTest.java @@ -18,6 +18,7 @@ * * @author Roland Praml, Foconis Analytics GmbH */ +@Disabled public class NetworkOutageTest { static void openPort(int port) throws Exception { diff --git a/pom.xml b/pom.xml index e136b0a..864590c 100644 --- a/pom.xml +++ b/pom.xml @@ -22,4 +22,18 @@ ebean-datasource-api + + + central + + + default + + [21,] + + + blackbox-tests + + +