package org.corfudb.runtime.object;
import com.codahale.metrics.Counter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.corfudb.protocols.logprotocol.SMREntry;
import org.corfudb.runtime.CorfuRuntime;
import org.corfudb.runtime.exceptions.TransactionAbortedException;
import org.corfudb.runtime.object.transactions.TransactionalContext;
import org.corfudb.util.MetricsUtils;
import org.corfudb.util.Utils;
import org.corfudb.util.serializer.ISerializer;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.function.Supplier;
import static java.lang.Long.min;
/**
* In the Corfu runtime, on top of a stream,
* an SMR object layer implements objects whose history of updates
* are backed by a stream.
*
* This class implements the methods that an in-memory corfu-object proxy carries
* in order to by in sync with a stream.
*
* We refer to the program's object as the -corfu object-,
* and to the internal object implementation as the -proxy-.
*
* If a Corfu object's method is an Accessor, it invokes the proxy's
* access() method.
*
* If a Corfu object's method is a Mutator or Accessor-Mutator, it invokes the
* proxy's logUpdate() method.
*
* Finally, if a Corfu object's method is an Accessor-Mutator,
* it obtains a result by invoking getUpcallResult().
*
* Created by mwei on 11/11/16.
*/
@Slf4j
public class CorfuCompileProxy<T> implements ICorfuSMRProxyInternal<T> {
/** The underlying object. This object stores the actual
state as well as the version of the object. It also
provides locks to access the object safely from a
multi-threaded context. */
@Getter
VersionLockedObject<T> underlyingObject;
/** The CorfuRuntime. This allows us to interact with the
* Corfu log.
*/
CorfuRuntime rt;
/** The ID of the stream of the log.
*/
UUID streamID;
/** The type of the underlying object. We use this to instantiate
* new instances of the underlying object.
*/
Class<T> type;
/** The serializer SMR entries will use to serialize their
* arguments.
*/
ISerializer serializer;
/** The arguments this proxy was created with.
*
*/
final Object[] args;
/**
* Metrics: meter (counter), histogram
*/
private MetricRegistry metrics = CorfuRuntime.getMetrics();
private String mpObj = CorfuRuntime.getMpObj();
private Timer timerAccess = metrics.timer(mpObj + "access");
private Timer timerLogWrite = metrics.timer(mpObj + "log-write");
private Timer timerTxn = metrics.timer(mpObj + "txn");
private Timer timerUpcall = metrics.timer(mpObj + "upcall");
private Counter counterAccessOptimistic = metrics.counter(mpObj + "access-optimistic");
private Counter counterAccessLocked = metrics.counter(mpObj + "access-locked");
private Counter counterTxnRetry1 = metrics.counter(mpObj + "txn-first-retry");
private Counter counterTxnRetryN = metrics.counter(mpObj + "txn-extra-retries");
public CorfuCompileProxy(CorfuRuntime rt, UUID streamID, Class<T> type, Object[] args,
ISerializer serializer,
Map<String, ICorfuSMRUpcallTarget<T>> upcallTargetMap,
Map<String, IUndoFunction<T>> undoTargetMap,
Map<String, IUndoRecordFunction<T>> undoRecordTargetMap,
Set<String> resetSet
) {
this.rt = rt;
this.streamID = streamID;
this.type = type;
this.args = args;
this.serializer = serializer;
underlyingObject = new VersionLockedObject<T>(this::getNewInstance,
new StreamViewSMRAdapter(rt, rt.getStreamsView().get(streamID)),
upcallTargetMap, undoRecordTargetMap,
undoTargetMap, resetSet);
}
/**
* {@inheritDoc}
*/
@Override
public <R> R access(ICorfuSMRAccess<R, T> accessMethod,
Object[] conflictObject) {
boolean isEnabled = MetricsUtils.isMetricsCollectionEnabled();
try (Timer.Context context = MetricsUtils.getConditionalContext(isEnabled, timerAccess)){
return accessInner(accessMethod, conflictObject, isEnabled);
}
}
private <R> R accessInner(ICorfuSMRAccess<R, T> accessMethod,
Object[] conflictObject, boolean isMetricsEnabled) {
if (TransactionalContext.isInTransaction()) {
return TransactionalContext.getCurrentContext()
.access(this, accessMethod, conflictObject);
}
// Linearize this read against a timestamp
final long timestamp =
rt.getSequencerView()
.nextToken(Collections.singleton(streamID), 0).getToken().getTokenValue();
log.debug("Access[{}] conflictObj={} version={}", this, conflictObject, timestamp);
// Perform underlying access
return underlyingObject.access(o -> o.getVersionUnsafe() >= timestamp && !o.isOptimisticallyModifiedUnsafe(),
o -> o.syncObjectUnsafe(timestamp),
o -> accessMethod.access(o));
}
/**
* {@inheritDoc}
*/
@Override
public long logUpdate(String smrUpdateFunction, final boolean keepUpcallResult,
Object[] conflictObject, Object... args) {
try (Timer.Context context = MetricsUtils.getConditionalContext(timerLogWrite)) {
return logUpdateInner(smrUpdateFunction, keepUpcallResult, conflictObject, args);
}
}
private long logUpdateInner(String smrUpdateFunction, final boolean keepUpcallResult,
Object[] conflictObject, Object... args) {
// If we aren't coming from a transactional context,
// redirect us to a transactional context first.
if (TransactionalContext.isInTransaction()) {
// We generate an entry to avoid exposing the serializer to the tx context.
SMREntry entry = new SMREntry(smrUpdateFunction, args, serializer);
return TransactionalContext.getCurrentContext()
.logUpdate(this, entry, conflictObject);
}
// If we aren't in a transaction, we can just write the modification.
// We need to add the acquired token into the pending upcall list.
SMREntry smrEntry = new SMREntry(smrUpdateFunction, args, serializer);
long address = underlyingObject.logUpdate(smrEntry, keepUpcallResult);
log.trace("Update[{}] {}@{} ({}) conflictObj={}",
this, smrUpdateFunction, address, args, conflictObject);
return address;
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public <R> R getUpcallResult(long timestamp, Object[] conflictObject) {
try (Timer.Context context = MetricsUtils.getConditionalContext(timerUpcall);) {
return getUpcallResultInner(timestamp, conflictObject);
}
}
/**
* {@inheritDoc}
*/
@Override
public void sync() {
// Linearize this read against a timestamp
final long timestamp =
rt.getSequencerView()
.nextToken(Collections.singleton(streamID), 0).getToken().getTokenValue();
log.debug("Sync[{}] {}", this, timestamp);
// Acquire locks and perform read.
underlyingObject.update(o -> {
o.syncObjectUnsafe(timestamp);
return null;
});
}
private <R> R getUpcallResultInner(long timestamp, Object[] conflictObject) {
// If we aren't coming from a transactional context,
// redirect us to a transactional context first.
if (TransactionalContext.isInTransaction()) {
return (R) TransactionalContext.getCurrentContext()
.getUpcallResult(this, timestamp, conflictObject);
}
// Check first if we have the upcall, if we do
// we can service the request right away.
if (underlyingObject.upcallResults.containsKey(timestamp)) {
log.trace("Upcall[{}] {} Direct", this, timestamp);
R ret = (R) underlyingObject.upcallResults.get(timestamp);
underlyingObject.upcallResults.remove(timestamp);
return ret == VersionLockedObject.NullValue.NULL_VALUE ? null : ret;
}
return underlyingObject.update(o-> {
o.syncObjectUnsafe(timestamp);
if (o.upcallResults.containsKey(timestamp)) {
log.trace("Upcall[{}] {} Sync'd", this, timestamp);
R ret = (R) o.upcallResults.get(timestamp);
o.upcallResults.remove(timestamp);
return ret == VersionLockedObject.NullValue.NULL_VALUE ? null : ret;
}
// The version is already ahead, but we don't have the result.
// The only way to get the correct result
// of the upcall would be to rollback. For now, we throw an exception
// since this is generally not expected. --- and probably a bug if it happens.
throw new RuntimeException("Attempted to get the result " +
"of an upcall@" + timestamp + " but we are @"
+ underlyingObject.getVersionUnsafe() +
" and we don't have a copy");
});
}
/**
* Get the ID of the stream this proxy is subscribed to.
*
* @return The UUID of the stream this proxy is subscribed to.
*/
@Override
public UUID getStreamID() {
return streamID;
}
/**
* Run in a transactional context.
*
* @param txFunction The function to run in a transactional context.
* @return The value supplied by the function.
*/
@Override
public <R> R TXExecute(Supplier<R> txFunction) {
boolean isEnabled = MetricsUtils.isMetricsCollectionEnabled();
try (Timer.Context context = MetricsUtils.getConditionalContext(isEnabled, timerTxn)) {
return TXExecuteInner(txFunction, isEnabled);
}
}
private <R> R TXExecuteInner(Supplier<R> txFunction, boolean isMetricsEnabled) {
long sleepTime = 1L;
final long maxSleepTime = 1000L;
int retries = 1;
while (true) {
try {
rt.getObjectsView().TXBegin();
R ret = txFunction.get();
rt.getObjectsView().TXEnd();
return ret;
} catch (TransactionAbortedException e) {
if (retries == 1) {
MetricsUtils.incConditionalCounter(isMetricsEnabled, counterTxnRetry1, 1);
}
MetricsUtils.incConditionalCounter(isMetricsEnabled, counterTxnRetryN, 1);
log.debug("Transactional function aborted due to {}, retrying after {} msec", e, sleepTime);
try {Thread.sleep(sleepTime); }
catch (Exception ex) {}
sleepTime = min(sleepTime * 2L, maxSleepTime);
retries++;
}
}
}
/**
* Get an object builder to build new objects.
*
* @return An object which permits the construction of new objects.
*/
@Override
public IObjectBuilder<?> getObjectBuilder() {
return rt.getObjectsView().build();
}
/**
* Return the type of the object being replicated.
*
* @return The type of the replicated object.
*/
@Override
public Class<T> getObjectType() {
return type;
}
/**
* Get the latest version read by the proxy.
*
* @return The latest version read by the proxy.
*/
@Override
public long getVersion() {
return underlyingObject.getVersionUnsafe();
}
/** Get a new instance of the real underlying object.
*
* @return An instance of the real underlying object
*/
@SuppressWarnings("unchecked")
private T getNewInstance() {
try {
T ret = null;
if (args == null || args.length == 0) {
ret = type.newInstance();
} else {
// This loop is not ideal, but the easiest way to get around Java boxing,
// which results in primitive constructors not matching.
for (Constructor<?> constructor : type.getDeclaredConstructors()) {
try {
ret = (T) constructor.newInstance(args);
break;
} catch (Exception e) {
// just keep trying until one works.
}
}
}
if (ret instanceof ICorfuSMRProxyWrapper) {
((ICorfuSMRProxyWrapper<T>) ret).setProxy$CORFUSMR(this);
}
return ret;
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public String toString() {
return type.getSimpleName() + "[" + Utils.toReadableID(streamID) + "]";
}
}