package org.corfudb.runtime.object.transactions;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.corfudb.protocols.logprotocol.ISMRConsumable;
import org.corfudb.protocols.logprotocol.SMREntry;
import org.corfudb.protocols.wireprotocol.ILogData;
import org.corfudb.protocols.wireprotocol.TxResolutionInfo;
import org.corfudb.runtime.exceptions.TransactionAbortedException;
import org.corfudb.runtime.object.ICorfuSMRAccess;
import org.corfudb.runtime.object.ICorfuSMRProxyInternal;
import org.corfudb.runtime.object.VersionLockedObject;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.IntStream;
import static org.corfudb.runtime.view.ObjectsView.TRANSACTION_STREAM_ID;
/** A Corfu optimistic transaction context.
*
* Optimistic transactions in Corfu provide the following isolation guarantees:
*
* (1) Read-your-own Writes:
* Reads in a transaction are guaranteed to observe a write in the same
* transaction, if a write happens before
* the read.
*
* (2) Opacity:
* Read in a transaction observe the state of the system ("snapshot") as of the time of the
* first read which occurs in the transaction ("first read
* timestamp"), except in case (1) above where they observe the own tranasction's writes.
*
* (3) Atomicity:
* Writes in a transaction are guaranteed to commit atomically,
* and commit if and only if none of the objects which were
* read (the "read set") were modified between the first read
* ("first read timestamp") and the time of commit.
*
* Created by mwei on 4/4/16.
*/
@Slf4j
public class OptimisticTransactionalContext extends AbstractTransactionalContext {
/** The proxies which were modified by this transaction. */
@Getter
private final Set<ICorfuSMRProxyInternal> modifiedProxies =
new HashSet<>();
OptimisticTransactionalContext(TransactionBuilder builder) {
super(builder);
}
/**
* Access within optimistic transactional context is implemented
* in via proxy.access() as follows:
*
* 1. First, we try to grab a read-lock on the proxy, and hope to "catch" the proxy in the
* snapshot version. If this succeeds, we invoke the corfu-object access method, and
* un-grab the read-lock.
*
* 2. Otherwise, we grab a write-lock on the proxy and bring it to the correct
* version
* - Inside proxy.setAsOptimisticStream, if there are currently optimistic
* updates on the proxy, we roll them back. Then, we set this
* transactional context as the proxy's new optimistic context.
* - Then, inside proxy.syncObjectUnsafe, depending on the proxy version,
* we may need to undo or redo committed changes, or apply forward committed changes.
*
* {@inheritDoc}
*/
@Override
public <R, T> R access(ICorfuSMRProxyInternal<T> proxy,
ICorfuSMRAccess<R, T> accessFunction,
Object[] conflictObject) {
log.debug("Access[{},{}] conflictObj={}", this, proxy, conflictObject);
// First, we add this access to the read set
addToReadSet(proxy, conflictObject);
// Next, we sync the object, which will bring the object
// to the correct version, reflecting any optimistic
// updates.
return proxy
.getUnderlyingObject()
.access(o -> (
getWriteSetEntrySize(proxy.getStreamID()) == 0 && // No updates
o.getVersionUnsafe() == getSnapshotTimestamp() && // And at the correct timestamp
(o.getOptimisticStreamUnsafe() == null ||
o.getOptimisticStreamUnsafe()
.isStreamCurrentContextThreadCurrentContext() )
),
o -> {
// Swap ourselves to be the active optimistic stream.
// Inside setAsOptimisticStream, if there are
// currently optimistic updates on the object, we
// roll them back. Then, we set this context as the
// object's new optimistic context.
setAsOptimisticStream(o);
// inside syncObjectUnsafe, depending on the object
// version, we may need to undo or redo
// committed changes, or apply forward committed changes.
o.syncObjectUnsafe(getSnapshotTimestamp());
},
o -> accessFunction.access(o)
);
}
/**
* if a Corfu object's method is an Accessor-Mutator, then although the mutation is delayed,
* it needs to obtain the result by invoking getUpcallResult() on the optimistic stream.
*
* This is similar to the second stage of access(), accept working on the optimistic stream instead of the
* underlying stream.- grabs the write-lock on the proxy.
* - uses proxy.setAsOptimisticStream in order to set itself as the proxy optimistic context,
* including rolling-back current optimistic changes, if any.
* - uses proxy.syncObjectUnsafe to bring the proxy to the desired version,
* which includes applying optimistic updates of the current
* transactional context.
*
* {@inheritDoc}
*/
@Override
public <T> Object getUpcallResult(ICorfuSMRProxyInternal<T> proxy,
long timestamp, Object[] conflictObject) {
// Getting an upcall result adds the object to the conflict set.
addToReadSet(proxy, conflictObject);
// if we have a result, return it.
SMREntry wrapper = getWriteSetEntryList(proxy.getStreamID()).get((int)timestamp);
if (wrapper != null && wrapper.isHaveUpcallResult()){
return wrapper.getUpcallResult();
}
// Otherwise, we need to sync the object
return proxy.getUnderlyingObject().update(o -> {
setAsOptimisticStream(o);
log.trace("Upcall[{}] {} Sync'd", this, timestamp);
o.syncObjectUnsafe(getSnapshotTimestamp());
SMREntry wrapper2 = getWriteSetEntryList(proxy.getStreamID()).get((int)timestamp);
if (wrapper2 != null && wrapper2.isHaveUpcallResult()){
return wrapper2.getUpcallResult();
}
// If we still don't have the upcall, this must be a bug.
throw new RuntimeException("Tried to get upcall during a transaction but" +
" we don't have it even after an optimistic sync (asked for " + timestamp +
" we have 0-" + (getWriteSetEntryList(proxy.getStreamID()).size() - 1) + ")");
});
}
/** Set the correct optimistic stream for this transaction (if not already).
*
* If the Optimistic stream doesn't reflect the current transaction context,
* we create the correct WriteSetSMRStream and pick the latest context as the
* current context.
* @param object Underlying object under transaction
* @param <T> Type of the underlying object
*/
<T> void setAsOptimisticStream(VersionLockedObject<T> object) {
if (object.getOptimisticStreamUnsafe() == null ||
!object.getOptimisticStreamUnsafe()
.isStreamCurrentContextThreadCurrentContext()) {
// We are setting the current context to the root context of nested transactions.
// Upon sync forward
// the stream will replay every entries from all parent transactional context.
WriteSetSMRStream newSMRStream =
new WriteSetSMRStream(TransactionalContext.getTransactionStackAsList(),
object.getID());
newSMRStream.currentContext = 0;
object.setOptimisticStreamUnsafe(newSMRStream);
}
}
/** Logs an update. In the case of an optimistic transaction, this update
* is logged to the write set for the transaction.
*
* Return the "address" of the update; used for retrieving results from operations via getUpcallRestult.
*
* @param proxy The proxy making the request.
* @param updateEntry The timestamp of the request.
* @param <T> The type of the proxy.
* @return The "address" that the update was written to.
*/
@Override
public <T> long logUpdate(ICorfuSMRProxyInternal<T> proxy,
SMREntry updateEntry,
Object[] conflictObjects) {
log.trace("LogUpdate[{},{}] {} ({}) conflictObj={}",
this, proxy, updateEntry.getSMRMethod(),
updateEntry.getSMRArguments(), conflictObjects);
return addToWriteSet(proxy, updateEntry, conflictObjects);
}
/**
* Commit a transaction into this transaction by merging the read/write
* sets.
*
* @param tc The transaction to merge.
*/
@SuppressWarnings("unchecked")
public void addTransaction(AbstractTransactionalContext tc) {
log.trace("Merge[{}] adding {}", this, tc);
// merge the conflict maps
mergeReadSetInto(tc.getReadSetInfo());
// merge the write-sets
mergeWriteSetInto(tc.getWriteSetInfo());
// "commit" the optimistic writes (for each proxy we touched)
// by updating the modifying context (as long as the context
// is still the same).
}
/** Commit the transaction. If it is the last transaction in the stack,
* append it to the log, otherwise merge it into a nested transaction.
*
* @return The address of the committed transaction.
* @throws TransactionAbortedException If the transaction was aborted.
*/
@Override
@SuppressWarnings("unchecked")
public long commitTransaction() throws TransactionAbortedException {
log.debug("TX[{}] request optimistic commit", this);
return getConflictSetAndCommit(() -> getReadSetInfo().getReadSetConflicts());
}
public long getConflictSetAndCommit(Supplier<Map<UUID, Set<Integer>>>
computeConflictSet) {
if (TransactionalContext.isInNestedTransaction()) {
getParentContext().addTransaction(this);
commitAddress = AbstractTransactionalContext.FOLDED_ADDRESS;
log.trace("Commit[{}] Folded into {}", this, getParentContext());
return commitAddress;
}
// If the write set is empty, we're done and just return
// NOWRITE_ADDRESS.
if (getWriteSetInfo().getWriteSet().getEntryMap().isEmpty()) {
log.trace("Commit[{}] Read-only commit (no write)", this);
return NOWRITE_ADDRESS;
}
// Write to the transaction stream if transaction logging is enabled
Set<UUID> affectedStreams = new HashSet<>(getWriteSetInfo().getWriteSet().getEntryMap().keySet());
if (this.builder.runtime.getObjectsView().isTransactionLogging()) {
affectedStreams.add(TRANSACTION_STREAM_ID);
}
// Now we obtain a conditional address from the sequencer.
// This step currently happens all at once, and we get an
// address of -1L if it is rejected.
long address = -1L;
try {
address = this.builder.runtime.getStreamsView()
.append(
// a set of stream-IDs that contains the affected streams
affectedStreams,
// a MultiObjectSMREntry that contains the update(s) to objects
collectWriteSetEntries(),
// TxResolution info:
// 1. snapshot timestamp
// 2. a map of conflict params, arranged by streamID's
// 3. a map of write conflict-params, arranged by
// streamID's
new TxResolutionInfo(getTransactionID(),
getSnapshotTimestamp(),
computeConflictSet.get(),
collectWriteConflictParams())
);
} catch (TransactionAbortedException ae) {
log.trace("Commit[{}] Rejected by sequencer", this);
abortTransaction(ae);
// rethrow the TXAbortedException
throw ae;
}
log.trace("Commit[{}] Acquire address {}", this, address);
super.commitTransaction();
commitAddress = address;
tryCommitAllProxies();
log.trace("Commit[{}] Written to {}", this, address);
return address;
}
/** Try to commit the optimistic updates to each proxy. */
protected void tryCommitAllProxies() {
// First, get the committed entry
// in order to get the backpointers
// and the underlying SMREntries.
ILogData committedEntry = this.builder.getRuntime()
.getAddressSpaceView().read(commitAddress);
updateAllProxies(x -> {
log.trace("Commit[{}] Committing {}", this, x);
// Commit all the optimistic updates
x.getUnderlyingObject().optimisticCommitUnsafe();
// If some other client updated this object, sync
// it forward to grab those updates
x.getUnderlyingObject().syncObjectUnsafe(
commitAddress-1);
// Also, be nice and transfer the undo
// log from the optimistic updates
// for this to work the write sets better
// be the same
List<SMREntry> committedWrites =
getWriteSetEntryList(x.getStreamID());
List<SMREntry> entryWrites =
((ISMRConsumable) committedEntry
.getPayload(this.getBuilder().runtime))
.getSMRUpdates(x.getStreamID());
if (committedWrites.size() ==
entryWrites.size()) {
IntStream.range(0, committedWrites.size())
.forEach(i -> {
if (committedWrites.get(i)
.isUndoable()) {
entryWrites.get(i)
.setUndoRecord(committedWrites.get(i)
.getUndoRecord());
}
});
}
// and move the stream pointer to "skip" this commit entry
x.getUnderlyingObject().seek(commitAddress + 1);
log.trace("Commit[{}] Committed {}", this, x);
});
}
@SuppressWarnings("unchecked")
protected void updateAllProxies(Consumer<ICorfuSMRProxyInternal> function) {
getModifiedProxies().forEach(x -> {
// If we are on the same thread, this will hold true.
if (x.getUnderlyingObject()
.optimisticallyOwnedByThreadUnsafe()) {
x.getUnderlyingObject().update(o -> {
// Make sure we're still the modifying thread
// even after getting the lock.
if (x.getUnderlyingObject()
.optimisticallyOwnedByThreadUnsafe()) {
function.accept(x);
}
return null;
});
}
});
}
/** Get the root context (the first context of a nested txn)
* which must be an optimistic transactional context.
* @return The root context.
*/
private OptimisticTransactionalContext getRootContext() {
AbstractTransactionalContext atc = TransactionalContext.getRootContext();
if (atc != null && !(atc instanceof OptimisticTransactionalContext)) {
throw new RuntimeException("Attempted to nest two different transactional context types");
}
return (OptimisticTransactionalContext)atc;
}
/**
* Get the first timestamp for this transaction.
*
* @return The first timestamp to be used for this transaction.
*/
@Override
public synchronized long obtainSnapshotTimestamp() {
final AbstractTransactionalContext atc = getRootContext();
if (atc != null && atc != this) {
// If we're in a nested transaction, the first read timestamp
// needs to come from the root.
return atc.getSnapshotTimestamp();
} else {
// Otherwise, fetch a read token from the sequencer the linearize
// ourselves against.
long currentTail = builder.runtime
.getSequencerView().nextToken(Collections.emptySet(), 0).getToken().getTokenValue();
log.trace("SnapshotTimestamp[{}] {}", this, currentTail);
return currentTail;
}
}
}