package org.corfudb.runtime.object;
import io.netty.util.internal.ConcurrentSet;
import lombok.extern.slf4j.Slf4j;
import org.corfudb.protocols.logprotocol.SMREntry;
import org.corfudb.runtime.exceptions.NoRollbackException;
import org.corfudb.runtime.object.transactions.WriteSetSMRStream;
import org.corfudb.runtime.view.Address;
import org.corfudb.util.Utils;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.StampedLock;
import java.util.function.*;
/**
* The VersionLockedObject maintains a versioned object which is
* backed by an ISMRStream, and is optionally backed by an additional
* optimistic update stream.
*
* Users of the VersionLockedObject cannot access the versioned object
* directly, rather they use the access() and update() methods to
* read and manipulate the object.
*
* access() and update() allow the user to provide functions to execute
* under locks. These functions execute various "unsafe" methods provided
* by this object which inspect and manipulate the object state.
*
* syncObjectUnsafe() enables the user to bring the object to a given version,
* and the VersionLockedObject manages any sync or rollback of updates
* necessary.
*
* Created by mwei on 11/13/16.
*/
@Slf4j
public class VersionLockedObject<T> {
/**
* The actual underlying object.
*/
T object;
/** A list of upcalls pending in the system. The proxy keeps this
* set so it can remember to save the upcalls for pending requests.
*/
final Set<Long> pendingUpcalls;
// This enum is necessary because null cannot be inserted
// into a ConcurrentHashMap.
enum NullValue {
NULL_VALUE
}
/** A list of upcall results, keyed by the address they were
* requested.
*/
final Map<Long, Object> upcallResults;
/** A lock, which controls access to modifications to
* the object. Any access to unsafe methods should
* obtain the lock.
*/
private final StampedLock lock;
/** The stream view this object is backed by.
*
*/
private final ISMRStream smrStream;
/** The optimistic SMR stream on this object, if any
*
*/
private WriteSetSMRStream optimisticStream;
/** The upcall map for this object. */
private final Map<String, ICorfuSMRUpcallTarget<T>> upcallTargetMap;
/** The undo record function map for this object. */
private final Map<String, IUndoRecordFunction<T>> undoRecordFunctionMap;
/** The undo target map for this object. */
private final Map<String, IUndoFunction<T>> undoFunctionMap;
/** The reset set for this object. */
private final Set<String> resetSet;
/** A function that generates a new instance of this object. */
private final Supplier<T> newObjectFn;
public VersionLockedObject(Supplier<T> newObjectFn,
StreamViewSMRAdapter smrStream,
Map<String, ICorfuSMRUpcallTarget<T>> upcallTargets,
Map<String, IUndoRecordFunction<T>> undoRecordTargets,
Map<String, IUndoFunction<T>> undoTargets,
Set<String> resetSet)
{
this.smrStream = smrStream;
this.upcallTargetMap = upcallTargets;
this.undoRecordFunctionMap = undoRecordTargets;
this.undoFunctionMap = undoTargets;
this.resetSet = resetSet;
this.newObjectFn = newObjectFn;
this.object = newObjectFn.get();
this.pendingUpcalls = new ConcurrentSet<>();
this.upcallResults = new ConcurrentHashMap<>();
lock = new StampedLock();
}
/** Access the internal state of the object, trying first to optimistically access
* the object, then obtaining a write lock the optimistic access fails.
*
* If the directAccessCheckFunction returns true, then we execute the accessFunction
* without running updateFunction. If false, we execute the updateFunction to
* allow the user to modify the state of the object before calling accessFunction.
*
* directAccessCheckFunction is executed under an optimistic read lock. Read-only
* unsafe operations are permitted.
*
* updateFunction is executed under a write lock. Both read and write unsafe operations
* are permitted.
*
* accessFunction is accessed either under a read lock or write lock depending on
* whether an update was necessary or not.
*
* @param directAccessCheckFunction A function which returns True if the object can be
* accessed without being updated.
* @param updateFunction A function which is executed when direct access
* is not allowed and the object must be updated.
* @param accessFunction A function which allows the user to directly access
* the object while locked in the state enforced by
* either the directAccessCheckFunction or updateFunction.
* @param <R> The type of the access function return.
* @return
*/
public <R> R access(Function<VersionLockedObject<T>, Boolean> directAccessCheckFunction,
Consumer<VersionLockedObject<T>> updateFunction,
Function<T, R> accessFunction) {
// First, we try to do an optimistic read on the object, in case it
// meets the conditions for direct access.
long ts = lock.tryOptimisticRead();
if (ts != 0) {
if (directAccessCheckFunction.apply(this)) {
log.trace("Access [{}] Direct (optimistic-read) access at {}", this, getVersionUnsafe());
try {
R ret = accessFunction.apply(object);
if (lock.validate(ts)) {
return ret;
}
} catch (Exception e) {
// If we have an exception, we didn't get a chance to validate the the lock.
// If it's still valid, then we should re-throw the exception since it was
// on a correct view of the object.
if (lock.validate(ts)) {
throw e;
}
// Otherwise, it is not on a correct view of the object (the object was
// modified) and we should try again by upgrading the lock.
log.trace("Access [{}] Direct (optimistic-read) exception, upgrading lock", this);
}
}
}
// Next, we just upgrade to a full write lock if the optimistic
// read fails, since it means that the state of the object was
// updated.
try {
// Attempt an upgrade
ts = lock.tryConvertToWriteLock(ts);
// Upgrade failed, try conversion again
if (ts == 0) { ts = lock.writeLock(); }
// Check if direct access is possible (unlikely).
if (directAccessCheckFunction.apply(this)) {
log.trace("Access [{}] Direct (writelock) access at {}", this, getVersionUnsafe());
R ret = accessFunction.apply(object);
if (lock.validate(ts)) {
return ret;
}
}
// If not, perform the update operations
updateFunction.accept(this);
log.trace("Access [{}] Updated (writelock) access at {}", this, getVersionUnsafe());
return accessFunction.apply(object);
// And perform the access
} finally {
lock.unlock(ts);
}
}
/** Update the object under a write lock.
*
* @param updateFunction A function to execute once the write lock has been acquired.
* @param <R> The type of the return of the updateFunction.
* @return The return value of the update function.
*/
public <R> R update(Function<VersionLockedObject<T>, R> updateFunction) {
long ts = 0;
try {
ts = lock.writeLock();
log.trace("Update[{}] (writelock)", this);
return updateFunction.apply(this);
} finally {
lock.unlock(ts);
}
}
/** Roll the object back to the supplied version if possible.
* This function may roll back to a point prior to the requested version.
* Otherwise, throws a NoRollbackException.
*
* Unsafe, requires that the caller has acquired a write lock.
*
* @param rollbackVersion The version to rollback to.
* @throws NoRollbackException If the object cannot be rolled back to
* the supplied version.
*/
public void rollbackObjectUnsafe(long rollbackVersion) {
log.trace("Rollback[{}] to {}", this, rollbackVersion);
rollbackStreamUnsafe(smrStream, rollbackVersion);
log.trace("Rollback[{}] completed", this);
}
/** Move the pointer for this object (effectively, forcefuly
* change the version of this object without playing
* any updates).
* @param globalAddress The global address to set the pointer to
*/
public void seek(long globalAddress) {
smrStream.seek(globalAddress);
}
/** Bring the object to the requested version, rolling back or syncing
* the object from the log if necessary to reach the requested version.
*
* @param timestamp The timestamp to update the object to.
*/
public void syncObjectUnsafe(long timestamp) {
// If there is an optimistic stream attached,
// and it belongs to this thread use that
if (optimisticallyOwnedByThreadUnsafe()) {
// If there are no updates, ensure we are at the right snapshot
if (optimisticStream.pos() == Address.NEVER_READ) {
final WriteSetSMRStream currentOptimisticStream =
optimisticStream;
// If we are too far ahead, roll back to the past
if (getVersionUnsafe() > timestamp) {
try {
rollbackObjectUnsafe(timestamp);
} catch (NoRollbackException nre) {
resetUnsafe();
}
}
// Now sync the regular log
syncStreamUnsafe(smrStream, timestamp);
// It's possible that due to reset,
// the optimistic stream is no longer
// present. Restore it.
optimisticStream = currentOptimisticStream;
}
syncStreamUnsafe(optimisticStream, Address.OPTIMISTIC);
} else {
// If there is an optimistic stream for another
// transaction, remove it by rolling it back first
if (this.optimisticStream != null) {
optimisticRollbackUnsafe();
this.optimisticStream = null;
}
// If we are too far ahead, roll back to the past
if (getVersionUnsafe() > timestamp) {
try {
rollbackObjectUnsafe(timestamp);
// Rollback successfully got us to the right
// version, we're done.
if (getVersionUnsafe() == timestamp) {
return;
}
} catch (NoRollbackException nre) {
resetUnsafe();
}
}
syncStreamUnsafe(smrStream, timestamp);
}
}
/** Log an update to this object, noting a request to save the
* upcall result if necessary.
* @param entry The entry to log.
* @param saveUpcall True, if the upcall result should be
* saved, false otherwise.
* @return The address the update was logged at.
*/
public long logUpdate(SMREntry entry, boolean saveUpcall) {
return smrStream.append(entry, t -> {
if (saveUpcall) {
pendingUpcalls.add(t.getToken().getTokenValue());
}
return true;
}, t -> {
if (saveUpcall) {
pendingUpcalls.remove(t.getToken());
}
return true;
});
}
/** Get a handle to the optimistic stream. */
public WriteSetSMRStream getOptimisticStreamUnsafe() {
return optimisticStream;
}
/** Drop the optimistic stream, effectively making optimistic updates
* to this object permanent.
*/
public void optimisticCommitUnsafe() {
optimisticStream = null;
}
/** Check whether or not this object was modified by this thread.
*
* @return True, if the object was modified by this thread.
* False otherwise.
*/
public boolean optimisticallyOwnedByThreadUnsafe() {
return optimisticStream == null ? false : optimisticStream.isStreamForThisThread();
}
/** Set the optimistic stream for this thread, rolling back
* any previous threads if they were present.
* @param optimisticStream The new optimistic stream to install.
*/
public void setOptimisticStreamUnsafe(WriteSetSMRStream optimisticStream) {
if (this.optimisticStream != null) {
optimisticRollbackUnsafe();
}
this.optimisticStream = optimisticStream;
}
/** Get the version of this object. This corresponds to the position
* of the pointer into the SMR stream.
* @return
*/
public long getVersionUnsafe() {
return smrStream.pos();
}
/** Check whether this object is currently under optimistic modifications. */
public boolean isOptimisticallyModifiedUnsafe() {
return optimisticStream != null &&
optimisticStream.pos() != Address.NEVER_READ;
}
/** Reset this object to the uninitialized state. */
public void resetUnsafe() {
log.debug("Reset[{}]", this);
object = newObjectFn.get();
smrStream.reset();
optimisticStream = null;
}
/** Get the ID of the stream backing this object.
* @return The ID of the stream backing this object.
* */
public UUID getID() {
return smrStream.getID();
}
/** Generate the summary string for this version locked object.
*
* The format of this string is [type]@[version][+]
* (where + is the optimistic flag)
*
* @return The summary string for this version locked object
*/
@Override
public String toString() {
return object.getClass().getSimpleName() + "[" + Utils.toReadableID(smrStream.getID()) + "]@"
+ (getVersionUnsafe() == Address.NEVER_READ ? "NR" : getVersionUnsafe())
+ (optimisticStream == null ? "" : "+" + optimisticStream.pos());
}
/** Given a SMR entry with an undo record, undo the update.
*
* @param record The record to undo.
*/
protected void applyUndoRecordUnsafe(SMREntry record) {
log.trace("Undo[{}] of {}@{} ({})", this, record.getSMRMethod(),
record.getEntry() != null ? record.getEntry().getGlobalAddress() : "OPT",
record.getUndoRecord());
IUndoFunction<T> undoFunction =
undoFunctionMap.get(record.getSMRMethod());
// If the undo function exists, apply it.
if (undoFunction != null) {
undoFunction.doUndo(object, record.getUndoRecord(),
record.getSMRArguments());
return;
}
// If this is a reset, undo by restoring the
// previous state.
else if (resetSet.contains(record.getSMRMethod())) {
object = (T) record.getUndoRecord();
// clear the undo record, since it is now
// consumed (the object may change)
record.clearUndoRecord();
return;
}
// Otherwise we don't know how to undo,
// throw a runtime exception, because
// this is a bug, undoRecords we don't know
// how to process shouldn't be in the log.
throw new RuntimeException("Unknown undo record in undo log");
}
/** Apply an SMR update to the object, possibly optimistically.
* @param entry The entry to apply.
*/
protected Object applyUpdateUnsafe(SMREntry entry) {
log.trace("Apply[{}] of {}@{} ({})", this, entry.getSMRMethod(),
entry.getEntry() != null ? entry.getEntry().getGlobalAddress() : "OPT",
entry.getSMRArguments());
ICorfuSMRUpcallTarget<T> target = upcallTargetMap.get(entry.getSMRMethod());
if (target == null) {
throw new RuntimeException("Unknown upcall " + entry.getSMRMethod());
}
// No undo record is present
// -OR- there this is an optimistic entry, calculate
// an undo record.
// (in the case of optimistic entries, the snapshot
// may have changed since the last time they were
// applied, so we need to recalculate undo) --- this
// is the case without snapshot isolation
if (!entry.isUndoable() || entry.getEntry() == null) {
// Can we generate an undo record?
IUndoRecordFunction<T> undoRecordTarget =
undoRecordFunctionMap
.get(entry.getSMRMethod());
// If there was no previously calculated undo entry
if (undoRecordTarget != null){
// calculate the undo record
entry.setUndoRecord(undoRecordTarget
.getUndoRecord(object, entry.getSMRArguments()));
log.trace("Apply[{}] Undo->{}", this, entry.getUndoRecord());
} else if (resetSet.contains(entry.getSMRMethod())) {
// This entry actually resets the object. So here
// we can safely get a new instance, and add the
// previous instance to the undo log.
entry.setUndoRecord(object);
object = newObjectFn.get();
log.trace("Apply[{}] Undo->RESET", this);
}
}
// now invoke the upcall
Object ret = target.upcall(object, entry.getSMRArguments());
return ret;
}
/** Roll back the given stream by applying undo records in reverse order
* from the current stream position until rollbackVersion.
* @param stream The stream of SMR updates to apply in
* reverse order.
* @param rollbackVersion The version to stop roll back at.
* @throws NoRollbackException If an entry in the stream did not contain
* undo information.
*/
protected void rollbackStreamUnsafe(ISMRStream stream, long rollbackVersion) {
// If we're already at or before the given version, there's
// nothing to do
if (stream.pos() <= rollbackVersion) {
return;
}
List<SMREntry> entries = stream.current();
while(entries != null) {
if (entries.stream().allMatch(x -> x.isUndoable())) {
// start from the end, process one at a time
ListIterator<SMREntry> it =
entries.listIterator(entries.size());
while (it.hasPrevious()) {
applyUndoRecordUnsafe(it.previous());
}
}
else {
throw new NoRollbackException();
}
entries = stream.previous();
if (stream.pos() <= rollbackVersion) {
return;
}
}
throw new NoRollbackException();
}
/** Sync this stream by playing updates forward in the stream until
* the given timestamp. If Address.MAX is given, updates will be
* applied until the current tail of the stream. If Address.OPTIMISTIC
* is given, updates will be applied to the end of the stream, and
* upcall results will be stored in the resulting entries.
* @param stream The stream to sync forward
* @param timestamp The timestamp to sync up to.
*/
protected void syncStreamUnsafe(ISMRStream stream, long timestamp) {
log.trace("Sync[{}] {}", this, (timestamp == Address.OPTIMISTIC)
? "Optimistic" : "to " + timestamp);
long syncTo = (timestamp == Address.OPTIMISTIC) ? Address.MAX : timestamp;
stream.streamUpTo(syncTo)
.forEachOrdered(entry -> {
try {
Object res = applyUpdateUnsafe(entry);
if (timestamp == Address.OPTIMISTIC) {
entry.setUpcallResult(res);
}
else if (pendingUpcalls.contains(entry.getEntry().getGlobalAddress())) {
log.debug("Sync[{}] Upcall Result {}", entry.getEntry().getGlobalAddress());
upcallResults.put(entry.getEntry().getGlobalAddress(), res == null ?
NullValue.NULL_VALUE : res);
pendingUpcalls.remove(entry.getEntry().getGlobalAddress());
}
entry.setUpcallResult(res);
} catch (Exception e) {
log.error("Sync[{}] Error: Couldn't execute upcall due to {}", this, e);
throw new RuntimeException(e);
}
});
}
/** Roll back the optimistic stream, resetting the object if it can not
* be restored.
*/
protected void optimisticRollbackUnsafe() {
try {
log.trace("OptimisticRollback[{}] started", this);
rollbackStreamUnsafe(this.optimisticStream,
Address.NEVER_READ);
log.trace("OptimisticRollback[{}] complete", this);
} catch (NoRollbackException nre) {
log.debug("OptimisticRollback[{}] failed", this);
resetUnsafe();
}
}
}