package com.linkedin.parseq;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.linkedin.parseq.function.Consumer3;
import com.linkedin.parseq.function.Function1;
import com.linkedin.parseq.internal.ArgumentUtil;
import com.linkedin.parseq.internal.Continuations;
import com.linkedin.parseq.promise.Promise;
import com.linkedin.parseq.promise.PromisePropagator;
import com.linkedin.parseq.promise.PromiseResolvedException;
import com.linkedin.parseq.promise.Promises;
import com.linkedin.parseq.promise.Settable;
import com.linkedin.parseq.promise.SettablePromise;
import com.linkedin.parseq.trace.Relationship;
import com.linkedin.parseq.trace.ResultType;
import com.linkedin.parseq.trace.ShallowTraceBuilder;
import com.linkedin.parseq.trace.TraceBuilder;
/**
* FusionTask implements optimization in case when multiple synchronous
* transformations are applied in sequence. Instead of rescheduling every transformation
* as a separate task it is faster to apply transformations in a chain.
*
* This is internal class and it should not be extended or used outside ParSeq.
* It may change or be removed at any time in a backwards incompatible way.
*
* @author Jaroslaw Odzga (jodzga@linkedin.com)
*
* @param <S> type of source
* @param <T> type of target
*/
class FusionTask<S, T> extends BaseTask<T> {
private static final Logger LOGGER = LoggerFactory.getLogger(FusionTask.class);
private static final Continuations CONTINUATIONS = new Continuations();
/* Encapsulates transformation from source to target, includes all predecessors' transformations, can't be null */
private final Consumer3<FusionTraceContext, Promise<S>, Settable<T>> _propagator;
/* Asynchronous task that will complete source promise, can be null in which case propagator must not depend on source */
private final Task<S> _asyncTask;
/* Trace builder for the predecessor, used for building trace relationships, can be null */
private final ShallowTraceBuilder _predecessorShallowTraceBuilder;
private FusionTask(final String desc, final Task<S> task, final PromisePropagator<S, T> propagator) {
super(desc, TaskType.FUSION.getName());
_propagator = completing(adaptToAcceptTraceContext(propagator));
_asyncTask = task;
_predecessorShallowTraceBuilder = null;
}
private <R> FusionTask(final String desc, final FusionTask<S, R> predecessor,
final PromisePropagator<R, T> propagator) {
super(desc, TaskType.FUSION.getName());
_asyncTask = predecessor._asyncTask;
_predecessorShallowTraceBuilder = predecessor.getShallowTraceBuilder();
_propagator = completing(compose(predecessor._propagator, adaptToAcceptTraceContext(propagator)));
}
/**
* Drops FusionTraceContext on the floor. This method adapts pure transformation to be used within FusionTask
* that needs additional information (FusionTraceContext) to build trace.
*/
private static <A, B> Consumer3<FusionTraceContext, Promise<A>, Settable<B>> adaptToAcceptTraceContext(
final PromisePropagator<A, B> propagator) {
return (traceContext, src, dest) -> propagator.accept(src, dest);
}
private void transitionDone(final FusionTraceContext traceContext) {
addRelationships(traceContext);
transitionPending();
transitionDone();
}
/**
* Has current task initiated propagation?
*/
private boolean isPropagationInitiator(final FusionTraceContext traceContext) {
return traceContext.getPropagationInitiator().getId().equals(getId());
}
/**
* Depending on the context, returns trace builder that will contain information about the
* transformation. This is used to avoid wrapping single transformation with "fused" parent.
*/
private ShallowTraceBuilder getEffectiveShallowTraceBuilder(final FusionTraceContext traceContext) {
if (isPropagationInitiator(traceContext)) {
return traceContext.getSurrogate();
} else {
return _shallowTraceBuilder;
}
}
private void addRelationships(final FusionTraceContext traceContext) {
final ShallowTraceBuilder effectoveShallowTraceBuilder = getEffectiveShallowTraceBuilder(traceContext);
TraceBuilder builder = getTraceBuilder();
builder.addRelationship(Relationship.PARENT_OF, traceContext.getParent().getShallowTraceBuilder(), effectoveShallowTraceBuilder);
if (_predecessorShallowTraceBuilder != null) {
builder.addRelationship(Relationship.SUCCESSOR_OF, effectoveShallowTraceBuilder, _predecessorShallowTraceBuilder);
}
}
private void addPotentialRelationships(final FusionTraceContext traceContext, final TraceBuilder builder) {
final ShallowTraceBuilder effectoveShallowTraceBuilder = getEffectiveShallowTraceBuilder(traceContext);
builder.addRelationship(Relationship.POTENTIAL_CHILD_OF, effectoveShallowTraceBuilder, traceContext.getParent().getShallowTraceBuilder());
if (_predecessorShallowTraceBuilder != null) {
builder.addRelationship(Relationship.POSSIBLE_SUCCESSOR_OF, effectoveShallowTraceBuilder, _predecessorShallowTraceBuilder);
}
}
/**
* Composes transformation with the transformation of the predecessor.
*/
private <R> Consumer3<FusionTraceContext, Promise<S>, Settable<T>> compose(
final Consumer3<FusionTraceContext, Promise<S>, Settable<R>> predecessor,
final Consumer3<FusionTraceContext, Promise<R>, Settable<T>> propagator) {
return (traceContext, src, dst) -> {
/*
* At this point we know that transformation chain's length > 1.
* This code is executed during task execution, not when task is constructed.
*/
traceContext.createSurrogate();
predecessor.accept(traceContext, src, new Settable<R>() {
@Override
public void done(R value) throws PromiseResolvedException {
try {
/* Track start time of the transformation. End time is tracked by closure created by completing() */
getEffectiveShallowTraceBuilder(traceContext).setStartNanos(System.nanoTime());
propagator.accept(traceContext, Promises.value(value), dst);
} catch (Exception e) {
/* This can only happen if there is an internal problem. Propagators should not throw any exceptions. */
LOGGER.error("ParSeq ingternal error. An exception was thrown by propagator", e);
}
}
@Override
public void fail(Throwable error) throws PromiseResolvedException {
try {
/* Track start time of the transformation. End time is tracked by closure created by completing() */
getEffectiveShallowTraceBuilder(traceContext).setStartNanos(System.nanoTime());
propagator.accept(traceContext, Promises.error(error), dst);
} catch (Exception e) {
/* This can only happen if there is an internal problem. Propagators should not throw any exceptions. */
LOGGER.error("ParSeq ingternal error. An exception was thrown by propagator.", e);
}
}
});
};
}
/**
* Adds closure to the propagator that maintains correct state of the task e.g. transitions between states,
* adds trace etc.
*/
private Consumer3<FusionTraceContext, Promise<S>, Settable<T>> completing(
final Consumer3<FusionTraceContext, Promise<S>, Settable<T>> propagator) {
return (traceContext, src, dest) -> {
final SettablePromise<T> settable = FusionTask.this.getSettableDelegate();
if (isPropagationInitiator(traceContext)) {
/* BaseTask's code handles completing the parent task
* we need to handle tracing of a surrogate task here
*/
propagator.accept(traceContext, src, new Settable<T>() {
@Override
public void done(T value) throws PromiseResolvedException {
final ShallowTraceBuilder shallowTraceBuilder = traceContext.getSurrogate();
if (shallowTraceBuilder != null) {
addRelationships(traceContext);
final long endNanos = System.nanoTime();
shallowTraceBuilder.setPendingNanos(endNanos);
shallowTraceBuilder.setEndNanos(endNanos);
final Function<T, String> traceValueProvider = _traceValueProvider;
shallowTraceBuilder.setResultType(ResultType.SUCCESS);
if (traceValueProvider != null) {
try {
shallowTraceBuilder.setValue(traceValueProvider.apply(value));
} catch (Exception e) {
shallowTraceBuilder.setValue(Exceptions.failureToString(e));
}
}
}
dest.done(value);
}
@Override
public void fail(Throwable error) throws PromiseResolvedException {
final ShallowTraceBuilder shallowTraceBuilder = traceContext.getSurrogate();
if (shallowTraceBuilder != null) {
addRelationships(traceContext);
final long endNanos = System.nanoTime();
shallowTraceBuilder.setPendingNanos(endNanos);
shallowTraceBuilder.setEndNanos(endNanos);
if (Exceptions.isEarlyFinish(error)) {
shallowTraceBuilder.setResultType(ResultType.EARLY_FINISH);
} else {
shallowTraceBuilder.setResultType(ResultType.ERROR);
shallowTraceBuilder.setValue(Exceptions.failureToString(error));
}
}
dest.fail(error);
}
});
} else if (transitionRun(traceContext.getParent().getTraceBuilder())) {
markTaskStarted();
//non-parent task executed for the first time
traceContext.getParent().getTaskLogger().logTaskStart(this);
Settable<T> next = new Settable<T>() {
@Override
public void done(final T value) throws PromiseResolvedException {
try {
transitionDone(traceContext);
final Function<T, String> traceValueProvider = _traceValueProvider;
_shallowTraceBuilder.setResultType(ResultType.SUCCESS);
if (traceValueProvider != null) {
try {
_shallowTraceBuilder.setValue(traceValueProvider.apply(value));
} catch (Exception e) {
_shallowTraceBuilder.setValue(Exceptions.failureToString(e));
}
}
settable.done(value);
traceContext.getParent().getTaskLogger().logTaskEnd(FusionTask.this, _traceValueProvider);
CONTINUATIONS.submit(() -> dest.done(value));
} catch (Exception e) {
CONTINUATIONS.submit(() -> dest.fail(e));
}
}
@Override
public void fail(final Throwable error) throws PromiseResolvedException {
try {
transitionDone(traceContext);
traceFailure(error);
settable.fail(error);
traceContext.getParent().getTaskLogger().logTaskEnd(FusionTask.this, _traceValueProvider);
CONTINUATIONS.submit(() -> dest.fail(error));
} catch (Exception e) {
CONTINUATIONS.submit(() -> dest.fail(e));
}
}
};
CONTINUATIONS.submit(() -> {
try {
propagator.accept(traceContext, src, next);
} catch (Exception e) {
/* This can only happen if there is an internal problem. Propagators should not throw any exceptions. */
LOGGER.error("ParSeq ingternal error. An exception was thrown by propagator.", e);
}
});
} else {
//non-parent tasks subsequent executions
addPotentialRelationships(traceContext, traceContext.getParent().getTraceBuilder());
Promises.propagateResult(settable, dest);
}
};
}
/**
* Create new FusionTask without any predecessors.
*/
public static <S, T> FusionTask<?, T> create(final String name, final PromisePropagator<S, T> propagator) {
return new FusionTask<S, T>(name, (Task<S>)null, propagator);
}
/**
* Create new FusionTask with an async predecessor.
*/
public static <S, T> FusionTask<?, T> create(final String name, final Task<S> task, final PromisePropagator<S, T> propagator) {
return new FusionTask<S, T>(name, task, propagator);
}
@Override
public <R> Task<R> apply(String desc, PromisePropagator<T, R> propagator) {
return new FusionTask<>(desc, this, propagator);
}
@Override
public Task<T> recoverWith(final String desc, final Function1<Throwable, Task<T>> func) {
ArgumentUtil.requireNotNull(func, "function");
final Task<T> that = this;
return Task.async(desc, context -> {
final SettablePromise<T> result = Promises.settable();
context.after(that).run(() -> {
if (that.isFailed()) {
if (!(Exceptions.isCancellation(that.getError()))) {
try {
Task<T> r = func.apply(that.getError());
if (r == null) {
throw new RuntimeException(desc + " returned null");
} else {
Promises.propagateResult(r, result);
return r;
}
} catch (Throwable t) {
result.fail(t);
return null;
}
} else {
result.fail(that.getError());
return null;
}
} else {
result.done(that.get());
return null;
}
} );
context.run(that);
return result;
});
}
private void propagate(final FusionTraceContext traceContext, final SettablePromise<T> result) {
try {
_propagator.accept(traceContext, _asyncTask, result);
} catch (Throwable t) {
result.fail(t);
}
}
@Override
protected Promise<? extends T> run(final Context context) throws Throwable {
final SettablePromise<T> result = Promises.settable();
String baseName = getName();
if (_asyncTask == null) {
/* There is no async predecessor, run propagation immediately */
FusionTraceContext traceContext = new FusionTraceContext(context,
FusionTask.this.getShallowTraceBuilder(), baseName);
propagate(traceContext, result);
} else {
/* There is async predecessor, need to run it first
PropagationTask will actually run propagation */
final Task<T> propagationTask = Task.async(baseName, ctx -> {
final SettablePromise<T> fusionResult = Promises.settable();
FusionTraceContext traceContext = new FusionTraceContext(ctx, FusionTask.this.getShallowTraceBuilder(), baseName);
propagate(traceContext, fusionResult);
return fusionResult;
});
propagationTask.getShallowTraceBuilder()
.setHidden(_shallowTraceBuilder.getHidden())
.setSystemHidden(_shallowTraceBuilder.getSystemHidden());
_shallowTraceBuilder.setName("async fused");
_shallowTraceBuilder.setSystemHidden(true);
context.after(_asyncTask).run(propagationTask);
context.run(_asyncTask);
Promises.propagateResult(propagationTask, result);
}
return result;
}
}