From d6c018242f8accc50cdba842eaf9c793465ca314 Mon Sep 17 00:00:00 2001 From: Zakaria El Orche Date: Tue, 22 Sep 2026 14:02:35 +0000 Subject: [PATCH] feat(ext-data): close the connection pool when the app stops TxManager is AutoCloseable and releases what it was built on: JdbcTxManager its data source when closeable, HibernateTxManager its session factory and then the data source Hibernate was handed, which Hibernate itself never closes. DataExtension registers the close as an onClose callback, so a stopped app no longer leaves its pool connected. Co-Authored-By: Claude Opus 5 --- .../flash-ext-data-core/docs/README.md | 2 + .../relism/flash/ext/data/DataExtension.java | 1 + .../relism/flash/ext/data/core/TxManager.java | 6 ++- .../data/hibernate/HibernateTxManager.java | 22 +++++++++ .../HibernateTxManagerCloseTest.java | 48 +++++++++++++++++++ .../flash/ext/data/jdbc/JdbcTxManager.java | 12 +++++ .../ext/data/jdbc/JdbcTxManagerCloseTest.java | 20 ++++++++ 7 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 flash-extensions/flash-ext-data-hibernate/src/test/java/dev/relism/flash/ext/data/hibernate/HibernateTxManagerCloseTest.java create mode 100644 flash-extensions/flash-ext-data-jdbc/src/test/java/dev/relism/flash/ext/data/jdbc/JdbcTxManagerCloseTest.java diff --git a/flash-extensions/flash-ext-data-core/docs/README.md b/flash-extensions/flash-ext-data-core/docs/README.md index df53379..77025c7 100644 --- a/flash-extensions/flash-ext-data-core/docs/README.md +++ b/flash-extensions/flash-ext-data-core/docs/README.md @@ -112,6 +112,8 @@ public abstract class Repository { - `Tx` in the `FlashContext` - `TxManager` in the `FlashContext` - an annotation processor for `@Transactional` +- `TxManager.close()` as an `onClose` callback, so stopping the app releases the manager's + session factory and connection pool; give the manager a pool you want closed with the app This makes the data layer composable with Flash's extension system without global state. diff --git a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/DataExtension.java b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/DataExtension.java index 017308b..870c61b 100644 --- a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/DataExtension.java +++ b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/DataExtension.java @@ -34,6 +34,7 @@ public final class DataExtension implements FlashExtension { @Override public void configure(FlashRegistrar app, FlashContext ctx) { + ctx.onClose(txManager::close); ctx.provide(Tx.class, tx); ctx.provide(TxManager.class, txManager); if (data != null) ctx.provide(Data.class, data); diff --git a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TxManager.java b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TxManager.java index 6a1b1c4..ac02baa 100644 --- a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TxManager.java +++ b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TxManager.java @@ -1,7 +1,11 @@ package dev.relism.flash.ext.data.core; -public interface TxManager { +public interface TxManager extends AutoCloseable { TxStatus begin(TxDefinition definition); void commit(TxStatus status); void rollback(TxStatus status); + + /** Releases what this manager was built on, its connection pool included. {@link dev.relism.flash.ext.data.DataExtension} calls it when the app stops. */ + @Override + void close(); } diff --git a/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxManager.java b/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxManager.java index 537893c..d0e053b 100644 --- a/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxManager.java +++ b/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxManager.java @@ -3,6 +3,10 @@ package dev.relism.flash.ext.data.hibernate; import dev.relism.flash.ext.data.core.*; import org.hibernate.Session; import org.hibernate.SessionFactory; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; +import org.hibernate.engine.spi.SessionFactoryImplementor; + +import javax.sql.DataSource; import java.util.Objects; @@ -16,6 +20,24 @@ public class HibernateTxManager implements TxManager { this.sf = Objects.requireNonNull(sessionFactory); } + /** + * Closes the session factory, then the data source it was given ({@code jakarta.persistence.nonJtaDataSource} + * or {@code hibernate.connection.datasource}): Hibernate stops a pool it built itself, never one handed to it. + */ + @Override + public void close() { + ConnectionProvider connections = sf.unwrap(SessionFactoryImplementor.class).getServiceRegistry().getService(ConnectionProvider.class); + DataSource ds = connections != null && connections.isUnwrappableAs(DataSource.class) ? connections.unwrap(DataSource.class) : null; + sf.close(); + if (ds instanceof AutoCloseable closeable) { + try { + closeable.close(); + } catch (Exception e) { + throw new IllegalStateException("Failed to close the data source", e); + } + } + } + @Override public TxStatus begin(TxDefinition definition) { return switch (definition.propagation()) { diff --git a/flash-extensions/flash-ext-data-hibernate/src/test/java/dev/relism/flash/ext/data/hibernate/HibernateTxManagerCloseTest.java b/flash-extensions/flash-ext-data-hibernate/src/test/java/dev/relism/flash/ext/data/hibernate/HibernateTxManagerCloseTest.java new file mode 100644 index 0000000..ae839e3 --- /dev/null +++ b/flash-extensions/flash-ext-data-hibernate/src/test/java/dev/relism/flash/ext/data/hibernate/HibernateTxManagerCloseTest.java @@ -0,0 +1,48 @@ +package dev.relism.flash.ext.data.hibernate; + +import org.hibernate.SessionFactory; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.junit.jupiter.api.Test; + +import javax.sql.DataSource; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HibernateTxManagerCloseTest { + + /** Stands in for a pool: closeable, and it records being closed. */ + static final class Pool implements DataSource, AutoCloseable { + boolean closed; + @Override public Connection getConnection() throws SQLException { return DriverManager.getConnection("jdbc:h2:mem:tx-close;DB_CLOSE_DELAY=-1"); } + @Override public Connection getConnection(String user, String password) throws SQLException { return getConnection(); } + @Override public void close() { closed = true; } + @Override public T unwrap(Class type) { throw new UnsupportedOperationException(); } + @Override public boolean isWrapperFor(Class type) { return false; } + @Override public PrintWriter getLogWriter() { return null; } + @Override public void setLogWriter(PrintWriter out) {} + @Override public void setLoginTimeout(int seconds) {} + @Override public int getLoginTimeout() { return 0; } + @Override public Logger getParentLogger() { throw new UnsupportedOperationException(); } + } + + @Test + void closingTheManagerClosesTheDataSourceHibernateWasGiven() { + Pool pool = new Pool(); + SessionFactory sf = new MetadataSources(new StandardServiceRegistryBuilder() + .applySetting(AvailableSettings.JAKARTA_NON_JTA_DATASOURCE, pool) + .applySetting(AvailableSettings.DIALECT, "org.hibernate.dialect.H2Dialect") + .build()).buildMetadata().buildSessionFactory(); + + new HibernateTxManager(sf).close(); + + assertTrue(sf.isClosed()); + assertTrue(pool.closed); + } +} diff --git a/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcTxManager.java b/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcTxManager.java index 08ad9b1..c61595d 100644 --- a/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcTxManager.java +++ b/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcTxManager.java @@ -17,6 +17,18 @@ public class JdbcTxManager implements TxManager { this.ds = Objects.requireNonNull(ds); } + /** Closes the data source when it is closeable, as a pool is; a plain {@code DataSource} holds nothing to release. */ + @Override + public void close() { + if (ds instanceof AutoCloseable closeable) { + try { + closeable.close(); + } catch (Exception e) { + throw new IllegalStateException("Failed to close the data source", e); + } + } + } + @Override public TxStatus begin(TxDefinition definition) { return switch (definition.propagation()) { diff --git a/flash-extensions/flash-ext-data-jdbc/src/test/java/dev/relism/flash/ext/data/jdbc/JdbcTxManagerCloseTest.java b/flash-extensions/flash-ext-data-jdbc/src/test/java/dev/relism/flash/ext/data/jdbc/JdbcTxManagerCloseTest.java new file mode 100644 index 0000000..ddf5844 --- /dev/null +++ b/flash-extensions/flash-ext-data-jdbc/src/test/java/dev/relism/flash/ext/data/jdbc/JdbcTxManagerCloseTest.java @@ -0,0 +1,20 @@ +package dev.relism.flash.ext.data.jdbc; + +import com.zaxxer.hikari.HikariDataSource; +import dev.relism.flash.ext.data.DataExtension; +import dev.relism.flash.extension.FlashApp; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JdbcTxManagerCloseTest { + + /** A pool outlives its app unless someone closes it; the data extension does, once requests have drained. */ + @Test + void stoppingTheAppClosesThePool() { + HikariDataSource pool = new HikariDataSource(); + pool.setJdbcUrl(TestDataSource.URL); + FlashApp.create(0).install(new DataExtension(new JdbcTxManager(pool))).start().stop().join(); + assertTrue(pool.isClosed()); + } +}