Java tutorial
/* * Copyright 2013 Gordon Burgett and individual contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.xflatdb.xflat.transaction; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.xflatdb.xflat.XFlatException; import org.xflatdb.xflat.convert.ConversionException; import org.xflatdb.xflat.convert.Converter; import org.xflatdb.xflat.db.EngineBase; import org.xflatdb.xflat.db.EngineTransactionManager; import org.xflatdb.xflat.db.XFlatDatabase; import org.xflatdb.xflat.util.DocumentFileWrapper; /** * A {@link TransactionManager} that uses the current thread as the context for transactions. * Each transaction opened by this manager will be bound to the current thread, and * the {@link #getTransaction() } method will return the transaction open on the current * thread, if any. * <p/> * This is the default TransactionManager used by XFlat. * @author Gordon */ public class ThreadContextTransactionManager extends EngineTransactionManager { private Map<Long, AmbientThreadedTransactionScope> currentTransactions = new ConcurrentHashMap<>(); private Map<Long, AmbientThreadedTransactionScope> committedTransactions = new ConcurrentHashMap<>(); private DocumentFileWrapper journalWrapper; private Document transactionJournal = null; private Log log = LogFactory.getLog(getClass()); /** * Creates a new ThreadContextTransactionManager, which will manage a mapping * of threads to transactions. * @param wrapper A wrapper which wraps the file to which this Transaction Manager * can save its Transaction Journal, for recovery in case of catastrophic error. */ public ThreadContextTransactionManager(DocumentFileWrapper wrapper) { this.journalWrapper = wrapper; } /** * Gets the Id of the current context, which is the current thread's ID. * @return The current thread's ID. */ protected Long getContextId() { return Thread.currentThread().getId(); } @Override public Transaction getTransaction() { TransactionBase tx = currentTransactions.get(getContextId()); if (tx == null || tx.getOptions().getPropagation() == Propagation.NOT_SUPPORTED) return null; return tx.getTransaction(); } @Override public TransactionScope openTransaction() throws TransactionPropagationException { return openTransaction(TransactionOptions.DEFAULT); } @Override public TransactionScope openTransaction(TransactionOptions options) throws TransactionPropagationException { AmbientThreadedTransactionScope ret; long contextId = getContextId(); switch (options.getPropagation()) { case MANDATORY: ret = currentTransactions.get(contextId); if (ret == null || ret.getOptions().getPropagation() == Propagation.NOT_SUPPORTED) { throw new TransactionPropagationException("propagation MANDATORY, but no current transaction."); } //use the current transaction, with a wrapper to prevent //this instance from closing it prematurely. return new WrappingTransactionScope(ret, options.getReadOnly()); case NESTED: throw new UnsupportedOperationException("Nested transactions not yet supported"); case NEVER: ret = currentTransactions.get(contextId); if (ret != null && ret.getOptions().getPropagation() != Propagation.NOT_SUPPORTED) { throw new TransactionPropagationException("propagation NEVER, but current transaction exists"); } //return a shell object representing the non-transactional operation. return new EmptyTransactionScope(options); case NOT_SUPPORTED: ret = currentTransactions.remove(contextId); if (ret == null || ret.getOptions().getPropagation() == Propagation.NOT_SUPPORTED) { //we are already operating non-transactionally, just need //to return a shell object. return new EmptyTransactionScope(options); } //need to return a shell that will also replace the suspended //transaction when it is closed. return new NotSupportedTransaction(ret, options); case REQUIRED: ret = currentTransactions.get(contextId); if (ret == null || ret.getOptions().getPropagation() == Propagation.NOT_SUPPORTED) { //no current transaction, create a new one //suspending the NOT_SUPPORTED transaction if it exists. ret = new AmbientThreadedTransactionScope(generateNewId(), ret, options); currentTransactions.put(contextId, ret); } //use the current transaction, with a wrapper to prevent //this instance from closing it prematurely. return new WrappingTransactionScope(ret, options.getReadOnly()); case REQUIRES_NEW: //create a new transaction, suspending the current one if it exists. ret = currentTransactions.get(contextId); ret = new AmbientThreadedTransactionScope(generateNewId(), ret, options); currentTransactions.put(contextId, ret); //use the current transaction, with a wrapper to prevent //this instance from closing it prematurely. return new WrappingTransactionScope(ret, options.getReadOnly()); case SUPPORTS: ret = currentTransactions.get(contextId); if (ret == null || ret.getOptions().getPropagation() == Propagation.NOT_SUPPORTED) { //we are already operating non-transactionally, just need //to return a shell object. return new EmptyTransactionScope(options); } //use the current transaction, with a wrapper to prevent //this instance from closing it prematurely. return new WrappingTransactionScope(ret, options.getReadOnly()); default: throw new UnsupportedOperationException( "Propagation behavior not supported: " + options.getPropagation().toString()); } } @Override public long isTransactionCommitted(long transactionId) { TransactionBase tx = committedTransactions.get(transactionId); return tx == null ? -1 : tx.commitId; } @Override public boolean isTransactionReverted(long transactionId) { //if we find it in the current transactions, check the transaction for (TransactionBase tx : currentTransactions.values()) { if (tx.id == transactionId) { return tx.isReverted(); } } //otherwise it might be in the committed transactions, if so it is not reverted. if (committedTransactions.get(transactionId) != null) { return false; } //if we lost it then it's reverted. return true; } @Override public long transactionlessCommitId() { return generateNewId(); } @Override public long getLowestOpenTransaction() { long lowest = Long.MAX_VALUE; for (TransactionBase tx : currentTransactions.values()) { if (tx.id < lowest) { lowest = tx.id; } } return lowest; } @Override public void bindEngineToCurrentTransaction(EngineBase engine) { TransactionBase tx = currentTransactions.get(getContextId()); if (tx == null) { return; } //we can get away with just adding it in an unsynchronized context because //this is never going to be called at the same time as unbind, since unbind //always happens in the context of a commit or revert (which is the same thread as this) //or another thread's cleanup after the transaction is closed. tx.boundEngines.add(engine); } @Override public synchronized void unbindEngineExceptFrom(EngineBase engine, Collection<Long> transactionIds) { Iterator<AmbientThreadedTransactionScope> it = this.committedTransactions.values().iterator(); while (it.hasNext()) { TransactionBase tx = it.next(); if (transactionIds.contains(tx.id)) { continue; } //try to remove its binding tx.boundEngines.remove(engine); if (tx.boundEngines.isEmpty()) { //remove it from the committed transactions if it is empty. it.remove(); } } } @Override public boolean anyOpenTransactions() { return !this.currentTransactions.isEmpty(); } private void loadJournal() throws IOException, JDOMException { transactionJournal = journalWrapper.readFile(); if (transactionJournal == null) { transactionJournal = new Document(); transactionJournal.setRootElement(new Element("transactionJournal")); } } private synchronized void commit(AmbientThreadedTransactionScope tx) throws TransactionException { //journal the entry so we can recover if catastrophic failure occurs TransactionJournalEntry entry = new TransactionJournalEntry(); entry.txId = tx.id; entry.commitId = tx.commitId; for (EngineBase e : tx.boundEngines) { entry.tableNames.add(e.getTableName()); } Element entryElement = null; if (tx.options.isDurable()) { //use the transaction journal to ensure durability try { if (transactionJournal == null) { loadJournal(); } entryElement = toElement.convert(entry); transactionJournal.getRootElement().addContent(entryElement); journalWrapper.writeFile(transactionJournal); } catch (ConversionException | IOException | JDOMException ex) { throw new TransactionException("Unable to commit, could not access journal file " + journalWrapper, ex); } } //commit all, and if any fail revert all. try { for (EngineBase e : tx.boundEngines) { if (log.isTraceEnabled()) log.trace(String.format("committing transaction %d to table %s", tx.id, e.getTableName())); e.commit(tx.getTransaction(), tx.getOptions()); } } catch (Exception ex) { try { //uncommit tx.commitId = -1; tx.revert(); } catch (XFlatException ex2) { throw new TransactionException( "Unable to commit, and another error occured during revert: " + ex2.getMessage(), ex); } //we were able to revert all, no need to keep the transaction in the journal. if (tx.options.isDurable()) { transactionJournal.getRootElement().removeContent(entryElement); try { journalWrapper.writeFile(transactionJournal); } catch (IOException ioEx) { //this is not the most important exception } } if (ex instanceof TransactionException) throw ex; throw new TransactionException("Unable to commit: " + ex.getMessage(), ex); } //remove it from the transaction journal if (tx.options.isDurable()) { transactionJournal.getRootElement().removeContent(entryElement); try { journalWrapper.writeFile(transactionJournal); } catch (IOException ex) { throw new TransactionException("Unable to commit, could not access journal file " + journalWrapper, ex); } } //we're all committed, so we can finally say so. committedTransactions.put(tx.id, tx); } private void revert(Iterable<EngineBase> boundEngines, long txId, boolean isRecovering) { Set<String> failedReverts = null; RuntimeException last = null; for (EngineBase e : boundEngines) { try { e.revert(txId, isRecovering); } catch (RuntimeException ex) { LogFactory.getLog(getClass()).error(ex); if (failedReverts == null) failedReverts = new HashSet<>(); failedReverts.add(e.getTableName()); last = ex; } } if (failedReverts != null && failedReverts.size() > 0) { StringBuilder msg = new StringBuilder( "Unable to revert all bound engines, the data in the following engines may be corrupt: "); for (String s : failedReverts) { msg.append(s).append(", "); } //the exceptions we caught were all runtime exceptions, so we are going to throw a runtime exception throw new XFlatException(msg.toString(), last); } } @Override public void close() { //all transactions auto-revert now. this.currentTransactions.clear(); } @Override public void recover(XFlatDatabase db) { //open the journal try { if (transactionJournal == null) { loadJournal(); } } catch (IOException | JDOMException ex) { throw new XFlatException("Unable to recover, could not access journal file " + journalWrapper, ex); } try { Iterator<Element> children = transactionJournal.getRootElement().getChildren().iterator(); while (children.hasNext()) { TransactionJournalEntry entry; try { entry = fromElement.convert(children.next()); } catch (ConversionException ex) { //entry is corrupt, remove and continue children.remove(); continue; } List<EngineBase> toRevert = new ArrayList<>(); for (String table : entry.tableNames) { toRevert.add(db.getEngine(table)); } //revert the transaction in all the engines revert(toRevert, entry.txId, true); //successful revert - remove the entry children.remove(); //save the journal after each successful revert this.journalWrapper.writeFile(transactionJournal); } } catch (XFlatException | IOException ex) { throw new XFlatException("Unable to recover", ex); } } @Override public boolean isCommitInProgress(long transactionId) { TransactionBase tx = this.currentTransactions.get(transactionId); if (tx == null) return false; //in-progress if the commit ID was assigned and the tx was not yet committed. return tx.commitId != -1 && !tx.isCommitted(); } /** * The base class for the different types of transactions handled by this * transaction manager. The different implementations are dependent on * the propagation level used when the transaction was opened. */ protected abstract class TransactionBase implements TransactionScope { protected TransactionOptions options; protected AtomicBoolean isCompleted = new AtomicBoolean(false); protected AtomicBoolean isRollbackOnly = new AtomicBoolean(false); protected volatile boolean isClosed = false; protected final long id; protected AmbientThreadedTransactionScope suspended; //we can get away with this being an unsynchronized HashSet because it will only ever be added to by one //thread, and then only so long as the transaction is open, and then will be removed from //by a different thread, but only one at a time, synchronized elsewhere, and after all adds are finished. final Set<EngineBase> boundEngines = new HashSet<>(); protected AtomicReference<Set<TransactionListener>> listeners = new AtomicReference<>(null); protected long commitId = -1; //The transaction representing this transaction scope. private final Transaction transaction = new Transaction() { @Override public long getTransactionId() { return id; } @Override public long getCommitId() { return commitId; } @Override public boolean isCommitted() { return TransactionBase.this.isCommitted(); } @Override public boolean isReverted() { return TransactionBase.this.isReverted(); } @Override public boolean isReadOnly() { return options.getReadOnly(); } }; public Transaction getTransaction() { return transaction; } protected TransactionBase(long id, AmbientThreadedTransactionScope suspended, TransactionOptions options) { this.id = id; this.suspended = suspended; this.options = options; if (this.options.getReadOnly()) { this.isRollbackOnly.set(true); } } protected void fireEvent(int event) { Set<TransactionListener> listeners = this.listeners.get(); if (listeners == null) return; TransactionEventObject evtObj = new TransactionEventObject(ThreadContextTransactionManager.this, this.transaction, event); synchronized (listeners) { for (Object l : listeners.toArray()) { ((TransactionListener) l).TransactionEvent(evtObj); } } } @Override public boolean isCommitted() { return this.isCompleted.get() && commitId > -1; } @Override public boolean isReverted() { return isCompleted.get() && commitId == -1; } @Override public void putTransactionListener(TransactionListener listener) { Set<TransactionListener> l = this.listeners.get(); if (l == null) { l = new HashSet<>(); if (!this.listeners.compareAndSet(null, l)) { l = this.listeners.get(); } } synchronized (l) { l.add(listener); } } @Override public void removeTransactionListener(TransactionListener listener) { Set<TransactionListener> l = this.listeners.get(); if (l == null) { return; } synchronized (l) { l.remove(listener); } } @Override public void setRevertOnly() { this.isRollbackOnly.set(true); } @Override public TransactionOptions getOptions() { return this.options; } @Override public void close() { if (suspended != null && !suspended.isClosed) { //need to put back the suspended transaction ThreadContextTransactionManager.this.currentTransactions.put(getContextId(), suspended); suspended = null; } this.isClosed = true; } } /** * A Transaction that is meant to exist within the context of one thread. * There should be no cross-thread transactional data access, only cross-thread * state querying. */ protected class AmbientThreadedTransactionScope extends TransactionBase { private List<WrappingTransactionScope> uncommittedScopes = new LinkedList<>(); private List<WrappingTransactionScope> wrappingScopes = new LinkedList<>(); protected AmbientThreadedTransactionScope(long id, AmbientThreadedTransactionScope suspended, TransactionOptions options) { super(id, suspended, options); } @Override public void commit() throws TransactionException { throw new UnsupportedOperationException("should not be called directly"); } private void doCommit() throws TransactionException { if (this.isRollbackOnly.get()) { throw new IllegalTransactionStateException("Cannot commit a rollback-only transaction"); } if (this.isCompleted.get()) { throw new IllegalTransactionStateException("Cannot commit a completed transaction"); } commitId = generateNewId(); ThreadContextTransactionManager.this.commit(this); //soon as commit returns, we are committed. this.isCompleted.set(true); fireEvent(TransactionEventObject.COMMITTED); } void completeWrappingScope(WrappingTransactionScope scope) throws TransactionException { if (uncommittedScopes.remove(scope) && uncommittedScopes.isEmpty()) { //all wrapping transaction scopes have completed, we can commit doCommit(); } //otherwise we do nothing, simply mark the wrapping scope as completed by removing it from the list. } void addWrappingScope(WrappingTransactionScope scope) { synchronized (this) { //add them at the beginning because the most recent ones //are most likely to close first. if (!scope.getOptions().getReadOnly()) { //only add it to uncommitted scopes if it can actually write. //ReadOnly scopes can close without commit, but an explicit revert //will still revert the entire ambient scope. uncommittedScopes.add(0, scope); } wrappingScopes.add(0, scope); } } @Override public void revert() { if (!isCompleted.compareAndSet(false, true)) { throw new IllegalTransactionStateException("Cannot rollback a completed transaction"); } if (!this.options.getReadOnly()) { doRevert(); } fireEvent(TransactionEventObject.REVERTED); } protected void doRevert() { ThreadContextTransactionManager.this.revert(this.boundEngines, this.id, false); } void closeWrappingScope(WrappingTransactionScope scope) { synchronized (this) { if (uncommittedScopes.remove(scope) && !this.isCompleted.get()) { //the scope was uncommitted, need to revert the transaction revert(); } if (wrappingScopes.remove(scope) && wrappingScopes.isEmpty()) { //all wrapping transaction scopes have closed, we can close. close(); } //otherwise, can't close yet, still have some open scopes. } } @Override public void close() { if (isCompleted.compareAndSet(false, true)) { //we completed in the close, need to revert. doRevert(); } //remove the transaction scope from the current transactions map Iterator<AmbientThreadedTransactionScope> it = currentTransactions.values().iterator(); while (it.hasNext()) { //Object equality because we don't know which if (it.next() == this) { it.remove(); break; } } super.close(); } } /** * A transaction that implements the {@link Propagation#NOT_SUPPORTED} behavior, * maintaining a reference to the suspended transaction so that it can be * replaced when this is closed. */ protected class NotSupportedTransaction extends TransactionBase { public NotSupportedTransaction(AmbientThreadedTransactionScope suspended, TransactionOptions options) { super(-1, suspended, options); } @Override public void commit() throws TransactionException { throw new IllegalTransactionStateException( "Cannot commit a transaction opened with propagation " + "NEVER or NOT_SUPPORTED"); } @Override public void revert() { throw new IllegalTransactionStateException( "Cannot revert a transaction opened with propagation " + "NEVER or NOT_SUPPORTED"); } } /** * A transaction object that represents no open transaction. This is * created by opening a transaction with the {@link Propagation#NEVER} or * with {@link Propagation#NOT_SUPPORTED} when the */ protected class EmptyTransactionScope implements TransactionScope { private TransactionOptions options; private volatile boolean isCommitted = false; private volatile boolean isReverted = false; private volatile boolean isClosed = false; public EmptyTransactionScope(TransactionOptions options) { this.options = options; } @Override public void commit() throws TransactionException { throw new IllegalTransactionStateException( "Cannot commit a transaction opened with propagation " + "NEVER or NOT_SUPPORTED"); } @Override public void revert() { throw new IllegalTransactionStateException( "Cannot revert a transaction opened with propagation " + "NEVER or NOT_SUPPORTED"); } @Override public void setRevertOnly() { } @Override public boolean isCommitted() { return isCommitted; } @Override public boolean isReverted() { return isReverted; } @Override public TransactionOptions getOptions() { return options; } @Override public void close() { //nothing to do isClosed = true; } @Override public void putTransactionListener(TransactionListener listener) { } @Override public void removeTransactionListener(TransactionListener listener) { } } /** * A TransactionScope object that provides a view onto the ambient scope. * There may be multiple wrapping transaction scopes all pointing to the * same ambient transaction scope. When ALL of these is committed, the * underlying ambient transaction is committed. */ protected class WrappingTransactionScope implements TransactionScope { private AmbientThreadedTransactionScope wrapped; private TransactionOptions options; protected WrappingTransactionScope(AmbientThreadedTransactionScope wrapped, boolean isReadOnly) { this.wrapped = wrapped; this.options = wrapped.getOptions().withReadOnly(isReadOnly); wrapped.addWrappingScope(this); } @Override public void commit() throws TransactionException { wrapped.completeWrappingScope(this); } @Override public void revert() { wrapped.revert(); } @Override public void setRevertOnly() { wrapped.setRevertOnly(); } @Override public boolean isCommitted() { return wrapped.isCommitted(); } @Override public boolean isReverted() { return wrapped.isReverted(); } @Override public TransactionOptions getOptions() { return this.options; } @Override public void close() { wrapped.closeWrappingScope(this); } @Override public void putTransactionListener(TransactionListener listener) { wrapped.putTransactionListener(listener); } @Override public void removeTransactionListener(TransactionListener listener) { wrapped.removeTransactionListener(listener); } } //<editor-fold desc="transaction journal"> private class TransactionJournalEntry { public long txId; public long commitId; public Set<String> tableNames = new HashSet<>(); } private Converter<TransactionJournalEntry, Element> toElement = new Converter<TransactionJournalEntry, Element>() { @Override public Element convert(TransactionJournalEntry source) throws ConversionException { Element ret = new Element("entry"); ret.setAttribute("txId", Long.toString(source.txId)); ret.setAttribute("commit", Long.toString(source.commitId)); for (String s : source.tableNames) { ret.addContent(new Element("table").setText(s)); } return ret; } }; private Converter<Element, TransactionJournalEntry> fromElement = new Converter<Element, TransactionJournalEntry>() { @Override public TransactionJournalEntry convert(Element source) throws ConversionException { TransactionJournalEntry ret = new TransactionJournalEntry(); try { String txId = source.getAttributeValue("txId"); if (txId == null) { throw new ConversionException("txId attribute required"); } ret.txId = Long.parseLong(txId); String commitId = source.getAttributeValue("commit"); if (commitId != null) { ret.commitId = Long.parseLong(commitId); } for (Element e : source.getChildren("table")) { ret.tableNames.add(e.getText()); } } catch (NumberFormatException ex) { throw new ConversionException("Conversion failure", ex); } return ret; } }; //</editor-fold> }