org.tentackle.dbms.DbTransactionFactory Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of tentackle-database Show documentation
Show all versions of tentackle-database Show documentation
Tentackle Low-Level DBMS Layer
/*
* Tentackle - https://tentackle.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.tentackle.dbms;
import org.tentackle.common.Constants;
import org.tentackle.common.EncryptedProperties;
import org.tentackle.common.Service;
import org.tentackle.common.ServiceFactory;
import org.tentackle.common.Timestamp;
import org.tentackle.log.Logger;
import org.tentackle.misc.TimeKeeper;
import java.lang.ref.WeakReference;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
interface DbTransactionFactoryHolder {
DbTransactionFactory INSTANCE = ServiceFactory.createService(DbTransactionFactory.class, DbTransactionFactory.class);
}
/**
* Factory for transactions.
* Collects duration statistics and monitors transactions.
*/
@Service(DbTransactionFactory.class) // defaults to self
public class DbTransactionFactory {
/**
* The singleton.
*
* @return the singleton
*/
public static DbTransactionFactory getInstance() {
return DbTransactionFactoryHolder.INSTANCE;
}
private static final Logger LOG = Logger.get(DbTransactionFactory.class);
private static final String TX_IDLE_INTERVAL = "txIdleInterval";
private static final String TX_IDLE_TIMEOUT = "txIdleTimeout";
private static final long DEFAULT_TX_IDLE_INTERVAL = Constants.SECOND_MS; // default is to check every second
private static final int DEFAULT_TX_IDLE_TIMEOUT = 60; // 1 minute idle transaction timeout
private final Map> transactions; // txId::transaction
private final Queue finishedTransactions; // txIds of finished transactions
private final TransactionStatistics statistics;
private TransactionSupervisor transactionSupervisor;
private long txIdleInterval = DEFAULT_TX_IDLE_INTERVAL;
private int txIdleTimeout = DEFAULT_TX_IDLE_TIMEOUT;
private volatile boolean configChanged;
private boolean collectingStatistics;
private volatile boolean terminationRequested;
/**
* Creates the transaction factory.
*/
public DbTransactionFactory() {
transactions = new ConcurrentHashMap<>();
finishedTransactions = new ConcurrentLinkedQueue<>();
statistics = new TransactionStatistics();
configChanged = true;
}
/**
* Configures the factory from the main session properties.
*
* @param properties the properties
*/
public void configure(EncryptedProperties properties) {
String val = properties.getPropertyIgnoreCase(TX_IDLE_INTERVAL);
if (val != null) {
setTxIdleInterval(Long.parseLong(val));
}
val = properties.getPropertyIgnoreCase(TX_IDLE_TIMEOUT);
if (val != null) {
setTxIdleTimeout(Integer.parseInt(val));
}
}
/**
* Creates a transaction.
* Notice that this method is only invoked for local sessions, never for remote sessions.
*
* @param db the session
* @param txName the transaction name, null if <unnamed>
* @param fromRemote true if initiated from remote client
*/
public DbTransaction create(Db db, String txName, boolean fromRemote) {
DbTransaction transaction = new DbTransaction(db, txName, fromRemote);
transactions.put(transaction.getTxNumber(), new WeakReference<>(transaction));
ensureSupervisorAlive();
return transaction;
}
/**
* Finishes a transaction.
* Invoked when a transaction is committed or rolled back.
* Notice that this method is only invoked for local sessions, never for remote sessions.
*
* @param transaction the transaction
*/
public void finish(DbTransaction transaction) {
long txId = transaction.getTxNumber();
transactions.remove(txId);
if (collectingStatistics) {
statistics.countTransaction(transaction);
}
finishedTransactions.add(txId); // hand over to supervisor thread asynchronously
}
/**
* Sets whether statistics should be collected.
*
* @param collectingStatistics true if transactions are counted and execution times measured
*/
public void setCollectingStatistics(boolean collectingStatistics) {
this.collectingStatistics = collectingStatistics;
}
/**
* Returns whether statistics are collected.
*
* @return true if transactions are counted and execution times measured
*/
public boolean isCollectingStatistics() {
return collectingStatistics;
}
/**
* Logs the statistics.
*
* @param level the logging level
* @param clear true if clear statistics after dump
*/
public void logStatistics(Logger.Level level, boolean clear) {
statistics.logStatistics(null, level, " >TXN-Stats: ", clear);
}
/**
* Gets the check interval for idle or unreferenced transactions in ms.
*
* @return the check interval
*/
public long getTxIdleInterval() {
return txIdleInterval;
}
/**
* Sets the check interval for idle or unreferenced transactions in ms.
*
* @param txIdleInterval the check interval
*/
public void setTxIdleInterval(long txIdleInterval) {
configChanged = this.txIdleInterval != txIdleInterval;
this.txIdleInterval = txIdleInterval;
}
/**
* Gets the number of check intervals that must elapse to for an idle transaction to time out.
*
* @return the idle count
*/
public int getTxIdleTimeout() {
return txIdleTimeout;
}
/**
* Sets the number of check intervals that must elapse to for an idle transaction to time out.
*
* @param txIdleTimeout the idle count, ≤ 0 to disable
*/
public void setTxIdleTimeout(int txIdleTimeout) {
configChanged = this.txIdleTimeout != txIdleTimeout;
this.txIdleTimeout = txIdleTimeout;
}
/**
* Requests supervisor thread termination.
*/
public void requestTermination() {
if (transactionSupervisor != null) {
terminationRequested = true;
transactionSupervisor.interrupt();
}
}
private synchronized void ensureSupervisorAlive() {
if (transactionSupervisor == null) {
transactionSupervisor = new TransactionSupervisor();
transactionSupervisor.start();
}
}
private synchronized void supervisorTerminated() {
transactionSupervisor = null;
}
/**
* The supervisor finds unreferenced transactions and cleans up.
* It also closes sessions with transactions being idle too long.
* The thread is started when the first transaction (local sessions only!) is created.
* For remote sessions, the thread will never be started and the {@code DbTransactionFactory} is just dummy,
* for example in a remote {@code ServerApplication}.
*/
private class TransactionSupervisor extends Thread {
private static class TxIdleCounter {
final String txName;
final long txNumber;
final long creationTime;
final TimeKeeper timeKeeper;
int idleCount;
TxIdleCounter(DbTransaction transaction) {
// we must not keep a reference to the transaction itself!
txName = transaction.getTxName();
txNumber = transaction.getTxNumber();
creationTime = transaction.getCreationTime();
timeKeeper = transaction.getTimeKeeper();
}
int count() {
return ++idleCount;
}
@Override
public String toString() {
return txName + ":" + txNumber + ", started " + new Timestamp(creationTime) + " (" + timeKeeper.end().millisToString() + " ms)";
}
}
private final Map idleMap = new HashMap<>(); // idle transactions suspected to time out
private TransactionSupervisor() {
super("Transaction Supervisor");
setDaemon(true);
}
@Override
public void run() {
while (!terminationRequested) {
if (configChanged) {
LOG.info("{0} running with interval={1} ms, maxIdleCount={2}", getName(), txIdleInterval, txIdleTimeout);
configChanged = false;
}
try {
sleep(txIdleInterval);
// remove finished transactions
Long finishedTxId;
while ((finishedTxId = finishedTransactions.poll()) != null) {
idleMap.remove(finishedTxId);
}
// check running transaction for idle timeout
for (Iterator>> iter = transactions.entrySet().iterator(); iter.hasNext(); ) {
Map.Entry> entry = iter.next();
DbTransaction transaction = entry.getValue().get();
if (transaction == null) {
// transactions cannot become unreferenced since the session is always attached when the tx starts. Attached sessions
// are referenced by the ManagedConnection until the tx ends, which will remove the tx from the transactions map above.
// If we encounter an unreferenced tx, it must have been detached within the transaction or some other weird condition.
Long txId = entry.getKey();
TxIdleCounter idleCounter = idleMap.remove(txId);
LOG.severe("unreferenced transaction {0} detected", idleCounter == null ? txId : idleCounter);
iter.remove();
}
else {
if (transaction.isAlive()) {
transaction.setAlive(false); // polled (will be setAlive as soon as some I/O happens again)
idleMap.remove(transaction.getTxNumber());
}
else {
if (txIdleTimeout > 0 &&
idleMap.computeIfAbsent(transaction.getTxNumber(), txNumber -> new TxIdleCounter(transaction)).count() > txIdleTimeout) {
String msg = "idle transaction " + transaction.getSession() + " timed out after " +
txIdleTimeout * txIdleInterval / Constants.SECOND_MS + " s";
LOG.warning(msg);
idleMap.remove(transaction.getTxNumber());
iter.remove();
Db session = transaction.getSession();
ManagedConnection mc = session.getConnection();
if (mc != null) {
final Connection con = mc.getConnection();
if (con != null) {
// do that in a separate thread, if it gets stuck, the supervisor still keeps running
createTxCleanupThread(mc, con).start();
}
else {
LOG.warning("managed connection {0} already closed", mc);
}
}
else {
LOG.warning("managed connection already detached from session {0}", session);
}
}
}
}
}
}
catch (InterruptedException ix) {
// daemon thread is terminated via requestTermination()
}
catch (RuntimeException ex) {
LOG.severe("cleanup transactions failed", ex);
}
}
supervisorTerminated();
LOG.info("{0} terminated", getName());
}
private static Thread createTxCleanupThread(ManagedConnection mc, Connection con) {
Thread txCleanupThread = new Thread("transaction cleanup " + mc) {
@Override
public void run() {
try {
LOG.warning("performing low-level rollback and close of {0}", con);
// really low level (as if the database closed it suddenly)
try {
con.rollback();
}
catch (SQLException sr) {
LOG.warning("low-level rollback failed", sr);
}
con.close(); // just close -> leave the cleanup to the thread stuck in transaction
}
catch (SQLException | RuntimeException ex) {
LOG.warning("closing JDBC connection failed", ex);
}
}
};
txCleanupThread.setDaemon(true);
return txCleanupThread;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy