package org.arbeitspferde.groningen; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.name.Named; import org.arbeitspferde.groningen.Datastore.DatastoreException; import org.arbeitspferde.groningen.common.BlockScope; import org.arbeitspferde.groningen.config.ConfigManager; import org.arbeitspferde.groningen.config.GroningenConfig; import org.arbeitspferde.groningen.config.PipelineScoped; import org.arbeitspferde.groningen.proto.Params.GroningenParams.PipelineSynchMode; import org.arbeitspferde.groningen.scorer.HistoricalBestPerformerScorer; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; /** * PipelineManager is used to start new pipelines. It also does the bookkeeping, maintaining * the list of currently running pipelines. * * @author mbushkov@google.com (Mikhail Bushkov) */ @Singleton public class PipelineManager { private static final Logger log = Logger.getLogger(PipelineManager.class.getCanonicalName()); private final PipelineIdGenerator pipelineIdGenerator; private final BlockScope pipelineScope; private final Provider<Pipeline> pipelineProvider; private final Datastore datastore; private final Map<PipelineSynchMode, Provider<PipelineSynchronizer>> synchronizerProviderMap; private final Provider<HistoricalBestPerformerScorer> bestPerformerScorerProvider; private final ConcurrentMap<PipelineId, Pipeline> pipelines; @Inject public PipelineManager(PipelineIdGenerator pipelineIdGenerator, @Named(PipelineScoped.SCOPE_NAME) BlockScope pipelineScope, Provider<Pipeline> pipelineProvider, Datastore datastore, Map<PipelineSynchMode, Provider<PipelineSynchronizer>> synchronizerProviderMap, Provider<HistoricalBestPerformerScorer> bestPerformerScorerProvider) { this.pipelineIdGenerator = pipelineIdGenerator; this.pipelineScope = pipelineScope; this.pipelineProvider = pipelineProvider; this.datastore = datastore; this.synchronizerProviderMap = synchronizerProviderMap; this.pipelines = new ConcurrentHashMap<>(); this.bestPerformerScorerProvider = bestPerformerScorerProvider; } public PipelineId restorePipeline(final PipelineState pipelineState, final ConfigManager configManager, final boolean blockUntilStarted) { final GroningenConfig firstConfig = configManager.queryConfig(); final PipelineId pipelineId = pipelineState.pipelineId(); final PipelineSynchronizer synchronizer = getRequestedPipelineSynchronizer(firstConfig); final ReentrantLock pipelineConstructionLock = new ReentrantLock(); final Condition pipelineConstructionCondition = pipelineConstructionLock.newCondition(); // NOTE(mbushkov): using AtomicReference here is somewhat redundant. We use a dedicated lock // (pipelineConstructionLock) and do not require atomicity. Still it's very handy to just // use a reference class that is already in a standard library final AtomicReference<Pipeline> pipelineReference = new AtomicReference<>(); // TODO(etheon): add metric exporter for this final PipelineStageInfo pipelineStageInfo = new PipelineStageInfo(); synchronizer.setPipelineStageTracker(pipelineStageInfo); final HistoricalBestPerformerScorer bestPerformerScorer = bestPerformerScorerProvider.get(); log.fine("starting thread for pipeline (restoring) " + pipelineId.toString()); Thread pipelineThread = new Thread("pipeline-restore-" + pipelineId.toString()) { @Override public void run() { try { pipelineScope.enter(); try { pipelineScope.seed(PipelineSynchronizer.class, synchronizer); pipelineScope.seed(PipelineId.class, pipelineId); pipelineScope.seed(ConfigManager.class, configManager); pipelineScope.seed(PipelineStageInfo.class, pipelineStageInfo); pipelineScope.seed(HistoricalBestPerformerScorer.class, bestPerformerScorer); Pipeline pipeline; pipelineConstructionLock.lock(); try { pipeline = pipelineProvider.get(); pipelineReference.set(pipeline); pipelines.put(pipelineId, pipeline); pipelineConstructionCondition.signal(); } finally { pipelineConstructionLock.unlock(); } log.fine("running pipeline " + pipelineId.toString()); pipeline.restoreState(pipelineState); pipeline.run(); } finally { pipelineStageInfo.set(PipelineStageState.PIPELINE_FINALIZATION_INPROGRESS); pipelineScope.exit(); pipelines.remove(pipelineId); try { datastore.deletePipelines(Lists.newArrayList(pipelineId)); pipelineStageInfo.set(PipelineStageState.PIPELINE_FINALIZED); } catch (DatastoreException e) { log.severe(String.format("deleting pipeline failed (pipeline id: %s): %s", pipelineId.toString(), e.getMessage())); } } } catch (RuntimeException e) { log.severe(e.toString()); } } }; pipelineThread.start(); if (blockUntilStarted) { pipelineConstructionLock.lock(); try { while (pipelineReference.get() == null) { pipelineConstructionCondition.awaitUninterruptibly(); } } finally { pipelineConstructionLock.unlock(); } } return pipelineId; } /** * Start new Pipeline with a given {@link ConfigManager} * * @param configManager {@link ConfigManager} to be used to query for pipeline's configurations * @param blockUntilStarted Block until pipeline's thread is actually started and the pipeline * itself is registered in the {@link PipelineManager} * @return {@link PipelineId} of the pipeline that is: a) about to be started * (if blockUntilStarted == false) b) was started (if blockUntilStarted == true) */ public PipelineId startPipeline(final ConfigManager configManager, final boolean blockUntilStarted) { final GroningenConfig firstConfig = configManager.queryConfig(); final PipelineId pipelineId = pipelineIdGenerator.generatePipelineId(firstConfig); final PipelineSynchronizer synchronizer = getRequestedPipelineSynchronizer(firstConfig); final ReentrantLock pipelineConstructionLock = new ReentrantLock(); final Condition pipelineConstructionCondition = pipelineConstructionLock.newCondition(); // NOTE(mbushkov): using AtomicReference here is somewhat redundant. We use a dedicated lock // (pipelineConstructionLock) and do not require atomicity. Still it's very handy to just // use a reference class that is already in a standard library final AtomicReference<Pipeline> pipelineReference = new AtomicReference<>(); // Create the pipeline stage tracking object and tie it to the synchronizer before the // pipeline is actually started. // TODO(etheon): add metric exporter for this final PipelineStageInfo pipelineStageInfo = new PipelineStageInfo(); synchronizer.setPipelineStageTracker(pipelineStageInfo); final HistoricalBestPerformerScorer bestPerformerScorer = bestPerformerScorerProvider.get(); log.fine("starting thread for pipeline " + pipelineId.toString()); Thread pipelineThread = new Thread("pipeline-" + pipelineId.toString()) { @Override public void run() { try { pipelineScope.enter(); try { pipelineScope.seed(PipelineSynchronizer.class, synchronizer); pipelineScope.seed(PipelineId.class, pipelineId); pipelineScope.seed(ConfigManager.class, configManager); pipelineScope.seed(PipelineStageInfo.class, pipelineStageInfo); pipelineScope.seed(HistoricalBestPerformerScorer.class, bestPerformerScorer); Pipeline pipeline; pipelineConstructionLock.lock(); try { pipeline = pipelineProvider.get(); pipelineReference.set(pipeline); pipelines.put(pipelineId, pipeline); pipelineConstructionCondition.signal(); } finally { pipelineConstructionLock.unlock(); } log.fine("writing to datastore " + pipelineId.toString()); try { datastore.createPipeline(pipeline.state(), /* checkForConflicts */ true); } catch (DatastoreException e) { log.severe(String.format("writing to datastore failed (pipeline id: %s): %s", pipelineId.toString(), e.getMessage())); } log.fine("running pipeline " + pipelineId.toString()); pipeline.run(); } finally { pipelineStageInfo.set(PipelineStageState.PIPELINE_FINALIZATION_INPROGRESS); pipelineScope.exit(); pipelines.remove(pipelineId); try { datastore.deletePipelines(Lists.newArrayList(pipelineId)); pipelineStageInfo.set(PipelineStageState.PIPELINE_FINALIZED); } catch (DatastoreException e) { log.severe(String.format("deleting pipeline failed (pipeline id: %s): %s", pipelineId.toString(), e.getMessage())); } } } catch (RuntimeException e) { log.severe(e.toString()); } } }; pipelineThread.start(); if (blockUntilStarted) { pipelineConstructionLock.lock(); try { while (pipelineReference.get() == null) { pipelineConstructionCondition.awaitUninterruptibly(); } } finally { pipelineConstructionLock.unlock(); } } return pipelineId; } /** * @param pipelineId {@link PipelineId} identifying the needed pipeline * @return {@link Pipeline} corresponding to current PipelineId or null if such pipeline was * not ever created or is already dead */ public Pipeline findPipelineById(PipelineId pipelineId) { return pipelines.get(pipelineId); } /** * Returns the snapshot of the list of all currently running pipelines * * @return Map of PipelineId->Pipeline */ public Map<PipelineId, Pipeline> getAllPipelines() { return new HashMap<>(pipelines); } /** * Map a requested mode of synchronizer the pipeline to a {@link PipelineSynchronizer} that * performs that type of synchronization. * * @param config the {@link GroningenConfig} specifying the requested experiment * @return the type of PipelineSynchronizer that implements the requested type of * synchronization, null if both there exists no direct mapping AND there is * not a mapping for the default type of synchronization - NONE */ @VisibleForTesting PipelineSynchronizer getRequestedPipelineSynchronizer(GroningenConfig config) { PipelineSynchMode syncMode = config.getParamBlock().getPipelineSyncType(); Provider<PipelineSynchronizer> syncProvider = synchronizerProviderMap.get(syncMode); if (syncProvider == null) { log.log(Level.WARNING, "no guice binding for " + syncMode.name()); syncProvider = synchronizerProviderMap.get(PipelineSynchMode.NONE); if (syncProvider == null) { log.log(Level.SEVERE, "no named binding for default syncMode NONE"); return null; } } return syncProvider.get(); } }