/* * 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.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.hypergraphdb.HGException; import org.hypergraphdb.HyperGraph; import org.hypergraphdb.event.HGTransactionEndEvent; import org.hypergraphdb.util.Cons; /** * * <p> * Implements a transaction in HyperGraphDB. * </p> * * <p> * Each transaction can carry an arbitrary set of attributes along with it. This * is useful for attaching contextual information to transactions without * intruding to otherwise simple APIs. When a transaction is committed or * aborted, all attributes are removed. * </p> * * @author Borislav Iordanov * */ @SuppressWarnings("unchecked") public final class HGTransaction implements HGStorageTransaction { static final Object NULL_VALUE = new Object(); private HGTransaction parent; private HGTransactionContext context; private HGStorageTransaction stran = null; private Map<Object, Object> attributes = new HashMap<Object, Object>(); Map<VBox<?>, VBoxBody<?>> bodiesRead = new HashMap<VBox<?>, VBoxBody<?>>(); private Map<VBox<?>, Object> boxesWritten = new HashMap<VBox<?>, Object>(); private long number; private boolean readonly = false; private ActiveTransactionsRecord activeTxRecord; private Set<Runnable> abortActions = new HashSet<Runnable>(); long getNumber() { return number; } <T> T getLocalValue(VBox<T> vbox) { T value = null; HGTransaction tx = this; do { value = (T) tx.boxesWritten.get(vbox); } while (value == null && (tx = tx.parent) != null); return value; } <T> T getBoxValue(VBox<T> vbox) { T value = getLocalValue(vbox); if (value == null) { VBoxBody<T> body = vbox.body.getBody(number); if (!readonly) bodiesRead.put(vbox, body); value = body.value; } return (value == NULL_VALUE) ? null : value; } <T> void setBoxValue(VBox<T> vbox, T value) { if (this.isReadOnly()) throw new TransactionIsReadonlyException(); boxesWritten.put(vbox, value == null ? NULL_VALUE : value); } private boolean isWriteTransaction() { return !boxesWritten.isEmpty(); } /** * A commit can proceed only if none of the values we've read during * the transaction has changed (i.e. has been committed) since we read * them. In order words, the latest/current body of each VBox is the same * as the one tagged with this transaction's number. */ protected boolean validateCommit() { if (!readonly) for (Map.Entry<VBox<?>, VBoxBody<?>> entry : bodiesRead.entrySet()) { // Compare versions instead of 'body' objects because we may have multiple // re-loads of the same version of some disk data - we allow that in // transactional caches if (entry.getKey().body.version != entry.getValue().version) { return false; } } return true; } protected Cons<VBoxBody<?>> performValidCommit() { number = context.getManager().mostRecentRecord.transactionNumber + 1; return doCommit(); } /** * Commit all "written boxes" and return a linked list of the newly attached bodies * to them. Those bodies will be garbage collected eventually when it is determined * that no current transaction (or future) could be using them (see ActiveTransactionRecord.clean()). */ protected Cons<VBoxBody<?>> doCommit() { Cons<VBoxBody<?>> newBodies = Cons.EMPTY; for (Map.Entry<VBox<?>, Object> entry : boxesWritten.entrySet()) { VBox<Object> vbox = (VBox<Object>)entry.getKey(); Object newValue = entry.getValue(); VBoxBody<?> newBody = vbox.commit(this, (newValue == NULL_VALUE) ? null : newValue, number); newBodies = newBodies.cons(newBody); } return newBodies; } void finish() { if (!readonly) for (Map.Entry<VBox<?>, VBoxBody<?>> entry : bodiesRead.entrySet()) entry.getKey().finish(this); for (Map.Entry<VBox<?>, Object> entry : boxesWritten.entrySet()) entry.getKey().finish(this); bodiesRead = null; boxesWritten = null; activeTxRecord.decrementRunning(); } private void fatalFailure(Throwable t) { // A Throwable here could only mean something rather severe, such // as an OutOfMemory error, so we will just re-throw it. However, in case // the caller (the client application) doesn't catch it but attempts // some other DB operation (e.g. a graph.close() in a finally block or some // such), another transaction could be attempted in an inconsistent state. // To prevent that from happening, we disable transaction with the manager: context.getManager().setEnabled(false); // and since this is not enough (RAM transaction are still available and could // go into an infinite loop if the logic behind the transaction numbers is // driven into an inconsistent path, we also nullify the transaction record // which will throw an NPE on any attempt to start a new transaction. context.getManager().mostRecentRecord = null; // We'll also just print it out in case it gets swallowed by application code // and nobody can find the actual reason for the crash. t.printStackTrace(); if (t instanceof Error) throw (Error)t; else { if (t instanceof RuntimeException) throw (RuntimeException)t; else throw new HGException(t); } } HGTransaction(HGTransactionContext context, HGTransaction parent, ActiveTransactionsRecord activeTxRecord, HGStorageTransaction impl, boolean readonly) { this.stran = impl; this.context = context; this.parent = parent; this.activeTxRecord = activeTxRecord; this.number = activeTxRecord.transactionNumber; this.readonly = readonly; } public HGStorageTransaction getStorageTransaction() { return stran; } public void commit() throws HGTransactionException { if (isReadOnly() && isWriteTransaction()) { for (Object obj : boxesWritten.values()) System.out.println("written object:" + obj); throw new TransactionIsReadonlyException(); } // If this is a nested transaction, everything is much simpler if (parent != null) { if (stran != null) stran.commit(); if (!readonly) parent.bodiesRead.putAll(bodiesRead); parent.boxesWritten.putAll(boxesWritten); finish(); HyperGraph graph = context.getManager().getHyperGraph(); graph.getEventManager().dispatch(graph, new HGTransactionEndEvent(this, true)); return; } // Otherwise this is a top-level transaction, we need to do more serious work. if (isWriteTransaction()) { context.getManager().COMMIT_LOCK.lock(); try { if (validateCommit()) { if (stran != null) stran.commit(); Cons<VBoxBody<?>> bodiesCommitted = performValidCommit(); // The commit is already done, so create a new ActiveTransactionsRecord ActiveTransactionsRecord newRecord = new ActiveTransactionsRecord(number, bodiesCommitted); context.getManager().mostRecentRecord.setNext(newRecord); //newRecord.setPrev(context.getManager().mostRecentRecord); context.getManager().mostRecentRecord = newRecord; // as this transaction changed number, we must // update the activeRecords accordingly // the correct order is to increment first the // new, and only then decrement the old newRecord.incrementRunning(); this.activeTxRecord.decrementRunning(); // This assignment is need to decrementRunning in the finish method below. this.activeTxRecord = newRecord; } else { try { privateAbort(); } catch (Throwable t) { fatalFailure(t); } throw new TransactionConflictException(); } } catch (TransactionConflictException rethrowme) { throw rethrowme; } catch (Throwable t) // this should never happen, we're not expecting any exceptions here { fatalFailure(t); } finally { context.getManager().COMMIT_LOCK.unlock(); } } else { if (stran != null) stran.commit(); } HyperGraph graph = context.getManager().getHyperGraph(); graph.getEventManager().dispatch(graph, new HGTransactionEndEvent(this, true)); finish(); } private void privateAbort() throws HGTransactionException { for (Runnable r : abortActions) r.run(); if (stran != null) stran.abort(); finish(); } public void abort() throws HGTransactionException { privateAbort(); HyperGraph graph = context.getManager().getHyperGraph(); graph.getEventManager().dispatch(graph, new HGTransactionEndEvent(this, false)); } public <T> T getAttribute(Object key) { return (T)attributes.get(key); } public Iterator<Object> getAttributeKeys() { return attributes.keySet().iterator(); } public void removeAttribute(Object key) { attributes.remove(key); } public void setAttribute(Object key, Object value) { attributes.put(key, value); } public boolean isReadOnly() { return this.readonly; } public void addAbortAction(Runnable r) { this.abortActions.add(r); } /** * <p>Return the parent transaction of this transaction or <code>null</code> is this is not a nested * transaction.</p> */ public HGTransaction getParent() { return this.parent; } /** * <p>Return the top-level transaction of which this is a nested transaction, or <code>this</code> in case * this is already a top-level transaction.</p> */ public HGTransaction getTopLevel() { HGTransaction t = this; while (t.parent != null) t = t.parent; return t; } }