/*
* Copyright 2012 LinkedIn, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.linkedin.parseq;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.linkedin.parseq.internal.ArgumentUtil;
import com.linkedin.parseq.internal.ContextImpl;
import com.linkedin.parseq.internal.LIFOBiPriorityQueue;
import com.linkedin.parseq.internal.PlanCompletionListener;
import com.linkedin.parseq.internal.PlanDeactivationListener;
import com.linkedin.parseq.internal.SerialExecutor;
import com.linkedin.parseq.internal.SerialExecutor.TaskQueue;
import com.linkedin.parseq.internal.PlanContext;
/**
* An object that can run a set {@link Task}s. Use {@link EngineBuilder} to
* create Engine instances.
*
* @author Chris Pettitt (cpettitt@linkedin.com)
* @author Jaroslaw Odzga (jodzga@linkedin.com)
*/
public class Engine {
public static final String LOGGER_BASE = Engine.class.getName();
// TODO: this constant should be renamed.
// Need to fix in the next major version release.
public static final String MAX_RELATIONSHIPS_PER_TRACE = "_MaxRelationshipsPerTrace_";
private static final int DEFUALT_MAX_RELATIONSHIPS_PER_TRACE = 65536;
public static final String MAX_CONCURRENT_PLANS = "_MaxConcurrentPlans_";
private static final int DEFUALT_MAX_CONCURRENT_PLANS = Integer.MAX_VALUE;
public static final String DRAIN_SERIAL_EXECUTOR_QUEUE = "_DrainSerialExecutorQueue_";
private static final boolean DEFAULT_DRAIN_SERIAL_EXECUTOR_QUEUE = true;
public static final String DEFAULT_TASK_QUEUE = "_DefaultTaskQueue_";
private static final State INIT = new State(StateName.RUN, 0);
private static final State TERMINATED = new State(StateName.TERMINATED, 0);
private static final Logger LOG = LoggerFactory.getLogger(LOGGER_BASE);
private static enum StateName {
RUN,
SHUTDOWN,
TERMINATED
}
private final Executor _taskExecutor;
private final DelayedExecutor _timerExecutor;
private final ILoggerFactory _loggerFactory;
private final TaskQueueFactory _taskQueueFactory;
private final AtomicReference<State> _stateRef = new AtomicReference<State>(INIT);
private final CountDownLatch _terminated = new CountDownLatch(1);
private final Map<String, Object> _properties;
private final int _maxRelationshipsPerTrace;
private final int _maxConcurrentPlans;
private final Semaphore _concurrentPlans;
private final boolean _drainSerialExecutorQueue;
private final PlanDeactivationListener _planDeactivationListener;
private final PlanCompletionListener _planCompletionListener;
private final PlanCompletionListener _taskDoneListener;
// Cache these, since we'll use them frequently and they can be pre-computed.
private final Logger _allLogger;
private final Logger _rootLogger;
/* package private */ Engine(final Executor taskExecutor, final DelayedExecutor timerExecutor,
final ILoggerFactory loggerFactory, final Map<String, Object> properties,
final PlanDeactivationListener planActivityListener,
final PlanCompletionListener planCompletionListener,
final TaskQueueFactory taskQueueFactory) {
_taskExecutor = taskExecutor;
_timerExecutor = timerExecutor;
_loggerFactory = loggerFactory;
_properties = properties;
_planDeactivationListener = planActivityListener;
_taskQueueFactory = createTaskQueueFactory(properties, taskQueueFactory);
_allLogger = loggerFactory.getLogger(LOGGER_BASE + ":all");
_rootLogger = loggerFactory.getLogger(LOGGER_BASE + ":root");
if (_properties.containsKey(MAX_RELATIONSHIPS_PER_TRACE)) {
_maxRelationshipsPerTrace = (Integer) getProperty(MAX_RELATIONSHIPS_PER_TRACE);
} else {
_maxRelationshipsPerTrace = DEFUALT_MAX_RELATIONSHIPS_PER_TRACE;
}
if (_properties.containsKey(MAX_CONCURRENT_PLANS)) {
_maxConcurrentPlans = (Integer) getProperty(MAX_CONCURRENT_PLANS);
} else {
_maxConcurrentPlans = DEFUALT_MAX_CONCURRENT_PLANS;
}
_concurrentPlans = new Semaphore(_maxConcurrentPlans);
if (_properties.containsKey(DRAIN_SERIAL_EXECUTOR_QUEUE)) {
_drainSerialExecutorQueue = (Boolean) getProperty(DRAIN_SERIAL_EXECUTOR_QUEUE);
} else {
_drainSerialExecutorQueue = DEFAULT_DRAIN_SERIAL_EXECUTOR_QUEUE;
}
_taskDoneListener = resolvedPromise -> {
assert _stateRef.get()._pendingCount > 0;
assert _stateRef.get()._stateName != StateName.TERMINATED;
State currState;
State newState;
do {
currState = _stateRef.get();
newState = new State(currState._stateName, currState._pendingCount - 1);
} while (!_stateRef.compareAndSet(currState, newState));
_concurrentPlans.release();
if (newState._stateName == StateName.SHUTDOWN && newState._pendingCount == 0) {
tryTransitionTerminate();
}
};
_planCompletionListener = planContext -> {
try {
planCompletionListener.onPlanCompleted(planContext);
} catch (Throwable t) {
LOG.error("Uncaught throwable from custom PlanCompletionListener.", t);
} finally {
_taskDoneListener.onPlanCompleted(planContext);
}
};
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private TaskQueueFactory createTaskQueueFactory(final Map<String, Object> properties, final TaskQueueFactory taskQueueFactory) {
if (taskQueueFactory == null) {
if (_properties.containsKey(DEFAULT_TASK_QUEUE)) {
String className = (String) properties.get(DEFAULT_TASK_QUEUE);
try {
final Class<? extends SerialExecutor.TaskQueue> clazz =
(Class<? extends TaskQueue>) Thread.currentThread().getContextClassLoader().loadClass(className);
return () -> {
try {
return clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
return new LIFOBiPriorityQueue();
}
};
} catch (ClassNotFoundException e) {
LOG.error("Failed to load TasQueue implementation: " + className + ", will use default implementation", e);
}
}
return LIFOBiPriorityQueue::new;
} else {
return taskQueueFactory;
}
}
public Object getProperty(String key) {
return _properties.get(key);
}
private final String defaultPlanClass(final Task<?> task) {
return task.getClass().getName();
}
/**
* Runs the given task. Task passed in as a parameter becomes a root on a new Plan.
* All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
* <p>
* This method throws {@code IllegalStateException} if Engine does not have capacity to run the task.
* Engine's capacity is specified by a {@value #MAX_CONCURRENT_PLANS} configuration property. Use
* {@link EngineBuilder#setEngineProperty(String, Object)} to set this property.
* For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
* {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
*
* @param task the task to run
* @throws IllegalStateException
*/
public void run(final Task<?> task) {
run(task, defaultPlanClass(task));
}
/**
* Runs the given task. Task passed in as a parameter becomes a root on a new Plan.
* All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
* <p>
* This method throws {@code IllegalStateException} if Engine does not have capacity to run the task.
* Engine's capacity is specified by a {@value #MAX_CONCURRENT_PLANS} configuration property. Use
* {@link EngineBuilder#setEngineProperty(String, Object)} to set this property.
* For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
* {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
*
* @param task the task to run
* @param planClass string that identifies a "class" of the Plan. Plan class ends up in a ParSeq
* Trace and can be used to group traces into "classes" when traces are statistically analyzed.
* @throws IllegalStateException
*/
public void run(final Task<?> task, final String planClass) {
if (!tryRun(task, planClass)) {
throw new IllegalStateException("Starting new plan rejected, exceeded limit of concurrent plans: " + _maxConcurrentPlans);
}
}
/**
* Runs the given task. Task passed in as a parameter becomes a root on a new Plan.
* All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
* <p>
* This method blocks until Engine has a capacity to run the task. Engine's capacity is
* specified by a {@value #MAX_CONCURRENT_PLANS} configuration property. Use
* {@link EngineBuilder#setEngineProperty(String, Object)} to set this property.
* For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
* {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
*
* @param task the task to run
*/
public void blockingRun(final Task<?> task) {
blockingRun(task, defaultPlanClass(task));
}
/**
* Runs the given task. Task passed in as a parameter becomes a root on a new Plan.
* All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
* <p>
* This method blocks until Engine has a capacity to run the task. Engine's capacity is
* specified by a {@value #MAX_CONCURRENT_PLANS} configuration property. Use
* {@link EngineBuilder#setEngineProperty(String, Object)} to set this property.
* For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
* {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
*
* @param task the task to run
* @param planClass string that identifies a "class" of the Plan. Plan class ends up in a ParSeq
* Trace and can be used to group traces into "classes" when traces are statistically analyzed.
*/
public void blockingRun(final Task<?> task, final String planClass) {
try {
_concurrentPlans.acquire();
runWithPermit(task, planClass);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* Runs the given task if Engine has a capacity to start new plan as specified by
* {@value #MAX_CONCURRENT_PLANS} configuration parameter.
* For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
* {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
* Task passed in as a parameter becomes a root on a new Plan.
* All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
* This method returns immediately and does not block. It returns {@code true} if Plan was successfully started.
* @param task the task to run
* @return true if Plan was started
*/
public boolean tryRun(final Task<?> task) {
return tryRun(task, defaultPlanClass(task));
}
/**
* Runs the given task if Engine has a capacity to start new plan as specified by
* {@value #MAX_CONCURRENT_PLANS} configuration parameter.
* For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
* {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
* Task passed in as a parameter becomes a root on a new Plan.
* All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
* This method returns immediately and does not block. It returns {@code true} if Plan was successfully started.
* @param task the task to run
* @param planClass string that identifies a "class" of the Plan
* @return true if Plan was started
*/
public boolean tryRun(final Task<?> task, final String planClass) {
if (_concurrentPlans.tryAcquire()) {
runWithPermit(task, planClass);
return true;
} else {
return false;
}
}
/**
* Runs the given task if Engine has a capacity to start new plan as specified by
* {@value #MAX_CONCURRENT_PLANS} configuration parameter within specified amount of time.
* For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
* {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
* Task passed in as a parameter becomes a root on a new Plan.
* All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
* If Engine does not have capacity to start the task, this method will block up to specified amount of
* time waiting for other concurrently running Plans to complete. If there is no capacity to start the task
* within specified amount of time this method will return false. It returns {@code true} if Plan was successfully started.
* @param task the task to run
* @param timeout amount of time to wait for Engine's capacity to run the task
* @param unit
* @return true if Plan was started within the given waiting time and the current thread has not
* been {@linkplain Thread#interrupt interrupted}.
*/
public boolean tryRun(final Task<?> task, final long timeout, final TimeUnit unit) throws InterruptedException {
return tryRun(task, defaultPlanClass(task), timeout, unit);
}
/**
* Runs the given task if Engine has a capacity to start new plan as specified by
* {@value #MAX_CONCURRENT_PLANS} configuration parameter within specified amount of time.
* For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
* {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
* Task passed in as a parameter becomes a root on a new Plan.
* All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
* If Engine does not have capacity to start the task, this method will block up to specified amount of
* time waiting for other concurrently running Plans to complete. If there is no capacity to start the task
* within specified amount of time this method will return false. It returns {@code true} if Plan was successfully started.
* @param task the task to run
* @param planClass string that identifies a "class" of the Plan
* @param timeout amount of time to wait for Engine's capacity to run the task
* @param unit
* @return true if Plan was started within the given waiting time and the current thread has not
* been {@linkplain Thread#interrupt interrupted}.
*/
public boolean tryRun(final Task<?> task, final String planClass, final long timeout, final TimeUnit unit) throws InterruptedException {
if (_concurrentPlans.tryAcquire(timeout, unit)) {
runWithPermit(task, planClass);
return true;
} else {
return false;
}
}
/**
* Runs the given task with its own context. Use {@code Tasks.seq} and
* {@code Tasks.par} to create and run composite tasks.
*
* @param task the task to run
*/
private void runWithPermit(final Task<?> task, final String planClass) {
ArgumentUtil.requireNotNull(task, "task");
ArgumentUtil.requireNotNull(planClass, "planClass");
State currState, newState;
do {
currState = _stateRef.get();
if (currState._stateName != StateName.RUN) {
task.cancel(new EngineShutdownException("Task submitted after engine shutdown"));
return;
}
newState = new State(StateName.RUN, currState._pendingCount + 1);
} while (!_stateRef.compareAndSet(currState, newState));
PlanContext planContext = new PlanContext(this, _taskExecutor, _timerExecutor, _loggerFactory, _allLogger,
_rootLogger, planClass, task, _maxRelationshipsPerTrace, _planDeactivationListener, _planCompletionListener,
_taskQueueFactory.newTaskQueue(), _drainSerialExecutorQueue);
new ContextImpl(planContext, task).runTask();
}
/**
* If the engine is currently running, this method will initiate an orderly
* shutdown. No new tasks will be accepted, but already running tasks will be
* allowed to finish. Use {@link #awaitTermination(int, java.util.concurrent.TimeUnit)}
* to wait for the engine to shutdown.
* <p>
* If the engine is already shutting down or stopped this method will have
* no effect.
*/
public void shutdown() {
if (tryTransitionShutdown()) {
tryTransitionTerminate();
}
}
/**
* Returns {@code true} if engine shutdown has been started or if the engine
* is terminated. Use {@link #isTerminated()} to determine if the engine is
* actually stopped and {@link #awaitTermination(int, java.util.concurrent.TimeUnit)}
* to wait for the engine to stop.
*
* @return {@code true} if the engine has started shutting down or if it has
* finished shutting down.
*/
public boolean isShutdown() {
return _stateRef.get()._stateName != StateName.RUN;
}
/**
* Returns {@code true} if the engine has completely stopped. Use
* {@link #awaitTermination(int, java.util.concurrent.TimeUnit)} to wait for
* the engine to terminate. Use {@link #shutdown()} to start engine shutdown.
*
* @return {@code true} if the engine has completed stopped.
*/
public boolean isTerminated() {
return _stateRef.get()._stateName == StateName.TERMINATED;
}
/**
* Waits for the engine to stop. Use {@link #shutdown()} to initiate
* shutdown.
*
* @param time the amount of time to wait
* @param unit the unit for the time to wait
* @return {@code true} if shutdown completed within the specified time or
* {@code false} if not.
* @throws InterruptedException if this thread is interrupted while waiting
* for the engine to stop.
*/
public boolean awaitTermination(final int time, final TimeUnit unit) throws InterruptedException {
return _terminated.await(time, unit);
}
private boolean tryTransitionShutdown() {
State currState, newState;
do {
currState = _stateRef.get();
if (currState._stateName != StateName.RUN) {
return false;
}
newState = new State(StateName.SHUTDOWN, currState._pendingCount);
} while (!_stateRef.compareAndSet(currState, newState));
return true;
}
private void tryTransitionTerminate() {
State currState;
do {
currState = _stateRef.get();
if (currState._stateName != StateName.SHUTDOWN || currState._pendingCount != 0) {
return;
}
} while (!_stateRef.compareAndSet(currState, TERMINATED));
_terminated.countDown();
}
private static class State {
private final StateName _stateName;
private final long _pendingCount;
private State(final StateName stateName, final long pendingCount) {
_pendingCount = pendingCount;
_stateName = stateName;
}
}
}