/*
* Copyright (C) 2015 SoftIndex LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.datakernel.service;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import io.datakernel.annotation.Nullable;
import io.datakernel.util.Stopwatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static com.google.common.base.Strings.repeat;
import static com.google.common.base.Throwables.getRootCause;
import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.union;
import static io.datakernel.util.Preconditions.checkArgument;
import static io.datakernel.util.Preconditions.checkState;
import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* Stores the dependency graph of services. Primarily used by
* {@link ServiceGraphModule}.
*/
public class ServiceGraph {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* This set used to represent edges between vertices. If N1 and N2 - nodes
* and between them exists edge from N1 to N2, it can be represent as
* adding to this SetMultimap element <N1,N2>. This collection consist of
* nodes in which there are edges and their keys - previous nodes.
*/
private final Multimap<Object, Object> forwards = LinkedHashMultimap.create();
/**
* This set used to represent edges between vertices. If N1 and N2 - nodes
* and between them exists edge from N1 to N2, it can be represent as
* adding to this SetMultimap element <N2,N1>. This collection consist of
* nodes in which there are edges and their keys - previous nodes.
*/
private final Multimap<Object, Object> backwards = LinkedHashMultimap.create();
private final Set<Object> runningNodes = new HashSet<>();
private final Map<Object, Service> services = new HashMap<>();
protected ServiceGraph() {
}
public static ServiceGraph create() {
return new ServiceGraph();
}
public ServiceGraph add(Object key, Service service, Object... dependencies) {
checkArgument(!services.containsKey(key));
if (service != null) {
services.put(key, service);
}
add(key, asList(dependencies));
return this;
}
public ServiceGraph add(Object key, Iterable<Object> dependencies) {
for (Object dependency : dependencies) {
checkArgument(!(dependency instanceof Service), "Dependency %s must be a key, not a service", dependency);
forwards.put(key, dependency);
backwards.put(dependency, key);
}
return this;
}
public ServiceGraph add(Object key, Object first, Object... rest) {
if (first instanceof Iterable)
return add(key, (Iterable) first);
add(key, concat(singleton(first), asList(rest)));
return this;
}
private ListenableFuture<LongestPath> processNode(final Object node, final boolean start,
Map<Object, ListenableFuture<LongestPath>> futures, final Executor executor) {
List<ListenableFuture<LongestPath>> dependencyFutures = new ArrayList<>();
for (Object dependencyNode : (start ? forwards : backwards).get(node)) {
ListenableFuture<LongestPath> dependencyFuture = processNode(dependencyNode, start, futures, executor);
dependencyFutures.add(dependencyFuture);
}
if (futures.containsKey(node)) {
return futures.get(node);
}
final SettableFuture<LongestPath> future = SettableFuture.create();
futures.put(node, future);
final ListenableFuture<LongestPath> dependenciesFuture = combineDependenciesFutures(dependencyFutures, executor);
dependenciesFuture.addListener(new Runnable() {
@Override
public void run() {
try {
final LongestPath longestPath = dependenciesFuture.get();
Service service = services.get(node);
if (service == null) {
logger.debug("...skipping no-service node: " + nodeToString(node));
future.set(longestPath);
return;
}
if (!start && !runningNodes.contains(node)) {
logger.debug("...skipping not running node: " + nodeToString(node));
future.set(longestPath);
return;
}
final Stopwatch sw = Stopwatch.createStarted();
final ListenableFuture<?> serviceFuture = (start ? service.start() : service.stop());
logger.info((start ? "Starting" : "Stopping") + " node: " + nodeToString(node));
serviceFuture.addListener(new Runnable() {
@Override
public void run() {
try {
serviceFuture.get();
if (start) {
runningNodes.add(node);
} else {
runningNodes.remove(node);
}
long elapsed = sw.elapsed(MILLISECONDS);
logger.info((start ? "Started" : "Stopped") + " " + nodeToString(node) + (elapsed >= 1L ? (" in " + sw) : ""));
future.set(new LongestPath(elapsed + (longestPath != null ? longestPath.totalTime : 0),
elapsed, node, longestPath));
} catch (InterruptedException | ExecutionException e) {
logger.error("error: " + nodeToString(node), e);
future.setException(getRootCause(e));
}
}
}, executor);
} catch (InterruptedException | ExecutionException e) {
future.setException(getRootCause(e));
}
}
}, executor);
return future;
}
private ListenableFuture<LongestPath> combineDependenciesFutures(List<ListenableFuture<LongestPath>> futures, Executor executor) {
if (futures.size() == 0) {
return Futures.immediateFuture(null);
}
if (futures.size() == 1) {
return futures.get(0);
}
final SettableFuture<LongestPath> settableFuture = SettableFuture.create();
final AtomicInteger atomicInteger = new AtomicInteger(futures.size());
final AtomicReference<LongestPath> bestPath = new AtomicReference<>();
final AtomicReference<Throwable> exception = new AtomicReference<>();
for (final ListenableFuture<LongestPath> future : futures) {
future.addListener(new Runnable() {
@Override
public void run() {
try {
LongestPath path = future.get();
if (bestPath.get() == null || (path != null && path.totalTime > bestPath.get().totalTime)) {
bestPath.set(path);
}
} catch (InterruptedException | ExecutionException e) {
if (exception.get() == null) {
exception.set(getRootCause(e));
}
}
if (atomicInteger.decrementAndGet() == 0) {
if (exception.get() != null) {
settableFuture.setException(exception.get());
} else {
settableFuture.set(bestPath.get());
}
}
}
}, executor);
}
return settableFuture;
}
/**
* Stops services from the service graph
*/
synchronized public ListenableFuture<?> startFuture() {
List<Object> circularDependencies = findCircularDependencies();
checkState(circularDependencies == null, "Circular dependencies found: %s", circularDependencies);
Set<Object> rootNodes = difference(union(services.keySet(), forwards.keySet()), backwards.keySet());
logger.info("Starting services");
logger.debug("Root nodes: {}", rootNodes);
return actionInThread(true, rootNodes);
}
/**
* Stops services from the service graph
*/
synchronized public ListenableFuture<?> stopFuture() {
Set<Object> leafNodes = difference(union(services.keySet(), backwards.keySet()), forwards.keySet());
logger.info("Stopping services");
logger.debug("Leaf nodes: {}", leafNodes);
return actionInThread(false, leafNodes);
}
private ListenableFuture<?> actionInThread(final boolean start, final Collection<Object> rootNodes) {
final SettableFuture<?> resultFuture = SettableFuture.create();
final ExecutorService executor = newSingleThreadExecutor();
executor.execute(new Runnable() {
@Override
public void run() {
Map<Object, ListenableFuture<LongestPath>> futures = new HashMap<>();
List<ListenableFuture<LongestPath>> rootFutures = new ArrayList<>();
for (Object rootNode : rootNodes) {
rootFutures.add(processNode(rootNode, start, futures, executor));
}
final ListenableFuture<LongestPath> rootFuture = combineDependenciesFutures(rootFutures, executor);
rootFuture.addListener(new Runnable() {
@Override
public void run() {
try {
LongestPath longestPath = rootFuture.get();
StringBuilder sb = new StringBuilder();
printLongestPath(sb, longestPath);
if (sb.length() != 0)
sb.deleteCharAt(sb.length() - 1);
logger.info("Longest path:\n" + sb);
resultFuture.set(null);
executor.shutdown();
} catch (InterruptedException | ExecutionException e) {
resultFuture.setException(getRootCause(e));
executor.shutdown();
}
}
}, executor);
}
});
return resultFuture;
}
private void printLongestPath(StringBuilder sb, LongestPath longestPath) {
if (longestPath == null)
return;
printLongestPath(sb, longestPath.tail);
sb.append(nodeToString(longestPath.head)).append(" : ");
sb.append(String.format("%1.3f sec", longestPath.time / 1000.0));
sb.append("\n");
}
@Override
@SuppressWarnings("StringConcatenationInsideStringBufferAppend")
public String toString() {
StringBuilder sb = new StringBuilder();
Set<Object> visited = new LinkedHashSet<>();
List<Iterator<Object>> path = new ArrayList<>();
Iterable<Object> roots = difference(union(services.keySet(), forwards.keySet()), backwards.keySet());
path.add(roots.iterator());
while (!path.isEmpty()) {
Iterator<Object> it = path.get(path.size() - 1);
if (it.hasNext()) {
Object node = it.next();
if (!visited.contains(node)) {
visited.add(node);
sb.append(repeat("\t", path.size() - 1) + "" + nodeToString(node) + "\n");
path.add(forwards.get(node).iterator());
} else {
sb.append(repeat("\t", path.size() - 1) + "" + nodeToString(node) + " *︎" + "\n");
}
} else {
path.remove(path.size() - 1);
}
}
if (sb.length() != 0)
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
private void removeIntermediate(Object vertex) {
for (Object backward : backwards.get(vertex)) {
forwards.get(backward).remove(vertex);
for (Object forward : forwards.get(vertex)) {
if (!forward.equals(backward)) {
forwards.get(backward).add(forward);
}
}
}
for (Object forward : forwards.get(vertex)) {
backwards.get(forward).remove(vertex);
for (Object backward : backwards.get(vertex)) {
if (!forward.equals(backward)) {
backwards.get(forward).add(backward);
}
}
}
forwards.removeAll(vertex);
backwards.removeAll(vertex);
}
/**
* Removes nodes which don't have services
*/
public void removeIntermediateNodes() {
List<Object> toRemove = new ArrayList<>();
for (Object v : union(forwards.keySet(), backwards.keySet())) {
if (!services.containsKey(v)) {
toRemove.add(v);
}
}
for (Object v : toRemove) {
removeIntermediate(v);
}
}
private List<Object> findCircularDependencies() {
Set<Object> visited = new LinkedHashSet<>();
List<Object> path = new ArrayList<>();
next:
while (true) {
for (Object node : path.isEmpty() ? services.keySet() : forwards.get(path.get(path.size() - 1))) {
int loopIndex = path.indexOf(node);
if (loopIndex != -1) {
logger.warn("Circular dependencies found: " + nodesToString(path.subList(loopIndex, path.size())));
return path.subList(loopIndex, path.size());
}
if (!visited.contains(node)) {
visited.add(node);
path.add(node);
continue next;
}
}
if (path.isEmpty())
break;
path.remove(path.size() - 1);
}
return null;
}
protected String nodeToString(Object node) {
return node.toString();
}
private String nodesToString(Iterable<Object> newNodes) {
StringBuilder sb = new StringBuilder().append("[");
Iterator<Object> iterator = newNodes.iterator();
while (iterator.hasNext()) {
sb.append(nodeToString(iterator.next()));
if (iterator.hasNext()) {
sb.append(", ");
}
}
return sb.append("]").toString();
}
private static class LongestPath {
private final long totalTime;
private final long time;
private final Object head;
@Nullable
private final LongestPath tail;
private LongestPath(long totalTime, long time, Object head, LongestPath tail) {
this.totalTime = totalTime;
this.time = time;
this.head = head;
this.tail = tail;
}
}
}