All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jtrim2.taskgraph.basic.CollectingTaskGraphBuilder Maven / Gradle / Ivy

package org.jtrim2.taskgraph.basic;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jtrim2.cancel.Cancellation;
import org.jtrim2.cancel.CancellationSource;
import org.jtrim2.cancel.CancellationToken;
import org.jtrim2.collections.CollectionsEx;
import org.jtrim2.concurrent.AsyncTasks;
import org.jtrim2.executor.CancelableFunction;
import org.jtrim2.executor.TaskExecutor;
import org.jtrim2.taskgraph.TaskFactory;
import org.jtrim2.taskgraph.TaskFactoryConfig;
import org.jtrim2.taskgraph.TaskFactoryGroupConfigurer;
import org.jtrim2.taskgraph.TaskFactoryKey;
import org.jtrim2.taskgraph.TaskFactoryProperties;
import org.jtrim2.taskgraph.TaskFactorySetup;
import org.jtrim2.taskgraph.TaskGraphBuilder;
import org.jtrim2.taskgraph.TaskGraphBuilderProperties;
import org.jtrim2.taskgraph.TaskGraphExecutor;
import org.jtrim2.taskgraph.TaskInputBinder;
import org.jtrim2.taskgraph.TaskInputRef;
import org.jtrim2.taskgraph.TaskNodeCreateArgs;
import org.jtrim2.taskgraph.TaskNodeKey;
import org.jtrim2.taskgraph.TaskNodeProperties;
import org.jtrim2.utils.LazyValues;

