/*
* 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 java.io.File;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.log4j.Logger;
import org.exist.EXistException;
import org.exist.security.PermissionDeniedException;
import org.exist.security.SecurityManager;
import org.exist.storage.BrokerPool;
import org.exist.storage.DBBroker;
import org.exist.storage.SystemTask;
import org.exist.storage.SystemTaskManager;
import org.exist.storage.journal.Journal;
import org.exist.storage.recovery.RecoveryManager;
import org.exist.util.ReadOnlyException;
import org.exist.xmldb.XmldbURI;
/**
* 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 {
public final static String RECOVERY_GROUP_COMMIT_ATTRIBUTE = "group-commit";
public final static String PROPERTY_RECOVERY_GROUP_COMMIT = "db-connection.recovery.group-commit";
public final static String RECOVERY_FORCE_RESTART_ATTRIBUTE = "force-restart";
public final static String PROPERTY_RECOVERY_FORCE_RESTART = "db-connection.recovery.force-restart";
/**
* Logger for this class
*/
private static final Logger LOG = Logger .getLogger(TransactionManager.class);
private long nextTxnId = 0;
private Journal journal;
private boolean enabled;
private boolean groupCommit = false;
private boolean forceRestart = false;
private int activeTransactions = 0;
private Lock lock = new ReentrantLock();
private BrokerPool pool;
/**
* Manages all system tasks
*/
private SystemTaskManager taskManager;
/**
* Initialize the transaction manager using the specified data directory.
*
* @param dataDir
* @throws EXistException
*/
public TransactionManager(BrokerPool pool, File dataDir, boolean transactionsEnabled) throws EXistException {
this.pool = pool;
enabled = transactionsEnabled;
if (enabled)
journal = new Journal(pool, dataDir);
Boolean groupOpt = (Boolean) pool.getConfiguration().getProperty(PROPERTY_RECOVERY_GROUP_COMMIT);
if (groupOpt != null) {
groupCommit = groupOpt.booleanValue();
if (LOG.isDebugEnabled())
LOG.debug("GroupCommits = " + groupCommit);
}
Boolean restartOpt = (Boolean) pool.getConfiguration().getProperty(PROPERTY_RECOVERY_FORCE_RESTART);
if (restartOpt != null) {
forceRestart = restartOpt.booleanValue();
if (LOG.isDebugEnabled())
LOG.debug("ForceRestart = " + forceRestart);
}
taskManager = new SystemTaskManager(pool);
}
public void initialize() throws EXistException, ReadOnlyException {
if (enabled)
journal.initialize();
activeTransactions = 0;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isIdle() {
return activeTransactions == 0;
}
/**
* Run a database recovery if required. This method is called once during
* startup from {@link org.exist.storage.BrokerPool}.
*
* @param broker
* @throws EXistException
*/
public boolean runRecovery(DBBroker broker) throws EXistException {
RecoveryManager recovery = new RecoveryManager(broker, journal, forceRestart);
return recovery.recover();
}
/**
* Create a new transaction. Creates a new transaction id that will
* be logged to disk immediately.
*/
public Txn beginTransaction() {
if (!enabled)
return null;
return new RunWithLock<Txn>() {
public Txn execute() {
long txnId = nextTxnId++;
try {
journal.writeToLog(new TxnStart(txnId));
} catch (TransactionException e) {
LOG.warn("Failed to create transaction. Error writing to log file.", e);
}
++activeTransactions;
return new Txn(txnId);
}
}.run();
}
/**
* Commit a transaction.
*
* @param txn
* @throws TransactionException
*/
public void commit(final Txn txn) throws TransactionException {
if (!enabled)
return;
new RunWithLock<Object>() {
public Object execute() {
--activeTransactions;
if (enabled) {
try {
journal.writeToLog(new TxnCommit(txn.getId()));
} catch (TransactionException e) {
LOG.error("transaction manager caught exception while committing", e);
}
if (!groupCommit)
journal.flushToLog(true);
}
txn.releaseAll();
processSystemTasks();
return null;
}
}.run();
}
public void abort(final Txn txn) {
if (!enabled || txn == null)
return;
new RunWithLock<Object>() {
public Object execute() {
--activeTransactions;
try {
journal.writeToLog(new TxnAbort(txn.getId()));
} catch (TransactionException e) {
LOG.warn("Failed to write abort record to journal: " + e.getMessage());
}
if (!groupCommit)
journal.flushToLog(true);
txn.releaseAll();
processSystemTasks();
return null;
}
}.run();
}
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).
*
* @throws TransactionException
*/
public void checkpoint(boolean switchFiles) throws TransactionException {
if (!enabled)
return;
long txnId = nextTxnId++;
journal.checkpoint(txnId, switchFiles);
}
public Journal getJournal() {
return journal;
}
public void reindex(DBBroker broker) {
broker.setUser(SecurityManager.SYSTEM_USER);
try {
broker.reindexCollection(XmldbURI.ROOT_COLLECTION_URI);
} catch (PermissionDeniedException e) {
LOG.warn("Exception during reindex: " + e.getMessage(), e);
}
}
public void shutdown(boolean checkpoint) {
if (enabled) {
long txnId = nextTxnId++;
journal.shutdown(txnId, checkpoint);
activeTransactions = 0;
}
}
public void triggerSystemTask(final SystemTask task) {
new RunWithLock<Object>() {
public Object execute() {
taskManager.triggerSystemTask(task);
return null;
}
}.run();
}
public void processSystemTasks() {
new RunWithLock<Object>() {
public Object execute() {
if (activeTransactions == 0)
taskManager.processTasks();
return null;
}
}.run();
}
/**
* Run code block with a lock on the transaction manager.
* Make sure locks are acquired in the right order.
*
* @author wolf
*
*/
private abstract class RunWithLock<T> {
public T run() {
DBBroker broker = null;
try {
// we first need to get a broker for the current thread
// before we acquire the transaction manager lock. Otherwise
// a deadlock may occur.
broker = pool.get(null);
try {
lock.lock();
return execute();
} finally {
lock.unlock();
}
} catch (EXistException e) {
LOG.warn("Transaction manager failed to acquire broker for running system tasks");
return null;
} finally {
pool.release(broker);
}
}
public abstract T execute();
}
}