/** * Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.engine.exec.plan; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.threeten.bp.Instant; import com.opengamma.engine.calcnode.CalculationJob; import com.opengamma.engine.calcnode.CalculationJobSpecification; import com.opengamma.engine.exec.JobIdSource; import com.opengamma.id.UniqueId; import com.opengamma.id.VersionCorrection; import com.opengamma.util.ArgumentChecker; /** * Holds the state corresponding to a current graph execution. The state is capable of delivering executable jobs by tracking when all dependant jobs have completed. */ public class ExecutingGraph { /** * Temporary information used to construct tail execution chains. */ private static final class TailJobInfo { private final long[] _requiredJobIds; private int _requiredJobIndex; public TailJobInfo(final int blockCount) { _requiredJobIds = new long[blockCount]; } public long[] getRequiredJobIds() { assert _requiredJobIndex == _requiredJobIds.length; return _requiredJobIds; } public boolean addRequiredJobId(final long id) { _requiredJobIds[_requiredJobIndex++] = id; return _requiredJobIndex == _requiredJobIds.length; } } /** * Information about planned jobs that are currently blocked on one or more executing jobs. * <p> * Tail jobs are not represented here - the blocking information is held in TailJobInfo - as the calculation node they are dispatched to is responsible for executing them in the correct sequence. */ private static final class BlockedJobInfo { private final PlannedJob _job; private int _waitingFor; public BlockedJobInfo(final PlannedJob job) { _job = job; _waitingFor = job.getInputJobCount(); } public PlannedJob getJob() { assert _waitingFor == 0; return _job; } public boolean unblock() { return --_waitingFor == 0; } } private final GraphExecutionPlan _plan; private final UniqueId _cycleId; private final Instant _valuationTime; private final VersionCorrection _resolverVersionCorrection; private final List<PlannedJob> _executable; private final Map<PlannedJob, BlockedJobInfo> _blocked; private final Map<CalculationJobSpecification, BlockedJobInfo[]> _executing; /** * Creates a new execution state. * * @param plan the owning execution plan, not null * @param cycleId the cycle identifier for job specifications, not null * @param valuationTime the valuation time for job specifications, not null * @param resolverVersionCorrection the resolution time stamp, not null */ protected ExecutingGraph(final GraphExecutionPlan plan, final UniqueId cycleId, final Instant valuationTime, final VersionCorrection resolverVersionCorrection) { ArgumentChecker.notNull(plan, "plan"); ArgumentChecker.notNull(cycleId, "cycleId"); ArgumentChecker.notNull(valuationTime, "valuationTime"); ArgumentChecker.notNull(resolverVersionCorrection, "resolverVersionCorrection"); _plan = plan; _cycleId = cycleId; _valuationTime = valuationTime; _resolverVersionCorrection = resolverVersionCorrection; _executable = new ArrayList<PlannedJob>(plan.getLeafJobs()); _blocked = new HashMap<PlannedJob, BlockedJobInfo>(); _executing = new HashMap<CalculationJobSpecification, BlockedJobInfo[]>(); } protected GraphExecutionPlan getPlan() { return _plan; } public String getCalculationConfiguration() { return getPlan().getCalculationConfiguration(); } public long getFunctionInitializationId() { return getPlan().getFunctionInitializationId(); } public UniqueId getCycleId() { return _cycleId; } public Instant getValuationTime() { return _valuationTime; } public VersionCorrection getResolverVersionCorrection() { return _resolverVersionCorrection; } /** * Allocates a job specification. * * @return the allocated job specification */ protected CalculationJobSpecification createJobSpecification() { // TODO: Should probably inject a job identifier source - if there are multiple view processor host processes then a static one like this // won't give us unique identifiers - we need to partition the identifier space amongst the processes that will use a common calc node pool return new CalculationJobSpecification(getCycleId(), getCalculationConfiguration(), getValuationTime(), JobIdSource.getId()); } /** * Creates an actual calculation job from a planned job that is destined for "tail" execution. * <p> * The caller must already hold the synchronisation lock. * * @param planned the planned job, not null * @param jobInfo the map containing information about the tail job set * @return the actual job */ protected CalculationJob createTailCalculationJob(final PlannedJob planned, final Map<PlannedJob, TailJobInfo> jobInfo) { final long[] requiredJobIds = jobInfo.get(planned).getRequiredJobIds(); final CalculationJob actual = planned.createCalculationJob(createJobSpecification(), getFunctionInitializationId(), getResolverVersionCorrection(), requiredJobIds); addDependentCalculationJobs(actual, planned); addTailCalculationJobs(actual, planned, jobInfo); return actual; } /** * Updates the state of any dependent fragments of the execution plan so that they may become executable when the job being created here completes. * * @param actual the job being created, not null * @param planned the execution plan information, not null */ protected void addDependentCalculationJobs(final CalculationJob actual, final PlannedJob planned) { final PlannedJob[] dependents = planned.getDependents(); if (dependents != null) { final BlockedJobInfo[] dependentsInfo = new BlockedJobInfo[dependents.length]; for (int i = 0; i < dependents.length; i++) { PlannedJob dependent = dependents[i]; BlockedJobInfo dependentInfo = _blocked.get(dependent); if (dependentInfo == null) { dependentInfo = new BlockedJobInfo(dependent); _blocked.put(dependent, dependentInfo); } dependentsInfo[i] = dependentInfo; } _executing.put(actual.getSpecification(), dependentsInfo); } else { _executing.put(actual.getSpecification(), null); } } /** * Adds the tail executing jobs to the job being created from the execution plan. * * @param actual the job being created, not null * @param planned the execution plan information, not null * @param jobInfo a map to use for temporary storage, not null */ protected void addTailCalculationJobs(final CalculationJob actual, final PlannedJob planned, final Map<PlannedJob, TailJobInfo> jobInfo) { if (planned.getTails() != null) { final long jobId = actual.getSpecification().getJobId(); for (PlannedJob tail : planned.getTails()) { TailJobInfo tailInfo = jobInfo.get(tail); if (tailInfo == null) { tailInfo = new TailJobInfo(tail.getInputJobCount()); jobInfo.put(tail, tailInfo); } if (tailInfo.addRequiredJobId(jobId)) { actual.addTail(createTailCalculationJob(tail, jobInfo)); } } } } /** * Creates an actual calculation job from a planned job. * <p> * The caller must already hold the synchronisation lock. * * @param planned the planned job, not null * @return the actual calculation job, not null */ protected CalculationJob createCalculationJob(final PlannedJob planned) { final CalculationJob actual = planned.createCalculationJob(createJobSpecification(), getFunctionInitializationId(), getResolverVersionCorrection(), null); addDependentCalculationJobs(actual, planned); if (planned.getTails() != null) { addTailCalculationJobs(actual, planned, new HashMap<PlannedJob, TailJobInfo>()); } return actual; } /** * Returns the next job that can be executed, or null if there are none available for execution. * <p> * A null return may happen if either the graph has completed execution, or there are jobs pending. * * @return an executable job, if one is available */ public synchronized CalculationJob nextExecutableJob() { final int index = _executable.size() - 1; if (index < 0) { return null; } final PlannedJob planned = _executable.remove(index); return createCalculationJob(planned); } /** * Tests whether execution of the graph is complete. * * @return true if graph execution has finished - there are no more executable jobs and all that were previously returned have been signaled as complete */ public synchronized boolean isFinished() { return _executable.isEmpty() && _executing.isEmpty() && _blocked.isEmpty(); } /** * Notifies of a job, previously returned by {@link #nextExecutableJob} (either directly or as a job's tail), having completed. * <p> * Any jobs that were not yet executable because they require one or more results from this job may now become executable. * * @param jobSpec the job that has completed, not null */ public synchronized void jobCompleted(CalculationJobSpecification jobSpec) { final BlockedJobInfo[] blockedJobs = _executing.remove(jobSpec); if (blockedJobs != null) { for (BlockedJobInfo blockedJob : blockedJobs) { if (blockedJob.unblock()) { final PlannedJob job = blockedJob.getJob(); _executable.add(job); _blocked.remove(job); } } } } @Override public String toString() { return "ExecutingGraph-" + _plan.getCalculationConfiguration(); } }