package com.linkedin.parseq.internal; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.ILoggerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.linkedin.parseq.Cancellable; import com.linkedin.parseq.DelayedExecutor; import com.linkedin.parseq.Engine; import com.linkedin.parseq.Task; import com.linkedin.parseq.trace.TraceBuilder; public class PlanContext { private static final Logger LOG = LoggerFactory.getLogger(PlanContext.class.getName()); /** Unique identifier for this plan. */ private final Long _id; /** The engine used to execute this plan. */ private final Engine _engine; private final String _planClass; private final Task<?> _root; /** * An executor that provides two guarantees: * * 1. Only one task is executed at a time * 2. The completion of a task happens-before the execution of the next task * * For more on the happens-before constraint see the java.util.concurrent * package documentation. */ private final SerialExecutor _taskExecutor; /** Scheduler for running time delayed tasks. */ private final DelayedExecutor _timerScheduler; private final TaskLogger _taskLogger; private final TraceBuilder _relationshipsBuilder; private final PlanCompletionListener _planCompletionListener; /** The number of uncompleted plans forked from this plan (including itself). */ private final AtomicInteger _pending; public PlanContext(final Engine engine, final Executor taskExecutor, final DelayedExecutor timerExecutor, final ILoggerFactory loggerFactory, final Logger allLogger, final Logger rootLogger, final String planClass, Task<?> root, final int maxRelationshipsPerTrace, final PlanDeactivationListener planDeactivationListener, PlanCompletionListener planCompletionListener, final SerialExecutor.TaskQueue<PrioritizableRunnable> taskQueue, final boolean drainSerialExecutorQueue) { _id = IdGenerator.getNextId(); _root = root; _relationshipsBuilder = new TraceBuilder(maxRelationshipsPerTrace, planClass, _id); _engine = engine; _taskExecutor = new SerialExecutor(taskExecutor, new CancellingPlanExceptionHandler(root), () -> { try { planDeactivationListener.onPlanDeactivated(PlanContext.this); } catch (Throwable t) { LOG.error("Failed to notify deactivation listener " + planDeactivationListener, t); } }, taskQueue, drainSerialExecutorQueue); _timerScheduler = timerExecutor; final Logger planLogger = loggerFactory.getLogger(Engine.LOGGER_BASE + ":planClass=" + planClass); _taskLogger = new TaskLogger(_id, root.getId(), allLogger, rootLogger, planLogger); _planClass = planClass; _planCompletionListener = planCompletionListener; _pending = new AtomicInteger(1); _root.addListener(p -> done()); } private PlanContext(Task<?> root, Long id, Engine engine, SerialExecutor serialExecutor, DelayedExecutor scheduler, String planClass, TaskLogger taskLogger, TraceBuilder relationshipsBuilder, PlanCompletionListener planCompletionListener) { _root = root; _id = id; _engine = engine; _taskExecutor = serialExecutor; _timerScheduler = scheduler; _planClass = planClass; _taskLogger = taskLogger; _relationshipsBuilder = relationshipsBuilder; _planCompletionListener = planCompletionListener; _pending = new AtomicInteger(1); _root.addListener(p -> done()); } public Long getId() { return _id; } public void execute(PrioritizableRunnable runnable) { _taskExecutor.execute(runnable); } public Cancellable schedule(long time, TimeUnit unit, Runnable runnable) { return _timerScheduler.schedule(time, unit, runnable); } public Object getEngineProperty(String key) { return _engine.getProperty(key); } public TaskLogger getTaskLogger() { return _taskLogger; } public TraceBuilder getRelationshipsBuilder() { return _relationshipsBuilder; } public String getPlanClass() { return _planClass; } public Task<?> getRootTask() { return _root; } /** * Creates a new {@link PlanContext} from the current plan for the given root * {@link Task}. The new plan can be completed independently from the current plan. * * @param root root task of the sub plan * @return a new PlanContext if this plan is not completed. Otherwise, returns null. */ public PlanContext fork(Task<?> root) { int pending; while ((pending = _pending.get()) > 0) { if (_pending.compareAndSet(pending, pending + 1)) { return new PlanContext(root, _id, _engine, _taskExecutor, _timerScheduler, _planClass, _taskLogger, _relationshipsBuilder, p -> done()); } } return null; } /** * Decrements the pending count by 1. Invokes {@link #_planCompletionListener} * if there is no more pending (sub-)plans. */ private void done() { if (_pending.decrementAndGet() == 0) { _planCompletionListener.onPlanCompleted(this); } } private static class CancellingPlanExceptionHandler implements UncaughtExceptionHandler { private final Task<?> _task; private CancellingPlanExceptionHandler(Task<?> task) { _task = task; } @Override public void uncaughtException(Throwable error) { final String msg = "Serial executor loop failed for plan: " + _task.getName(); final SerialExecutionException ex = new SerialExecutionException(msg, error); final boolean wasCancelled = _task.cancel(ex); LOG.error(msg + ". The plan was " + (wasCancelled ? "" : "not ") + "cancelled.", ex); } } }