/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-04 The eXist Project
* http://exist-db.org
*
* This program 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
* of the License, or (at your option) any later version.
*
* This program 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 program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* $Id$
*/
package org.exist.storage.txn;
import net.jcip.annotations.GuardedBy;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.EXistException;
import org.exist.security.PermissionDeniedException;
import org.exist.storage.*;
import org.exist.storage.journal.JournalException;
import org.exist.storage.journal.JournalManager;
import org.exist.xmldb.XmldbURI;
import java.io.IOException;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* This is the central entry point to the transaction management service.
*
* There's only one TransactionManager per database instance that can be
* retrieved via {@link BrokerPool#getTransactionManager()}. TransactionManager
* provides methods to create, commit and rollback a transaction.
*
* @author wolf
*
*/
public class TransactionManager implements BrokerPoolService {
/**
* Logger for this class
*/
private static final Logger LOG = LogManager.getLogger(TransactionManager.class);
private long nextTxnId = 0;
private final BrokerPool pool;
private final Optional<JournalManager> journalManager;
private final SystemTaskManager systemTaskManager;
private final Map<Long, TxnCounter> transactions = new HashMap<>();
private final Lock lock = new ReentrantLock();
/**
* Initialize the transaction manager using the specified data directory.
*
* @param pool
* @param journalManager
* @param systemTaskManager
* @throws EXistException
*/
public TransactionManager(final BrokerPool pool, final Optional<JournalManager> journalManager,
final SystemTaskManager systemTaskManager) {
this.pool = pool;
this.journalManager = journalManager;
this.systemTaskManager = systemTaskManager;
}
/**
* Create a new transaction. Creates a new transaction id that will
* be logged to disk immediately.
*/
public Txn beginTransaction() {
return withLock(broker -> {
final long txnId = nextTxnId++;
if(LOG.isDebugEnabled()) {
LOG.debug("Starting new transaction: " + txnId);
}
if(journalManager.isPresent()) {
try {
journalManager.get().journal(new TxnStart(txnId));
} catch(final JournalException e) {
LOG.error("Failed to create transaction. Error writing to log file.", e);
}
}
final Txn txn = new Txn(TransactionManager.this, txnId);
transactions.put(txn.getId(), new TxnCounter());
return txn;
});
}
/**
* Commit a transaction.
*
* @param txn
* @throws TransactionException
*/
public void commit(final Txn txn) throws TransactionException {
//we can only commit something which is in the STARTED state
if (txn.getState() != Txn.State.STARTED) {
return;
}
withLock(broker -> {
if(journalManager.isPresent()) {
try {
journalManager.get().journalGroup(new TxnCommit(txn.getId()));
} catch(final JournalException e) {
LOG.error("Failed to write commit record to journal: " + e.getMessage());
}
}
txn.signalCommit();
txn.releaseAll();
transactions.remove(txn.getId());
processSystemTasks();
if(LOG.isDebugEnabled()) {
LOG.debug("Committed transaction: " + txn.getId());
}
});
}
public void abort(final Txn txn) {
Objects.requireNonNull(txn);
//we can only abort something which is in the STARTED state
if (txn.getState() != Txn.State.STARTED) {
return;
}
withLock(broker -> {
transactions.remove(txn.getId());
if(journalManager.isPresent()) {
try {
journalManager.get().journalGroup(new TxnAbort(txn.getId()));
} catch(final JournalException e) {
LOG.error("Failed to write abort record to journal: " + e.getMessage());
}
}
txn.signalAbort();
txn.releaseAll();
processSystemTasks();
});
}
/**
* Make sure the transaction has either been committed or aborted.
*
* @param txn
*/
public void close(final Txn txn) {
Objects.requireNonNull(txn);
//if the transaction is already closed, do nothing
if (txn.getState() == Txn.State.CLOSED) {
return;
}
try {
//if the transaction is started, then we should auto-abort the uncommitted transaction
if (txn.getState() == Txn.State.STARTED) {
LOG.warn("Transaction was not committed or aborted, auto aborting!");
abort(txn);
}
} finally {
txn.setState(Txn.State.CLOSED); //transaction is now closed!
}
}
/**
* Keep track of a new operation within the given transaction.
*
* @param txnId
*/
public void trackOperation(final long txnId) {
final TxnCounter count = transactions.get(txnId);
// checkpoint operations do not create a transaction, so we have to check for null here
if (count != null) {
count.increment();
}
}
public Lock getLock() {
return lock;
}
/**
* Create a new checkpoint. A checkpoint fixes the current database state. All dirty pages
* are written to disk and the journal file is cleaned.
*
* This method is called from
* {@link org.exist.storage.BrokerPool} within pre-defined periods. It
* should not be called from somewhere else. The database needs to
* be in a stable state (all transactions completed, no operations running).
*
* @param switchFiles Indicates whether a new journal file should be started
*
* @throws TransactionException
*/
public void checkpoint(final boolean switchFiles) throws TransactionException {
final long txnId = nextTxnId++;
if(journalManager.isPresent()) {
try {
journalManager.get().checkpoint(txnId, switchFiles);
} catch(final JournalException e) {
throw new TransactionException(e.getMessage(), e);
}
}
}
/**
* @Deprecated This mixes concerns and should not be here.
*/
@Deprecated
public void reindex(final DBBroker broker) throws IOException {
broker.pushSubject(broker.getBrokerPool().getSecurityManager().getSystemSubject());
try {
broker.reindexCollection(XmldbURI.ROOT_COLLECTION_URI);
} catch (final PermissionDeniedException e) {
LOG.error("Exception during reindex: " + e.getMessage(), e);
} finally {
broker.popSubject();
}
}
public void shutdown() {
if(LOG.isDebugEnabled()) {
LOG.debug("Shutting down transaction manager. Uncommitted transactions: " + transactions.size());
}
final int uncommitted = uncommittedTransaction();
shutdown(uncommitted == 0);
}
public void shutdown(final boolean checkpoint) {
final long txnId = nextTxnId++;
if(journalManager.isPresent()) {
journalManager.get().shutdown(txnId, checkpoint);
}
transactions.clear();
}
private int uncommittedTransaction() {
int count = 0;
if (transactions.isEmpty()) {
return count;
}
for (final Map.Entry<Long, TxnCounter> entry : transactions.entrySet()) {
if (entry.getValue().counter > 0) {
LOG.warn("Found an uncommitted transaction with id " + entry.getKey() + ". Pending operations: " + entry.getValue().counter);
count++;
}
}
if (count > 0) {
LOG.warn("There are uncommitted transactions. A recovery run may be triggered upon restart.");
}
return count;
}
public void triggerSystemTask(final SystemTask task) {
withLock(broker -> {
systemTaskManager.triggerSystemTask(task);
});
}
public void processSystemTasks() {
withLock(broker -> {
if(transactions.isEmpty()) {
systemTaskManager.processTasks();
}
});
}
public void debug(final PrintStream out) {
out.println("Active transactions: "+ transactions.size());
}
/**
* Run a consumer within a lock on the transaction manager.
* Make sure locks are acquired in the right order.
*
* @param lockedCn A consumer that must be run exclusively
* with respect to the TransactionManager
* instance
*/
@GuardedBy("lock")
private void withLock(final Consumer<DBBroker> lockedCn) {
withLock(broker -> {
lockedCn.accept(broker);
return null;
});
}
/**
* Run a function within a lock on the transaction manager.
* Make sure locks are acquired in the right order.
*
* @param lockedFn A function that must be run exclusively
* with respect to the TransactionManager
* instance
*
* @return The result of lockedFn
*/
@GuardedBy("lock")
private <T> T withLock(final Function<DBBroker, T> lockedFn) {
try(final DBBroker broker = pool.getBroker()) {
try {
lock.lock();
return lockedFn.apply(broker);
} finally {
lock.unlock();
}
} catch (final EXistException e) {
LOG.error("Transaction manager failed to acquire broker for running system tasks");
return null;
}
}
/**
* Keep track of the number of operations processed within a transaction.
* This is used to determine if there are any uncommitted transactions
* during shutdown.
*/
protected final static class TxnCounter {
int counter = 0;
public void increment() {
counter++;
}
}
}