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 extends TaskFactoryConfig, ?>> 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;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy