/** * Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.engine.exec; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.opengamma.engine.calcnode.CalculationJob; import com.opengamma.engine.calcnode.CalculationJobResult; import com.opengamma.engine.calcnode.CalculationJobSpecification; import com.opengamma.engine.calcnode.JobResultReceiver; import com.opengamma.engine.exec.plan.ExecutingGraph; import com.opengamma.engine.exec.plan.GraphExecutionPlan; import com.opengamma.engine.exec.stats.GraphExecutorStatisticsGatherer; import com.opengamma.engine.view.cycle.SingleComputationCycle; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.async.Cancelable; /** * Executes a {@link GraphExecutionPlan} by forming jobs and submitting them to the available calculation nodes. */ public class PlanExecutor implements JobResultReceiver, Cancelable, DependencyGraphExecutionFuture { private static final Logger s_logger = LoggerFactory.getLogger(PlanExecutor.class); private static final class ExecutingJob implements Cancelable { private final CalculationJob _job; private volatile Cancelable _cancel; public ExecutingJob(final CalculationJob job) { _job = job; } public CalculationJob getJob() { return _job; } public void setCancel(final Cancelable cancel) { _cancel = cancel; } private Cancelable getCancel() { return _cancel; } // Cancelable @Override public boolean cancel(final boolean mayInterruptedIfRunning) { Cancelable cancel = getCancel(); if (cancel != null) { s_logger.debug("Cancelling {} for job {}", _cancel, _job); return cancel.cancel(mayInterruptedIfRunning); } else { return false; } } } private enum State { NOT_STARTED, EXECUTING, CANCELLED, FINISHED; } private final SingleComputationCycle _cycle; private final ExecutingGraph _graph; private final AtomicInteger _notifyLock = new AtomicInteger(); private Map<CalculationJobSpecification, ExecutingJob> _executing = new HashMap<CalculationJobSpecification, ExecutingJob>(); private State _state; private int _nodeCount; private long _executionTime; private long _startTime; private Listener _listener; public PlanExecutor(final SingleComputationCycle cycle, final GraphExecutionPlan plan) { ArgumentChecker.notNull(cycle, "cycle"); ArgumentChecker.notNull(plan, "plan"); _cycle = cycle; _graph = plan.createExecution(cycle.getUniqueId(), cycle.getValuationTime(), cycle.getVersionCorrection()); _state = State.NOT_STARTED; plan.reportStatistics(getStatisticsGatherer()); } protected SingleComputationCycle getCycle() { return _cycle; } protected ExecutingGraph getGraph() { return _graph; } protected GraphExecutorStatisticsGatherer getStatisticsGatherer() { return getCycle().getViewProcessContext().getGraphExecutorStatisticsGathererProvider().getStatisticsGatherer(getCycle().getViewProcessId()); } protected void storeTailJobs(final CalculationJob job) { for (CalculationJob tail : job.getTail()) { _executing.put(tail.getSpecification(), new ExecutingJob(tail)); if (tail.getTail() != null) { storeTailJobs(tail); } } } protected void cancelableTailJobs(final CalculationJob job, final Cancelable handle) { for (CalculationJob tail : job.getTail()) { final ExecutingJob executing = _executing.get(tail.getSpecification()); if (executing != null) { executing.setCancel(handle); } if (tail.getTail() != null) { cancelableTailJobs(tail, handle); } } } protected void submit(final CalculationJob job) { final ExecutingJob executing; synchronized (this) { if (_executing == null) { // Already complete or cancelled; don't submit anything new s_logger.debug("Not submitting {} - already completed or cancelled", job); return; } s_logger.debug("Submitting {}", job); executing = new ExecutingJob(job); _executing.put(job.getSpecification(), executing); if (job.getTail() != null) { storeTailJobs(job); } } final Cancelable handle = getCycle().getViewProcessContext().getComputationJobDispatcher().dispatchJob(job, this); executing.setCancel(handle); synchronized (this) { if (_executing == null) { // Completed or cancelled during the submission handle.cancel(true); return; } if (job.getTail() != null) { cancelableTailJobs(job, handle); } } } protected void submitExecutableJobs() { CalculationJob nextJob = getGraph().nextExecutableJob(); int count = 0; while (nextJob != null) { submit(nextJob); nextJob = getGraph().nextExecutableJob(); count++; } s_logger.info("Submitted {} executable jobs for {}", count, this); } public void start() { synchronized (this) { if (_state != State.NOT_STARTED) { s_logger.error("Already started executing {}", this); throw new IllegalStateException(_state.toString()); } _state = State.EXECUTING; _startTime = System.nanoTime(); } if (getGraph().isFinished()) { s_logger.info("Execution plan {} is empty", this); notifyComplete(); } else { s_logger.info("Starting executing {}", this); submitExecutableJobs(); } } protected long notifyComplete() { final long startTime; final Listener listener; synchronized (this) { if (_executing != null) { s_logger.info("Finished executing {}", this); _state = State.FINISHED; _executing = null; } else { s_logger.info("Already completed or cancelled execution of {}", this); } notifyAll(); startTime = _startTime; listener = _listener; _listener = null; } if (listener != null) { listener.graphCompleted(getGraph().getCalculationConfiguration()); } return System.nanoTime() - startTime; } // Cancelable @Override public boolean cancel(final boolean mayInterruptIfRunning) { final Collection<ExecutingJob> jobs; synchronized (this) { if (_executing == null) { // Already complete or cancelled s_logger.warn("Can't cancel - already completed or previously cancelled execution of {}", this); return false; } jobs = _executing.values(); _executing = null; _state = State.CANCELLED; notifyAll(); } s_logger.info("Cancelling current jobs of {}", this); for (ExecutingJob job : jobs) { job.cancel(mayInterruptIfRunning); } return true; } // JobResultReceiver @Override public void resultReceived(final CalculationJobResult result) { final ExecutingJob job; synchronized (this) { if (_executing == null) { // Already cancelled (or complete) s_logger.debug("Ignoring result for already completed (or cancelled) {}", this); return; } job = _executing.remove(result.getSpecification()); if (job == null) { s_logger.warn("Unexpected (or duplicate completion of) {} for {}", result, this); return; } _nodeCount += result.getResultItems().size(); _executionTime += result.getDuration(); } final ExecutingGraph graph = getGraph(); _notifyLock.incrementAndGet(); graph.jobCompleted(result.getSpecification()); s_logger.debug("{} completed for {}", result, this); submitExecutableJobs(); getCycle().jobCompleted(job.getJob(), result); if (_notifyLock.decrementAndGet() == 0) { if (graph.isFinished()) { // If the lock count is still 0, then we will notify completion. If another thread caused graph completion, and is still notifying // the job completion, the count will be positive. If another thread caused graph completion and got here before us then the count // will already be -1. if (_notifyLock.compareAndSet(0, -1)) { final long duration = notifyComplete(); getStatisticsGatherer().graphExecuted(graph.getCalculationConfiguration(), _nodeCount, _executionTime, duration); } } } } // Future @Override public synchronized boolean isCancelled() { return _state == State.CANCELLED; } @Override public boolean isDone() { return _executing == null; } @Override public synchronized String get() throws InterruptedException, ExecutionException { while (_executing != null) { s_logger.debug("Waiting for completion of {}", this); wait(); } if (_state == State.CANCELLED) { s_logger.info("Cancelled {}", this); throw new CancellationException(); } return getGraph().getCalculationConfiguration(); } @Override public synchronized String get(final long timeout, final TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { if (_executing != null) { s_logger.debug("Waiting for completion of {}", this); wait(unit.toMillis(timeout)); if (_executing != null) { s_logger.info("Timeout on {}", this); throw new TimeoutException(); } } if (_state == State.CANCELLED) { s_logger.warn("Cancelled {}", this); throw new CancellationException(); } return getGraph().getCalculationConfiguration(); } @Override public void setListener(final Listener listener) { synchronized (this) { _listener = listener; if (_executing != null) { return; } } listener.graphCompleted(_graph.getCalculationConfiguration()); } // Object @Override public String toString() { return getGraph().toString() + " for " + getCycle().toString(); } }