/**
 * Defines a simple implementation of {@code TaskGraphBuilder} which collects the
 * added task node keys and simply passes them to a given {@link TaskGraphExecutorFactory}.
 * Once graph building is requested, the graph is built with its task nodes already aware of
 * their input. That is, the nodes of the graph will be translated into {@link TaskNode} instances.
 *
 * 

Thread safety

* The methods of this class may not be used by multiple threads concurrently, unless otherwise noted. * *

Synchronization transparency

* The methods of this class are not synchronization transparent in general. * * @see CollectingTaskGraphDefConfigurer * @see RestrictableTaskGraphExecutor */ public final class CollectingTaskGraphBuilder implements TaskGraphBuilder { private static final Logger LOGGER = Logger.getLogger(CollectingTaskGraphBuilder.class.getName()); private final TaskGraphBuilderProperties.Builder properties; private final Map, TaskFactoryConfig> configs; private final TaskGraphExecutorFactory executorFactory; private final Set> nodeKeys; /** * Creates a new {@code CollectingTaskGraphBuilder} with the given task factory definitions * and {@code TaskGraphExecutorFactory}. * * @param configs the task factory definitions used to create the task nodes. * This argument cannot be {@code null} and cannot contain {@code null} elements. * @param executorFactory the {@code TaskGraphExecutorFactory} used to create * the {@code TaskGraphExecutor} actually executing the task graph. This argument cannot * be {@code null}. */ public CollectingTaskGraphBuilder( Collection> configs, TaskGraphExecutorFactory executorFactory) { Objects.requireNonNull(configs, "configs"); Objects.requireNonNull(executorFactory, "executorFactory"); this.properties = new TaskGraphBuilderProperties.Builder(); this.executorFactory = executorFactory; this.nodeKeys = Collections.newSetFromMap(new ConcurrentHashMap<>()); this.configs = CollectionsEx.newHashMap(configs.size()); configs.forEach((config) -> { this.configs.put(config.getDefKey(), config); }); Objects.requireNonNull(this.configs, "configs"); } /** * {@inheritDoc } */ @Override public void addNode(TaskNodeKey nodeKey) { if (!configs.containsKey(nodeKey.getFactoryKey())) { throw new IllegalArgumentException("There is no factory to create this node: " + nodeKey); } if (!nodeKeys.add(nodeKey)) { throw new IllegalStateException("Duplicate node key: " + nodeKey); } } /** * {@inheritDoc } */ @Override public TaskGraphBuilderProperties.Builder properties() { return properties; } /** * {@inheritDoc } */ @Override public CompletionStage buildGraph(CancellationToken cancelToken) { Objects.requireNonNull(cancelToken, "cancelToken"); TaskGraphBuilderImpl builder = new TaskGraphBuilderImpl( cancelToken, properties.build(), configs, executorFactory); return builder.build(nodeKeys); } private static final class TaskGraphBuilderImpl { private final Map, FactoryDef> factoryDefs; private final TaskGraphBuilderProperties properties; private final CancellationSource graphBuildCancel; private final Lock taskGraphLock; private final Map, BuildableTaskNode> nodes; private final DirectedGraph.Builder> taskGraphBuilder; private final AtomicInteger outstandingBuilds; private final CompletableFuture graphBuildResult; private final TaskGraphExecutorFactory executorFactory; public TaskGraphBuilderImpl( CancellationToken cancelToken, TaskGraphBuilderProperties properties, Map, TaskFactoryConfig> factoryDefs, TaskGraphExecutorFactory executorFactory) { this.properties = properties; this.taskGraphLock = new ReentrantLock(); this.nodes = new ConcurrentHashMap<>(); this.taskGraphBuilder = new DirectedGraph.Builder<>(); this.graphBuildCancel = Cancellation.createChildCancellationSource(cancelToken); this.outstandingBuilds = new AtomicInteger(0); this.graphBuildResult = new CompletableFuture<>(); this.executorFactory = executorFactory; this.factoryDefs = CollectionsEx.newHashMap(factoryDefs.size()); // We try to minimize the configuration if possible Map> configurers = new IdentityHashMap<>(); TaskFactoryProperties defaultFactoryProperties = properties.getDefaultFactoryProperties(); factoryDefs.forEach((key, config) -> { Supplier lazyConfigurer = configurers.computeIfAbsent( config.getConfigurer(), (groupConfigurer) -> lazyGroupProperties(groupConfigurer, defaultFactoryProperties)); this.factoryDefs.put(key, factoryDef(lazyConfigurer, config)); }); } private static Supplier lazyGroupProperties( TaskFactoryGroupConfigurer groupConfigurer, TaskFactoryProperties defaults) { return LazyValues.lazyValue(() -> { TaskFactoryProperties.Builder resultBuilder = new TaskFactoryProperties.Builder(defaults); groupConfigurer.setup(resultBuilder); return resultBuilder.build(); }); } public CompletionStage build(Set> nodeKeys) { incOutstandingBuilds(); nodeKeys.forEach(this::addNode); decOutstandingBuilds(); return graphBuildResult; } private static FactoryDef factoryDef( Supplier groupPropertiesRef, TaskFactoryConfig config) { return new FactoryDef<>(config.getDefKey(), groupPropertiesRef, config.getSetup()); } private void addNode(TaskNodeKey nodeKey) { Objects.requireNonNull(nodeKey, "nodeKey"); BuildableTaskNode newNode = new BuildableTaskNode<>(nodeKey); BuildableTaskNode prev = nodes.putIfAbsent(nodeKey, newNode); assert prev == null; buildChildren(graphBuildCancel.getToken(), newNode); } public NodeTaskRef createNode( CancellationToken cancelToken, TaskNodeKey nodeKey, TaskInputBinder inputBinder) throws Exception { return createNodeBridge(cancelToken, nodeKey, inputBinder); } private NodeTaskRef createNodeBridge( CancellationToken cancelToken, TaskNodeKey nodeKey, TaskInputBinder inputBinder) throws Exception { TaskFactoryKey factoryKey = nodeKey.getFactoryKey(); FactoryDef factoryDef = getFactoryDef(factoryKey); return factoryDef.createTaskNode(cancelToken, nodeKey, inputBinder); } public BuildableTaskNode addAndBuildNode(TaskNodeKey nodeKey) { BuildableTaskNode newNode = new BuildableTaskNode<>(nodeKey); BuildableTaskNode prev = nodes.putIfAbsent(nodeKey, newNode); if (prev != null) { @SuppressWarnings("unchecked") BuildableTaskNode result = (BuildableTaskNode) prev; assert result.getKey().equals(nodeKey); return result; } buildChildren(graphBuildCancel.getToken(), newNode); return newNode; } private void buildChildren(CancellationToken cancelToken, BuildableTaskNode newNode) { TaskNodeKey key = newNode.getKey(); TaskFactoryProperties factoryProperties = getFactoryDef(key.getFactoryKey()).getProperties(); TaskExecutor factoryExecutor = factoryProperties.getFactoryExecutor(); incOutstandingBuilds(); CompletionStage future = factoryExecutor.execute(cancelToken, (taskCancelToken) -> { Set> childrenKeys = newNode.buildChildren(taskCancelToken, this); taskGraphLock.lock(); try { taskGraphBuilder.addNodeWithChildren(key, childrenKeys); } finally { taskGraphLock.unlock(); } }); future.whenComplete((result, error) -> { // We intentionally do not decrease the oustandingBuilds to prevent // success notification. if (error != null) { onError(key, error); return; } decOutstandingBuilds(); }); } private void incOutstandingBuilds() { outstandingBuilds.incrementAndGet(); } private void decOutstandingBuilds() { int outstandingBuildCount = outstandingBuilds.decrementAndGet(); if (outstandingBuildCount == 0) { onSuccess(); } } private FactoryDef getFactoryDef(TaskFactoryKey key) { @SuppressWarnings("unchecked") FactoryDef result = (FactoryDef) factoryDefs.get(key); if (result == null) { throw new IllegalStateException("Missing node factory definition for key: " + key); } assert result.getDefKey().equals(key); return result; } private void onError(TaskNodeKey nodeKey, Throwable error) { try { try { graphBuildCancel.getController().cancel(); if (!AsyncTasks.isCanceled(error)) { properties.getNodeCreateErrorHandler().onError(nodeKey, error); } } finally { graphBuildResult.completeExceptionally(error); } } catch (Throwable subError) { subError.addSuppressed(error); LOGGER.log(Level.SEVERE, "Error while handling error of a task node: " + nodeKey, subError); } } private void onSuccess() { try { // No synchronization is necessary because we already know that we have built the graph, // so no more node will be added. DependencyDag> graph = new DependencyDag<>(taskGraphBuilder.build()); TaskGraphExecutor executor = executorFactory.createExecutor(graph, getBuiltNodes()); graphBuildResult.complete(executor); } catch (Throwable ex) { LOGGER.log(Level.SEVERE, "Error while attempting to notify graph built handler.", ex); graphBuildResult.completeExceptionally(ex); } } private Iterable> getBuiltNodes() { List> result = new ArrayList<>(nodes.size()); nodes.values().forEach((buildableNode) -> { result.add(buildableNode.getBuiltNode()); }); return result; } } private static final class FactoryDef { private final TaskFactoryKey defKey; private final Supplier groupPropertiesRef; private final TaskFactorySetup setup; public FactoryDef( TaskFactoryKey defKey, Supplier groupPropertiesRef, TaskFactorySetup setup) { this.defKey = defKey; this.groupPropertiesRef = groupPropertiesRef; this.setup = setup; } public NodeTaskRef createTaskNode( CancellationToken cancelToken, TaskNodeKey nodeKey, TaskInputBinder inputs) throws Exception { TaskNodeProperties defaults = getProperties().getDefaultNodeProperties(); TaskNodeCreateArgs createArgs = new TaskNodeCreateArgs<>(nodeKey, defaults, inputs); CancelableFunction nodeTask = createFactory().createTaskNode(cancelToken, createArgs); return new NodeTaskRef<>(createArgs.properties().build(), nodeTask); } public TaskFactory createFactory() throws Exception { return setup.setup(getProperties()); } public TaskFactoryKey getDefKey() { return defKey; } public TaskFactoryProperties getProperties() { return groupPropertiesRef.get(); } } private static final class BuildableTaskNode { private final TaskNodeKey key; private final CompletableFuture taskFuture; private TaskNode builtNode; public BuildableTaskNode(TaskNodeKey key) { this.key = key; this.taskFuture = new CompletableFuture<>(); } public TaskNodeKey getKey() { return key; } public CompletableFuture getTaskFuture() { return taskFuture; } public Set> buildChildren( CancellationToken cancelToken, TaskGraphBuilderImpl nodeBuilder) throws Exception { Objects.requireNonNull(cancelToken, "cancelToken"); Objects.requireNonNull(nodeBuilder, "nodeBuilder"); TaskInputBinderImpl inputBinder = new TaskInputBinderImpl(cancelToken, nodeBuilder); NodeTaskRef nodeTask = nodeBuilder.createNode(cancelToken, key, inputBinder); builtNode = new TaskNode<>(key, nodeTask, taskFuture); return inputBinder.closeAndGetInputs(); } public TaskNode getBuiltNode() { assert builtNode != null; return builtNode; } } private static final class TaskInputBinderImpl implements TaskInputBinder { private final TaskGraphBuilderImpl nodeBuilder; private Set> inputKeys; public TaskInputBinderImpl(CancellationToken cancelToken, TaskGraphBuilderImpl nodeBuilder) { this.nodeBuilder = nodeBuilder; this.inputKeys = new HashSet<>(); } @Override public TaskInputRef bindInput(TaskNodeKey defKey) { Set> currentInputKeys = inputKeys; if (currentInputKeys == null) { throw new IllegalStateException("May only be called from the associated task node factory."); } BuildableTaskNode child = nodeBuilder.addAndBuildNode(defKey); inputKeys.add(child.getKey()); AtomicReference> resultRef = new AtomicReference<>(child.getTaskFuture()); return () -> { CompletableFuture nodeFuture = resultRef.getAndSet(null); if (nodeFuture == null) { throw new IllegalStateException("Input already consumed for key: " + defKey); } return TaskNode.getExpectedResultNow(defKey, nodeFuture); }; } public Set> closeAndGetInputs() { Set> result = inputKeys; inputKeys = null; return result; } } }