package edu.brown.hstore.callbacks;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.log4j.Logger;
import edu.brown.hstore.HStoreSite;
import edu.brown.hstore.Hstoreservice.Status;
import edu.brown.hstore.conf.HStoreConf;
import edu.brown.hstore.txns.AbstractTransaction;
import edu.brown.hstore.txns.LocalTransaction;
import edu.brown.logging.LoggerUtil;
import edu.brown.logging.LoggerUtil.LoggerBoolean;
import edu.brown.utils.ClassUtil;
import edu.brown.utils.PartitionSet;
import edu.brown.utils.StringUtil;
public abstract class PartitionCountingCallback<X extends AbstractTransaction> implements TransactionCallback {
private static final Logger LOG = Logger.getLogger(PartitionCountingCallback.class);
private static final LoggerBoolean debug = new LoggerBoolean();
private static final LoggerBoolean trace = new LoggerBoolean();
static {
LoggerUtil.attachObserver(LOG, debug, trace);
}
// ----------------------------------------------------------------------------
// INTERNAL DATA MEMBERS
// ----------------------------------------------------------------------------
protected final HStoreSite hstore_site;
protected final HStoreConf hstore_conf;
private int counter = 0;
private final PartitionSet partitions = new PartitionSet();
private final PartitionSet receivedPartitions = new PartitionSet();
/**
* The current transaction handle that this callback is assigned to
*/
protected X ts;
/**
* This flag is set to true when the unblockCallback() is invoked
*/
private final AtomicBoolean unblockInvoked = new AtomicBoolean(false);
/**
* This flag is set to true after the unblockCallback() invocation is finished
* This prevents somebody from checking whether we have invoked the unblock callback
* but are still in the middle of processing it.
*/
private boolean unblockFinished = false;
/**
* We'll flip this flag if one of our partitions replies with an
* unexpected abort. This ensures that we only send out the ABORT
* to all the HStoreSites once.
*/
private final AtomicBoolean abortInvoked = new AtomicBoolean(false);
/**
* This flag is set to true after the abortCallback() invocation is finished
* This prevents somebody from checking whether we have invoked the abort callback
* but are still in the middle of processing it.
*/
private boolean abortFinished = false;
/**
* This flag is set to true if the callback has been cancelled
*/
private boolean canceled = false;
// We retain the original parameters of the last init()
private Long orig_txn_id = null;
private int orig_counter;
private final PartitionSet runPartitions = new PartitionSet();
private final PartitionSet abortPartitions = new PartitionSet();
// ----------------------------------------------------------------------------
// CONSTRUCTOR
// ----------------------------------------------------------------------------
/**
* Constructor
* If invoke_even_if_aborted set to true, then this callback will still execute
* the unblockCallback() method after all the responses have arrived.
* @param invoke_even_if_aborted
*/
protected PartitionCountingCallback(HStoreSite hstore_site) {
this.hstore_site = hstore_site;
this.hstore_conf = hstore_site.getHStoreConf();
}
// ----------------------------------------------------------------------------
// INITIALIZATION
// ----------------------------------------------------------------------------
/**
* Initialize the BlockingCallback's counter and transaction info
* @param ts - The transaction handle for this callback
* @param partitions - The partitions that we expected to get notifications for
*/
protected void init(X ts, PartitionSet partitions) {
if (debug.val)
LOG.debug(String.format("%s - Initialized %s with partitions %s [counter=%d, hashCode=%d]",
ts, this.getClass().getSimpleName(), partitions, partitions.size(), this.hashCode()));
int counter_val = partitions.size();
this.orig_counter = counter_val;
this.counter = counter_val;
this.partitions.addAll(partitions);
this.ts = ts;
this.orig_txn_id = this.ts.getTransactionId();
}
@Override
public boolean isInitialized() {
return (this.ts != null);
}
// ----------------------------------------------------------------------------
// FINISH
// ----------------------------------------------------------------------------
@Override
public final void finish() {
if (debug.val)
LOG.debug(String.format("%s - Finishing %s [hashCode=%d]",
this.ts, this.getClass().getSimpleName(), this.hashCode()));
this.abortInvoked.lazySet(false);
this.abortFinished = false;
this.unblockInvoked.lazySet(false);
this.unblockFinished = false;
this.canceled = false;
this.partitions.clear();
this.receivedPartitions.clear();
this.counter = 0;
this.orig_counter = 0;
this.ts = null;
this.runPartitions.clear();
this.abortPartitions.clear();
}
// ----------------------------------------------------------------------------
// UTILITY METHODS
// ----------------------------------------------------------------------------
/**
* Returns true if either the unblock or abort callbacks have been invoked
* and have finished their processing
*/
public synchronized final boolean allCallbacksFinished() {
if (this.canceled == false && this.orig_counter > 0) {
if (this.counter != 0) return (false);
if (this.unblockFinished || this.abortFinished) return (true);
return ((this.unblockInvoked.get() && this.unblockFinished) ||
(this.abortInvoked.get() && this.abortFinished));
}
return (true);
}
protected final X getTransaction() {
return (this.ts);
}
/**
* Return the current state of this callback's internal counter
*/
protected synchronized final int getCounter() {
return (this.counter);
}
/**
* Return all of the partitions involved in this callback.
*/
public PartitionSet getPartitions() {
return (this.partitions);
}
/**
* Return all of the partitions that this callback has received.
*/
public PartitionSet getReceivedPartitions() {
return (this.receivedPartitions);
}
/**
* Tell the HStoreCoordinator to invoke the TransactionFinish process
* @param status
*/
protected final void finishTransaction(Status status) {
assert(this.ts != null) :
"Null transaction handle for txn #" + this.orig_txn_id;
if (debug.val)
LOG.debug(String.format("%s - Invoking TransactionFinish protocol from %s [status=%s]\n%s",
this.ts, this.getClass().getSimpleName(), status,
StringUtil.join("\n", ClassUtil.getStackTrace())));
// Let everybody know that the party is over!
if (this.ts instanceof LocalTransaction) {
LocalTransaction local_ts = (LocalTransaction)this.ts;
LocalFinishCallback callback = ((LocalTransaction)this.ts).getFinishCallback();
callback.init(local_ts, status);
this.hstore_site.getCoordinator().transactionFinish(local_ts, status, callback);
}
}
protected final Long getOrigTransactionId() {
return (this.orig_txn_id);
}
protected final int getOrigCounter() {
return (this.orig_counter);
}
// ----------------------------------------------------------------------------
// RUN
// ----------------------------------------------------------------------------
public void run(int partition) {
// If this is the last result that we were waiting for, then we'll invoke
// the unblockCallback()
if (this.decrementCounter(partition)) {
this.unblock();
this.runPartitions.add(partition);
}
}
/**
* This allows you to decrement the counter without actually needing
* to create a ProtocolBuffer message.
* @param delta
* @return Returns the new value of the counter
*/
public synchronized final boolean decrementCounter(int partition) {
assert(this.partitions.contains(partition)) :
String.format("Trying to decrement for unexpected partition %d for %s [expected=%s]",
partition, this.ts, this.partitions);
if (this.receivedPartitions.contains(partition) == false) {
this.counter--;
this.receivedPartitions.add(partition);
if (debug.val)
LOG.debug(String.format("%s - %s.decrementCounter(partition=%d) / " +
"COUNTER: %d - %d = %d [origCtr=%d]%s",
this.ts, this.getClass().getSimpleName(), partition,
this.counter+1, 1, this.counter, this.orig_counter,
(trace.val ? "\n" + partition : "")));
assert(this.counter >= 0 && (this.counter == (this.partitions.size() - this.receivedPartitions.size()))) :
String.format("Invalid %s counter after decrementing for %s at partition %d\n%s",
this.getClass().getSimpleName(),
this.ts, partition, this);
return (this.counter == 0);
}
return (false);
}
// ----------------------------------------------------------------------------
// SUCCESSFUL UNBLOCKING
// ----------------------------------------------------------------------------
/**
* Internal method for calling the unblockCallback()
*/
private final void unblock() {
if (this.canceled == false && this.abortInvoked.get() == false) {
if (this.unblockInvoked.compareAndSet(false, true)) {
if (debug.val)
LOG.debug(String.format("%s - Invoking %s.unblockCallback() [hashCode=%d]",
this.ts, this.getClass().getSimpleName(), this.hashCode()));
this.unblockCallback();
this.unblockFinished = true;
} else {
String msg = String.format("%s - Tried to invoke %s.unblockCallback() twice [hashCode=%d]",
this.ts, this.getClass().getSimpleName(), this.hashCode());
throw new RuntimeException(msg);
}
}
}
public final boolean isUnblocked() {
return (this.unblockInvoked.get());
}
/**
* This method is invoked once all of the T messages are received
*/
protected abstract void unblockCallback();
// ----------------------------------------------------------------------------
// ABORT
// ----------------------------------------------------------------------------
@Override
public final void abort(int partition, Status status) {
assert(this.ts != null) :
String.format("Null transaction handle for txn #%s in %s [counter=%d/%d]\n%s",
this.orig_txn_id, this.getClass().getSimpleName(),
this.counter, this.orig_counter,
(this.orig_txn_id != null ? this.hstore_site.getTransaction(this.orig_txn_id) : null));
assert(this.ts.isInitialized()) :
String.format("Uninitialized transaction handle for txn #%s in %s [lastTxn=%s / origCounter=%d/%d]",
this.orig_txn_id, this.getClass().getSimpleName(),
this.counter, this.orig_counter);
// Always attempt to add the partition
this.decrementCounter(partition);
this.abortPartitions.add(partition);
// If this is the first response that told us to abort, then we'll
// send the abort message out
if (this.canceled == false && this.abortInvoked.compareAndSet(false, true)) {
if (this.unblockInvoked.get()) {
LOG.warn(String.format("%s - Trying to call %s.abortCallback() after having been unblocked!\n%s",
this.ts, this.getClass().getSimpleName(), this));
return;
}
if (debug.val)
LOG.debug(String.format("%s - Invoking %s.abortCallback() [partition=%d, status=%s]",
this.ts, this.getClass().getSimpleName(), partition, status));
try {
this.abortCallback(partition, status);
} catch (RuntimeException ex) {
String msg = String.format("Failed to invoke %s.abortCallback() for %s [partition=%d, status=%s]",
this.getClass().getSimpleName(), this.ts,
partition, status);
LOG.error(msg, ex);
throw ex;
}
// If we abort, then we have to send out an ABORT to
// all of the partitions that we originally sent INIT requests too
// Note that we do this *even* if we haven't heard back from the remote
// HStoreSite that they've acknowledged our transaction
// We don't care when we get the response for this
if (this.ts.isPredictSinglePartition() == false) {
if (this.ts instanceof LocalTransaction) {
this.finishTransaction(status);
} else {
// FIXME
}
} else {
this.hstore_site.queueDeleteTransaction(this.ts.getTransactionId(), status);
}
this.abortFinished = true;
}
}
@Override
public final boolean isAborted() {
return (this.abortInvoked.get());
}
/**
* The callback that is invoked when the first ABORT status arrives for this transaction
* This is guaranteed to be called only once per transaction in this method
*/
protected abstract void abortCallback(int partition, Status status);
// ----------------------------------------------------------------------------
// CANCEL
// ----------------------------------------------------------------------------
@Override
public void cancel() {
this.canceled = true;
}
@Override
public final boolean isCanceled() {
return (this.canceled);
}
// ----------------------------------------------------------------------------
// DEBUG METHODS
// ----------------------------------------------------------------------------
@Override
public String toString() {
String ret = String.format("[Invoked=%s/%s, Aborted=%s/%s, Canceled=%s, Counter=%d/%d] => Deletable=%s",
this.unblockInvoked.get(), this.unblockFinished,
this.abortInvoked.get(), this.abortFinished,
this.canceled,
this.counter, this.orig_counter,
this.allCallbacksFinished());
// if (debug.val) {
ret += String.format("\nReceivedPartitions=%s / AllPartitions=%s",
this.receivedPartitions, this.partitions);
ret += String.format("\nRunPartitions=%s / AbortPartitions=%s",
this.runPartitions, this.abortPartitions);
// }
return (ret);
}
}