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()); + } +}