org.multiverse.commitbarriers.CountDownCommitBarrier Maven / Gradle / Ivy
The newest version!
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.
*
* 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.
*
* A {@code CountDownCommitBarrier} is initialized with a given count. 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.
*
* This functionality is useful for two phase commit related functionality.
*
* The CountDownCommitBarrier can't be reused, so it is not cyclic like the {@link java.util.concurrent.CyclicBarrier}.
*
* 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.
*
* 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 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.
*
* Call is ignored when extra is 0.
*
* 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.
*
* If extra is 0, this call is ignored (unless the Txn is not active, then a
* IllegalTransactionState will be thrown.
*
* 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 onCommitTasks = null;
lock.lock();
try {
if (getStatus() == Status.Closed) {
parties -= extra;
if (isLastParty()) {
onCommitTasks = signalCommit();
}
}
} finally {
lock.unlock();
}
executeTasks(onCommitTasks);
}
}
}