feat: Vite extension and Maven plugin; data pools close on stop #19
@@ -112,6 +112,8 @@ public abstract class Repository<T, ID> {
|
||||
- `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.
|
||||
|
||||
|
||||
+1
@@ -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);
|
||||
|
||||
+5
-1
@@ -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();
|
||||
}
|
||||
|
||||
+22
@@ -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()) {
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
/** 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()) {
|
||||
|
||||
+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