ratpack.service.internal.ServicesGraph Maven / Gradle / Ivy
/*
* Copyright 2016 the original author or authors.
*
* 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 ratpack.service.internal;
import com.google.common.base.Joiner;
import com.google.common.collect.*;
import org.slf4j.Logger;
import ratpack.api.Nullable;
import ratpack.exec.ExecController;
import ratpack.func.Predicate;
import ratpack.registry.Registry;
import ratpack.server.StartupFailureException;
import ratpack.server.internal.DefaultRatpackServer;
import ratpack.service.*;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public class ServicesGraph {
public static final Logger LOGGER = DefaultRatpackServer.LOGGER;
private final List nodes;
private final AtomicInteger toStartCount = new AtomicInteger();
private final AtomicInteger toStopCount = new AtomicInteger();
private final AtomicInteger starting = new AtomicInteger();
private final CountDownLatch startLatch = new CountDownLatch(1);
private final CountDownLatch stopLatch = new CountDownLatch(1);
private final ExecController execController;
private final AtomicReference failureRef = new AtomicReference<>();
private volatile boolean startupFailed;
public ServicesGraph(Registry registry) throws Exception {
this.nodes = ImmutableList.builder()
.addAll(Iterables.transform(registry.getAll(Service.class), Node::new))
.addAll(adaptLegacy(registry))
.build();
this.toStartCount.set(nodes.size());
this.toStopCount.set(nodes.size());
this.execController = registry.get(ExecController.class);
defineDependencies(registry);
}
private Iterable adaptLegacy(Registry registry) {
@SuppressWarnings("deprecation") Class type = ratpack.server.Service.class;
return Iterables.transform(registry.getAll(type), s -> new Node(new DefaultLegacyServiceAdapter(s)));
}
private void defineDependencies(Registry registry) throws Exception {
SpecBacking specBacking = new SpecBacking();
for (ServiceDependencies dependencies : registry.getAll(ServiceDependencies.class)) {
dependencies.define(specBacking);
}
List legacyNodes = Lists.newArrayList();
for (Node node : nodes) {
if (node.isLegacy()) {
for (Node legacyNode : legacyNodes) {
node.addDependency(legacyNode);
legacyNode.addDependent(node);
}
legacyNodes.add(node);
}
DependsOn dependsOn = node.getDependsOn();
if (dependsOn != null) {
specBacking.dependsOn(
s -> node.getImplClass().isInstance(s),
s -> {
for (Class> dependencyType : dependsOn.value()) {
if (dependencyType.isInstance(unpackIfLegacy(s))) {
return true;
}
}
return false;
}
);
}
}
}
public synchronized void start(StartEvent startEvent) throws StartupFailureException, InterruptedException {
if (startLatch.getCount() > 0) {
if (nodes.isEmpty()) {
startLatch.countDown();
} else {
LOGGER.info("Initializing " + nodes.size() + " services...");
starting.incrementAndGet();
//noinspection Convert2streamapi
for (Node node : nodes) {
if (node.dependencies.isEmpty()) {
node.start(startEvent);
}
}
if (starting.decrementAndGet() == 0 && toStartCount.get() > 0) {
onCycle();
}
}
}
startLatch.await();
StartupFailureException startupFailureException = failureRef.get();
if (startupFailureException != null) {
stop(new DefaultEvent(startEvent.getRegistry(), false));
throw startupFailureException;
}
}
public synchronized void stop(StopEvent stopEvent) throws InterruptedException {
if (stopLatch.getCount() > 0) {
doStop(stopEvent);
}
stopLatch.await();
}
private void doStop(StopEvent stopEvent) {
if (nodes.isEmpty()) {
stopLatch.countDown();
} else {
LOGGER.info("Stopping " + nodes.size() + " services...");
nodes.forEach(n -> {
if (n.dependentsToStopCount.get() == 0) {
n.stop(stopEvent);
}
});
}
}
private void serviceDidStart(Node node, StartEvent startEvent) {
node.dependencies.forEach(Node::dependentStarted);
if (toStartCount.decrementAndGet() == 0) {
if (startupFailed) {
StartupFailureException exception = processFailure();
failureRef.set(exception);
}
startLatch.countDown();
} else {
node.dependents.forEach(n -> n.dependencyStarted(startEvent));
if (starting.decrementAndGet() == 0 && toStartCount.get() > 0) {
onCycle();
}
}
}
private void onCycle() {
String joinedServiceNames = FluentIterable.from(nodes)
.filter(Node::notStarted)
.transform(n -> n.service.getName())
.join(Joiner.on(", "));
failureRef.set(new StartupFailureException("dependency cycle detected involving the following services: [" + joinedServiceNames + "]"));
startLatch.countDown();
}
@Nullable
private StartupFailureException processFailure() {
StartupFailureException exception = null;
for (Node node : nodes) {
if (node.startError != null) {
StartupFailureException nodeException = new StartupFailureException("Service '" + node.service.getName() + "' failed to start", node.startError);
if (exception == null) {
exception = nodeException;
} else {
exception.addSuppressed(nodeException);
}
}
}
return exception;
}
private void serviceDidStop(Node node, StopEvent stopEvent) {
if (toStopCount.decrementAndGet() == 0) {
stopLatch.countDown();
} else {
node.dependencies.forEach(n -> n.dependentStopped(stopEvent));
}
}
private class SpecBacking implements ServiceDependenciesSpec {
@Override
public ServiceDependenciesSpec dependsOn(Predicate super Service> dependents, Predicate super Service> dependencies) throws Exception {
List dependentNodes = Lists.newArrayList();
List dependencyNodes = Lists.newArrayList();
for (Node node : nodes) {
boolean dependent = false;
if (dependents.apply(node.service)) {
dependent = true;
dependentNodes.add(node);
}
if (dependencies.apply(node.service)) {
if (dependent) {
throw new IllegalStateException("Service '" + node.service.getName() + "' marked as dependent and dependency");
}
dependencyNodes.add(node);
}
}
for (Node dependencyNode : dependencyNodes) {
for (Node dependentNode : dependentNodes) {
dependentNode.addDependency(dependencyNode);
dependencyNode.addDependent(dependentNode);
}
}
return this;
}
}
private class Node {
private final Service service;
private final Set dependents = Sets.newHashSet();
private final AtomicInteger dependentsToStopCount = new AtomicInteger();
private final Set dependencies = Sets.newHashSet();
private final AtomicInteger dependenciesToStartCount = new AtomicInteger();
private final AtomicBoolean stopped = new AtomicBoolean();
private volatile boolean running;
private volatile Throwable startError;
public Node(Service service) {
this.service = service;
}
public DependsOn getDependsOn() {
return getImplClass().getAnnotation(DependsOn.class);
}
private Class> getImplClass() {
return isLegacy() ? ((DefaultLegacyServiceAdapter) service).getAdapted().getClass() : service.getClass();
}
private boolean isLegacy() {
return service instanceof DefaultLegacyServiceAdapter;
}
public boolean notStarted() {
return !running;
}
public void addDependency(Node node) {
dependencies.add(node);
dependenciesToStartCount.incrementAndGet();
}
public void addDependent(Node node) {
dependents.add(node);
}
public void dependencyStarted(StartEvent startEvent) {
if (dependenciesToStartCount.decrementAndGet() == 0) {
start(startEvent);
}
}
public void dependentStarted() {
dependentsToStopCount.incrementAndGet();
}
public void dependentStopped(StopEvent stopEvent) {
if (dependentsToStopCount.decrementAndGet() <= 0) {
stop(stopEvent);
}
}
public void start(StartEvent startEvent) {
starting.incrementAndGet();
if (startupFailed) {
serviceDidStart(this, startEvent);
return;
}
execController.fork()
.onComplete(e -> {
if (startError == null) {
running = true;
}
serviceDidStart(this, startEvent);
})
.onError(e -> {
startError = e;
startupFailed = true;
})
.start(e -> service.onStart(startEvent));
}
public void stop(StopEvent stopEvent) {
if (stopped.compareAndSet(false, true)) {
if (running) {
execController.fork()
.onComplete(e ->
serviceDidStop(this, stopEvent)
)
.onError(e ->
LOGGER.warn("Service '" + service.getName() + "' thrown an exception while stopping.", e)
)
.start(e -> service.onStop(stopEvent));
} else {
serviceDidStop(this, stopEvent);
}
}
}
}
@SuppressWarnings("deprecation")
public static boolean isOfType(Service service, Class> type) {
if (service instanceof LegacyServiceAdapter) {
ratpack.server.Service legacyService = ((LegacyServiceAdapter) service).getAdapted();
return type.isInstance(legacyService);
} else {
return type.isInstance(service);
}
}
@SuppressWarnings("deprecation")
public static Object unpackIfLegacy(Service service) {
if (service instanceof LegacyServiceAdapter) {
return ((LegacyServiceAdapter) service).getAdapted();
} else {
return service;
}
}
}