/*
* This file is part of the HyperGraphDB source distribution. This is copyrighted
* software. For permitted uses, licensing options and redistribution, please see
* the LicensingInformation file at the root level of the distribution.
*
* Copyright (c) 2005-2010 Kobrix Software, Inc. All rights reserved.
*/
package org.hypergraphdb.transaction;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import org.hypergraphdb.HGException;
import org.hypergraphdb.HyperGraph;
import org.hypergraphdb.util.HGUtils;
/**
*
* <p>
* The <code>HGTransactionManager</code> handles transactional activity for a single
* HyperGraph instance. You can obtain the transaction manager for a HyperGraph by
* calling its <code>getTransactionManager</code> method.
* </p>
*
* @author Borislav Iordanov
*
*/
public class HGTransactionManager
{
private HyperGraph graph;
private HGTransactionFactory factory;
private ThreadLocal<HGTransactionContext> tcontext = new ThreadLocal<HGTransactionContext>();
private boolean enabled = true;
volatile ActiveTransactionsRecord mostRecentRecord = new ActiveTransactionsRecord(0, null);
final ReentrantLock COMMIT_LOCK = new ReentrantLock(true);
TxMonitor txMonitor = null;
AtomicInteger conflicted = new AtomicInteger(0);
AtomicInteger successful = new AtomicInteger(0);
/**
* <p>Return <code>true</code> if the transaction are enabled and <code>false</code>
* otherwise.</p>
*/
public boolean isEnabled()
{
return enabled;
}
/**
* <p>Enable or disable transaction. Note that all current transactions will
* be silently aborted so make sure any pending transactions are completed before
* calling this method.</p>
*
* @param enabled <code>true</code> if transaction must be henceforth enabled and
* <code>false</code> otherwise.
*/
public void setEnabled(boolean enabled)
{
this.enabled = enabled;
}
/**
* <p>Enable transactions - equivalent to <code>setEnabled(true)</code>.</p>
*/
public void enable() { setEnabled(true); }
/**
* <p>Disable transactions - equivalent to <code>setEnabled(false)</code></p>
*/
public void disable() { setEnabled(false); }
/**
* <p>Return the <code>HGTransactionContext</code> instance associated with the current
* thread.</p>
*/
public HGTransactionContext getContext()
{
HGTransactionContext ctx = tcontext.get();
if (ctx == null)
{
ctx = new DefaultTransactionContext(this);
tcontext.set(ctx);
}
return ctx;
}
/**
* <p>
* Construct a new transaction manager with the given storage transaction factory. This
* method is normally called only internally. To obtain the transaction manager
* bound to a HyperGraph, use <code>HyperGraph.getTransactionManager</code>.
* </p>
*
* @param factory The <code>HGTransactionFactory</code> responsible for
* fabricating new transactions.
*/
public HGTransactionManager(HGTransactionFactory factory)
{
this.factory = factory;
}
/**
* <p>
* Set the {@link HyperGraph} instance associated with this
* <code>HGTransactionManager</code>.
* <strong>Do not call - used internally during initialization.
* </p>
*/
public void setHyperGraph(HyperGraph graph)
{
this.graph = graph;
}
/**
* <p>Return the {@link HyperGraph} instance associated with this
* <code>HGTransactionManager</code>.
* </p>
*/
public HyperGraph getHyperGraph()
{
return graph;
}
/**
* <p>
* Attach the given transaction context to the current thread. This
* method is normally called in a server environment. By default,
* a transaction context will be created and bound to a thread
* if need be, every time a new transaction is requested. So when
* HyperGraph is embedded in a client application, there is no
* need to explicitly attach/detach contexts to threads. However, in
* an environment using thread pooling such as is common in servers where
* a single transaction can span multiple requests, use the
* <code>threadAttach</code> and <code>threadDetach</code> methods
* to switch transactions contexts bound to clients.
* </p>
*
* @param tContext
*/
public void threadAttach(HGTransactionContext tContext)
{
tcontext.set(tContext);
}
/**
* <p>
* Detach the transaction context bound to the current thread. This
* method is normally called in a server environment. By default,
* a transaction context will be created and bound to a thread
* if need be, every time a new transaction is requested. So when
* HyperGraph is embedded in a client application, there is no
* need to explicitly attach/detach contexts to threads.
* </p>
*
* <p>
* <strong>IMPORTANT NOTE:</strong> when managing transaction
* contexts explicitly, you are responsible for closing
* all pending transactions in the context before disposing of it.
* This is done by invoking the <code>HGTransactionContext.endAll</code>
* method.
* </p>
*
* @param tContext
*/
public void threadDetach()
{
tcontext.set(null);
}
/**
* <p>
* Create and return a child transaction of the given parent transaction.
* </p>
*
* @param The parent <code>HGTransaction</code> - if null, a top-level
* transaction object is returned.
* @return The newly created transaction.
*/
HGTransaction createTransaction(HGTransaction parent, HGTransactionConfig config)
{
HGStorageTransaction storageTx = config.isNoStorage() || !enabled ? null
: factory.createTransaction(getContext(), config, parent);
ActiveTransactionsRecord activeRecord = mostRecentRecord.getRecordForNewTransaction();
if (enabled)
{
HGTransaction result = new HGTransaction(getContext(),
parent,
activeRecord,
storageTx,
config.isReadonly());
if (txMonitor != null)
txMonitor.transactionCreated(result);
return result;
}
else
return new HGTransaction(getContext(), parent, activeRecord, new VanillaTransaction(), config.isReadonly());
}
/**
* <p>
* Begin a new transaction in the current transaction context. If there's
* no transaction context bound to the active thread, one will be created.
* </p>
*/
public void beginTransaction()
{
beginTransaction(HGTransactionConfig.DEFAULT);
}
/**
* <p>
* Begin a new transaction in the current transaction context. If there's
* no transaction context bound to the active thread, one will be created.
* </p>
*
* @param config A {@link HGTransactionConfig} instance holding configuration
* parameters for the newly created transaction.
*/
public void beginTransaction(HGTransactionConfig config)
{
getContext().beginTransaction(config);
}
/**
* <p>
* Terminate the currently active transaction. The transaction will
* be aborted or committed based on the <code>success</code> flag
* (abort when false and commit when true).
* </p>
*
* <p>
* You are graced with a <code>HGException</code> if there's no currently
* active transaction.
* </p>
*
* @param success
*/
public void endTransaction(boolean success) throws HGTransactionException
{
HGTransactionContext ctx = tcontext.get();
if (ctx == null)
throw new HGException("Attempt to end a transaction with no transaction context currently active.");
else
ctx.endTransaction(success);
}
/**
* <p>
* Commit the current transaction by calling <code>endTransaction(true)</code>.
* Wrap the possible <code>HGTransactionException</code> in a <code>HGException</code>.
* </p>
*/
public void commit()
{
try
{
endTransaction(true);
}
catch (HGTransactionException ex)
{
throw new HGException(ex);
}
}
/**
* <p>
* Abort the current transaction by calling <code>endTransaction(false)</code>.
* Wrap the possible <code>HGTransactionException</code> in a <code>HGException</code>.
* </p>
*/
public void abort()
{
try
{
endTransaction(false);
}
catch (HGTransactionException ex)
{
throw new HGException(ex);
}
}
/**
* <p>
* Equivalent to <code>ensureTransaction(transaction, HGTransactionConfig.DEFAULT)</code>.
* </p>
*/
public <V> V ensureTransaction(Callable<V> transaction)
{
return ensureTransaction(transaction, HGTransactionConfig.DEFAULT);
}
/**
* <p>
* Perform a unit of work encapsulated as a transaction and return the result. This method
* will reuse the currently active transaction if there is one or create a new transaction
* otherwise.
* </p>
*
* @param <V> The type of the return value.
* @param transaction The transaction process encapsulated as a <code>Callable</code> instance.
* @param config The configuration of this transaction - note that if there's a current transaction in
* effect, this configuration parameter will be ignored as no new transaction will be created.
* @return The result of <code>transaction.call()</code>.
* @throws The method will (re)throw any exception that does not result from a deadlock.
*/
public <V> V ensureTransaction(Callable<V> transaction, HGTransactionConfig config)
{
if (getContext().getCurrent() != null)
try
{
return transaction.call();
}
catch (Exception ex)
{
throw new RuntimeException(ex);
}
else
return transact(transaction, config);
}
private void handleTxException(Throwable t)
{
// If there is a DeadlockException at the root of this, we have to simply abort
// the transaction and try again.
boolean retry = false;
for (Throwable cause = t; cause != null; cause = cause.getCause())
if (factory.canRetryAfter(cause))
{
retry = true;
break;
}
if (!retry)
{
if (t instanceof RuntimeException)
throw (RuntimeException)t;
else
throw new HGException(t);
}
}
/**
* <p>
* Equivalent to <code>transact(transaction, HGTransactionConfig.DEFAULT)</code>.
* </p>
*/
public <V> V transact(Callable<V> transaction)
{
return transact(transaction, HGTransactionConfig.DEFAULT);
}
/**
* <p>
* Perform a unit of work encapsulated as a transaction and return the result. This method
* explicitly allows deadlock (or write conflicts) to occur and it will re-attempt the transaction in such a
* case indefinitely. In order for the transaction to eventually complete, the underlying
* transactional system must be configured to be fair or to prioritize transaction randomly
* (which is the default behavior).
* </p>
*
* <p>
* If the <code>transaction.call()</code> returns without an exception, but the underlying
* database transaction has already been committed or aborted, this method return without
* doing anything further. Otherwise, upon a normal return from <code>transaction.call()</code>
* it will try to commit and re-try indefinitely if the commit fails.
* </p>
*
* <p>
* It is important that the <code>transaction.call()</code> doesn't leave any open nested transactions
* on the transaction stack.
* </p>
*
* @param <V> The type of the return value.
* @param transaction The transaction process encapsulated as a <code>Callable</code> instance.
* @param config The transaction configuration parameters.
* @return The result of <code>transaction.call()</code> or <code>null</code> if the transaction
* was aborted by the application by throwing a {@link HGUserAbortException}.
* @throws The method will (re)throw any exception that does not result from a deadlock.
*/
public <V> V transact(Callable<V> transaction, HGTransactionConfig config)
{
// We retry for as long as it takes. There's no reason
// why a transaction shouldn't eventually be able to acquire
// the locks it needs.
while (true)
{
beginTransaction(config);
V result = null;
try
{
result = transaction.call();
}
catch (HGUserAbortException ex)
{
try { endTransaction(false); }
catch (HGTransactionException tex) { tex.printStackTrace(System.err); }
return null;
}
catch (Throwable t)
{
try { endTransaction(false); }
catch (HGTransactionException tex) { tex.printStackTrace(System.err); }
if (HGUtils.getRootCause(t) instanceof TransactionIsReadonlyException &&
config.isWriteUpgradable())
{
config = HGTransactionConfig.DEFAULT;
}
else
{
handleTxException(t); // will re-throw if we can't retry the transaction
conflicted.incrementAndGet();
}
// System.out.println("Retrying transaction");
continue;
}
try
{
endTransaction(true);
successful.incrementAndGet(); // "successful" means not conflicting with other transactions
return result;
}
catch (Throwable t)
{
if (HGUtils.getRootCause(t) instanceof TransactionIsReadonlyException &&
config.isWriteUpgradable())
{
config = HGTransactionConfig.DEFAULT;
}
else
{
handleTxException(t); // will re-throw if we can't retry the transaction
conflicted.incrementAndGet();
}
// System.out.println("Retrying transaction");
}
}
}
}