Merge branch 'feature/ext-data/close-pool-on-stop' into feature/ext-vite/replace-web-bundler
This commit is contained in:
@@ -112,6 +112,8 @@ public abstract class Repository<T, ID> {
|
|||||||
- `Tx` in the `FlashContext`
|
- `Tx` in the `FlashContext`
|
||||||
- `TxManager` in the `FlashContext`
|
- `TxManager` in the `FlashContext`
|
||||||
- an annotation processor for `@Transactional`
|
- 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.
|
This makes the data layer composable with Flash's extension system without global state.
|
||||||
|
|
||||||
|
|||||||
+1
@@ -34,6 +34,7 @@ public final class DataExtension implements FlashExtension {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure(FlashRegistrar<?> app, FlashContext ctx) {
|
public void configure(FlashRegistrar<?> app, FlashContext ctx) {
|
||||||
|
ctx.onClose(txManager::close);
|
||||||
ctx.provide(Tx.class, tx);
|
ctx.provide(Tx.class, tx);
|
||||||
ctx.provide(TxManager.class, txManager);
|
ctx.provide(TxManager.class, txManager);
|
||||||
if (data != null) ctx.provide(Data.class, data);
|
if (data != null) ctx.provide(Data.class, data);
|
||||||
|
|||||||
+5
-1
@@ -1,7 +1,11 @@
|
|||||||
package dev.relism.flash.ext.data.core;
|
package dev.relism.flash.ext.data.core;
|
||||||
|
|
||||||
public interface TxManager {
|
public interface TxManager extends AutoCloseable {
|
||||||
TxStatus begin(TxDefinition definition);
|
TxStatus begin(TxDefinition definition);
|
||||||
void commit(TxStatus status);
|
void commit(TxStatus status);
|
||||||
void rollback(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();
|
||||||
}
|
}
|
||||||
|
|||||||
+22
@@ -3,6 +3,10 @@ package dev.relism.flash.ext.data.hibernate;
|
|||||||
import dev.relism.flash.ext.data.core.*;
|
import dev.relism.flash.ext.data.core.*;
|
||||||
import org.hibernate.Session;
|
import org.hibernate.Session;
|
||||||
import org.hibernate.SessionFactory;
|
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;
|
import java.util.Objects;
|
||||||
|
|
||||||
@@ -16,6 +20,24 @@ public class HibernateTxManager implements TxManager {
|
|||||||
this.sf = Objects.requireNonNull(sessionFactory);
|
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
|
@Override
|
||||||
public TxStatus begin(TxDefinition definition) {
|
public TxStatus begin(TxDefinition definition) {
|
||||||
return switch (definition.propagation()) {
|
return switch (definition.propagation()) {
|
||||||
|
|||||||
+48
@@ -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> T unwrap(Class<T> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
@@ -17,6 +17,18 @@ public class JdbcTxManager implements TxManager {
|
|||||||
this.ds = Objects.requireNonNull(ds);
|
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
|
@Override
|
||||||
public TxStatus begin(TxDefinition definition) {
|
public TxStatus begin(TxDefinition definition) {
|
||||||
return switch (definition.propagation()) {
|
return switch (definition.propagation()) {
|
||||||
|
|||||||
+20
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user