package org.multiverse.commitbarriers; import org.multiverse.api.Txn; import org.multiverse.api.TxnStatus; import org.multiverse.api.exceptions.DeadTxnException; import org.multiverse.api.exceptions.PreparedTxnException; import org.multiverse.api.lifecycle.TxnEvent; import org.multiverse.api.lifecycle.TxnListener; import java.util.List; import static java.lang.String.format; /** * A synchronization aid that allows a set of threads and transaction to all wait for each other to reach a common * barrier point; once this barrier is opened, all transaction atomically commit. A CountDownCommitBarrier is useful * in programs involving a fixed sized party of threads/transactions that must occasionally wait for each other. * <p/> * The CountDownCommitBarrier looks a lot like the {@link java.util.concurrent.CountDownLatch}. So if you have * experience with that functionality, this one should feel familiar. * <p/> * A {@code CountDownCommitBarrier} is initialized with a given <em>count</em>. The * {@link #joinCommit(org.multiverse.api.Txn)} await} methods block until the current count reaches * zero due to invocations of the {@link #countDown} method, after which all waiting threads are released. Unlike the * CountDownLatch, it isn't allowed for a new transaction to call one of the join methods after the barrier has * aborted or committed. * <p/> * This functionality is useful for two phase commit related functionality. * <p/> * The CountDownCommitBarrier can't be reused, so it is not cyclic like the {@link java.util.concurrent.CyclicBarrier}. * <p/> * A CountDownCommitBarrier is thread-safe to use of course. * * @author Peter Veentjer. * @see VetoCommitBarrier */ public final class CountDownCommitBarrier extends CommitBarrier { private volatile int parties; /** * Create a new CountDownCommitBarrier that uses an unfair lock. * * @param parties the number of parties waiting. If the number of parties is 0, the VetoCommitBarrier is created * committed, else it will be closed. * @throws IllegalArgumentException if parties is smaller than 0. */ public CountDownCommitBarrier(int parties) { this(parties, false); } /** * Creates a new CountDownCommitBarrier. * * @param parties the number of parties waiting. If the number of parties is 0, the VetoCommitBarrier is created * committed, else it will be closed. * @param fair if the lock bu this CountDownCommitBarrier is fair. * @throws IllegalArgumentException if parties smaller than 0. */ public CountDownCommitBarrier(int parties, boolean fair) { super(parties == 0 ? Status.Committed : Status.Closed, fair); if (parties < 0) { throw new IllegalArgumentException(); } this.parties = parties; } /** * Returns the number of parties that want to join this CountDownCommitBarrier. * * @return the number of parties. */ public int getParties() { return parties; } protected boolean isLastParty() { return getNumberWaiting() == parties; } /** * Signal that one party has returned. If this is the last party to returned, all transactions * will commit. * <p/> * If the all parties already have returned, this call is ignored. This is the same behavior * as the {@link java.util.concurrent.CountDownLatch#countDown()} method provides. */ public void countDown() { List<Runnable> onCommitTasks = null; lock.lock(); try { switch (getStatus()) { case Closed: addJoiner(); if (isLastParty()) { onCommitTasks = signalCommit(); } break; case Aborted: break; case Committed: break; default: throw new IllegalStateException(); } } finally { lock.unlock(); } executeTasks(onCommitTasks); } /** * Adds 1 additional party to this CountDownCommitBarrier. * * @throws CommitBarrierOpenException if this CountDownCommitBarrier already is committed or aborted. * @see #atomicIncParties(int) */ public void atomicIncParties() { atomicIncParties(1); } /** * Atomically adds additional parties to this CountDownCommitBarrier. * <p/> * Call is ignored when extra is 0. * <p/> * This method is not transactional, so be very careful calling it within a transaction. Transactions can be * retried, so this method could be called more than once. This means that the number of added parties could * be completely bogus. For a transactional version see {@link #incParties(org.multiverse.api.Txn, int)}. * * @param extra the additional parties. * @throws IllegalArgumentException if extra smaller than 0. * @throws CommitBarrierOpenException if this CountDownCommitBarrier already is open. */ public void atomicIncParties(int extra) { if (extra < 0) { throw new IllegalArgumentException(); } lock.lock(); try { switch (getStatus()) { case Closed: if (extra == 0) { return; } parties += extra; break; case Aborted: String abortMsg = "Can't call countDown on already aborted CountDownCommitBarrier"; throw new CommitBarrierOpenException(abortMsg); case Committed: String commitMsg = "Can't call countDown on already committed CountDownCommitBarrier"; throw new CommitBarrierOpenException(commitMsg); default: throw new IllegalStateException(); } } finally { lock.unlock(); } } /** * Increases the number of parties that need to return before this CommitBarrier can open. * The parties are only increased after the transaction has committed. * <p/> * If extra is 0, this call is ignored (unless the Txn is not active, then a * IllegalTransactionState will be thrown. * <p/> * This is the call you want to use when you are doing an atomicIncParties inside a transaction. * A transaction can be retried multiple times, and if number of parties is incremented more than * once, you run into problems. That is why a transaction can be passed where a compensating * tasks is registered on, that removes the added parties when the transaction is aborted. * * @param tx the transaction where this operation lifts on. * @param extra the number of extra parties * @throws NullPointerException if tx is null. * @throws IllegalArgumentException is extra smaller than zero. * @throws org.multiverse.api.exceptions.IllegalTxnStateException * if the transaction is not in the correct * state for this operation (so not active). */ public void incParties(Txn tx, int extra) { if (tx == null) { throw new NullPointerException(); } if (tx.getStatus() != TxnStatus.Active) { if (tx.getStatus() == TxnStatus.Prepared) { tx.abort(); throw new PreparedTxnException( format("[%s] Can't call incParties on non active transaction because it is %s", tx.getConfig().getFamilyName(), tx.getStatus())); } else { throw new DeadTxnException( format("[%s] Can't call incParties on non active transaction because it is %s", tx.getConfig().getFamilyName(), tx.getStatus())); } } if (extra < 0) { throw new IllegalArgumentException(); } lock.lock(); try { switch (getStatus()) { case Closed: if (extra == 0) { return; } parties += extra; tx.register(new RestorePartiesCompensatingTask(extra)); break; case Aborted: String abortMsg = format("[%s] Can't call incParties on already aborted CountDownCommitBarrier", tx.getConfig().getFamilyName()); throw new CommitBarrierOpenException(abortMsg); case Committed: String commitMsg = format("[%s] Can't call incParties on already committed CountDownCommitBarrier", tx.getConfig().getFamilyName()); throw new CommitBarrierOpenException(commitMsg); default: throw new IllegalStateException(); } } finally { lock.unlock(); } } /** * A TransactionLifecycleListener that is responsible for restoring the the number of * parties after the transaction that increased them, was aborted. */ private class RestorePartiesCompensatingTask implements TxnListener { private final int extra; RestorePartiesCompensatingTask(int extra) { this.extra = extra; } @Override public void notify(Txn tx, TxnEvent event) { //todo: in the 0.6 release of multiverse this was pre-abort.. but since the pre-abort doesn't //exist anymore.. if (event != TxnEvent.PostAbort) { return; } List<Runnable> onCommitTasks = null; lock.lock(); try { if (getStatus() == Status.Closed) { parties -= extra; if (isLastParty()) { onCommitTasks = signalCommit(); } } } finally { lock.unlock(); } executeTasks(onCommitTasks); } } }