io.activej.service.ServiceGraphModule Maven / Gradle / Ivy
Show all versions of activej-servicegraph Show documentation
/*
* Copyright (C) 2020 ActiveJ 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.activej.service;
import io.activej.async.service.ReactiveService;
import io.activej.common.builder.AbstractBuilder;
import io.activej.common.initializer.Initializer;
import io.activej.common.service.BlockingService;
import io.activej.eventloop.Eventloop;
import io.activej.inject.Injector;
import io.activej.inject.Key;
import io.activej.inject.Scope;
import io.activej.inject.annotation.Provides;
import io.activej.inject.annotation.ProvidesIntoSet;
import io.activej.inject.binding.Binding;
import io.activej.inject.binding.OptionalDependency;
import io.activej.inject.module.AbstractModule;
import io.activej.inject.util.ScopedKey;
import io.activej.inject.util.Trie;
import io.activej.launcher.LauncherService;
import io.activej.net.ReactiveServer;
import io.activej.reactor.net.BlockingSocketServer;
import io.activej.service.adapter.ServiceAdapter;
import io.activej.service.adapter.ServiceAdapters;
import io.activej.worker.WorkerPool;
import io.activej.worker.WorkerPools;
import io.activej.worker.annotation.Worker;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import javax.sql.DataSource;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Supplier;
import static io.activej.common.Checks.checkState;
import static io.activej.common.Utils.difference;
import static io.activej.common.Utils.intersection;
import static io.activej.common.reflection.ReflectionUtils.isClassPresent;
import static io.activej.inject.binding.BindingType.SYNTHETIC;
import static io.activej.inject.binding.BindingType.TRANSIENT;
import static io.activej.service.Utils.combineAll;
import static io.activej.service.Utils.completedExceptionallyFuture;
import static io.activej.service.adapter.ServiceAdapters.*;
import static java.lang.Thread.currentThread;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.stream.Collectors.*;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Builds dependency graph of {@code Service} objects based on DI's object
* graph. Service graph module is capable to start services concurrently.
*
* Consider some lifecycle details of this module:
*
* -
* Put all objects from the graph which can be treated as
* {@link Service} instances.
*
* -
* Starts services concurrently starting at leaf graph nodes (independent
* services) and ending with root nodes.
*
* -
* Stop services starting from root and ending with independent services.
*
*
*
* An ability to use {@link ServiceAdapter} objects allows to create a service
* from any object by providing it's {@link ServiceAdapter} and registering
* it in {@code ServiceGraphModule}. Take a look at {@link ServiceAdapters},
* which has a lot of implemented adapters. It's necessarily to annotate your
* object provider with {@link Worker @Worker} or Singleton
* annotation.
*
* An application terminates if a circular dependency found.
*/
public final class ServiceGraphModule extends AbstractModule {
private static final Logger logger = getLogger(ServiceGraphModule.class);
private final Map, ServiceAdapter>> registeredServiceAdapters = new LinkedHashMap<>();
private final Set> excludedKeys = new LinkedHashSet<>();
private final Map, ServiceAdapter>> keys = new LinkedHashMap<>();
private final Map, Set>> dependencies = new HashMap<>();
private final Map, Set>> excludedDependencies = new HashMap<>();
private final Executor executor;
private ServiceGraphModule() {
this.executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 10, TimeUnit.MILLISECONDS,
new SynchronousQueue<>());
}
/**
* Creates a builder for service graph module with default configuration, which is able to
* handle {@code Service, BlockingService, AutoCloseable, ExecutorService,
* Timer, DataSource, ReactiveService, ReactiveServer} and
* {@code Eventloop} as services.
*
* @return builder for {@link ServiceGraphModule}
*/
public static Builder builder() {
return new ServiceGraphModule().new Builder()
.with(Service.class, forService())
.with(BlockingService.class, forBlockingService())
.with(AutoCloseable.class, forAutoCloseable())
.with(ExecutorService.class, forExecutorService())
.with(Timer.class, forTimer())
.initialize(b -> {
try {
currentThread().getContextClassLoader().loadClass("javax.sql.DataSource");
b.with(DataSource.class, forDataSource());
} catch (ClassNotFoundException ignored) {
}
})
.initialize(ServiceGraphModule::tryRegisterAsyncComponents);
}
/**
* Creates a default service graph module with default configuration, which is able to
* handle {@code Service, BlockingService, AutoCloseable, ExecutorService,
* Timer, DataSource, ReactiveService, ReactiveServer} and
* {@code Eventloop} as services.
*
* @return default {@link ServiceGraphModule}
*/
public static ServiceGraphModule create() {
return builder().build();
}
public final class Builder extends AbstractBuilder
implements ServiceGraphModuleSettings {
private Builder() {}
/**
* Puts an instance of class and its factory to the builder
*
* @param type of service
* @param type key with which the specified factory is to be associated
* @param factory value to be associated with the specified type
* @return Builder with change
*/
@Override
public Builder with(Class extends T> type, ServiceAdapter factory) {
registeredServiceAdapters.put(type, factory);
return this;
}
/**
* Puts the key and its factory to the builder
*
* @param key key with which the specified factory is to be associated
* @param factory value to be associated with the specified key
* @param type of service
* @return Builder with change
*/
@Override
public Builder withKey(Key key, ServiceAdapter factory) {
keys.put(key, factory);
return this;
}
@Override
public Builder withExcludedKey(Key key) {
excludedKeys.add(key);
return this;
}
/**
* Adds the dependency for key
*
* @param key key for adding dependency
* @param keyDependency key of dependency
* @return Builder with change
*/
@Override
public Builder withDependency(Key> key, Key> keyDependency) {
dependencies.computeIfAbsent(key, key1 -> new HashSet<>()).add(keyDependency);
return this;
}
/**
* Removes the dependency
*
* @param key key for removing dependency
* @param keyDependency key of dependency
* @return Builder with change
*/
@Override
public Builder withExcludedDependency(Key> key, Key> keyDependency) {
excludedDependencies.computeIfAbsent(key, key1 -> new HashSet<>()).add(keyDependency);
return this;
}
@Override
protected ServiceGraphModule doBuild() {
return ServiceGraphModule.this;
}
}
public static final class ServiceKey implements ServiceGraph.Key {
private final Key> key;
private final @Nullable WorkerPool workerPool;
private ServiceKey(Key> key) {
this.key = key;
this.workerPool = null;
}
private ServiceKey(Key> key, @Nullable WorkerPool workerPool) {
this.key = key;
this.workerPool = workerPool;
}
public Key> getKey() {
return key;
}
@Override
public Type getType() {
return key.getType();
}
@Override
public @Nullable String getSuffix() {
return workerPool == null ? null : "" + workerPool.getSize();
}
@Override
public @Nullable String getIndex() {
return workerPool == null || workerPool.getId() == 0 ? null : "" + (workerPool.getId() + 1);
}
@Override
public @Nullable Object getQualifier() {
return key.getQualifier();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ServiceKey other = (ServiceKey) o;
return
workerPool == other.workerPool &&
key.equals(other.key);
}
@Override
public int hashCode() {
return Objects.hash(key, workerPool);
}
@Override
public String toString() {
return key + (workerPool == null ? "" : ":" + workerPool.getId());
}
}
// Registers service adapters for asynchronous components if they are present in classpath
private static void tryRegisterAsyncComponents(Builder builder) {
if (isClassPresent("io.activej.eventloop.Eventloop")) {
// 'eventloop' module is present
builder
.with(Eventloop.class, forEventloop())
.with(ReactiveService.class, forReactiveService());
}
if (isClassPresent("io.activej.net.ReactiveServer")) {
// 'net' module is present
builder
.with(BlockingSocketServer.class, forBlockingSocketServer())
.with(ReactiveServer.class, forReactiveServer());
}
}
@Provides
ServiceGraph serviceGraph(Injector injector) {
ServiceGraph serviceGraph = ServiceGraph.create();
serviceGraph.setStartCallback(() -> doStart(serviceGraph, injector));
return serviceGraph;
}
@ProvidesIntoSet
LauncherService service(ServiceGraph serviceGraph, OptionalDependency>> initializers) {
Builder builder = this.new Builder();
for (Initializer initializer : initializers.orElse(Set.of())) {
initializer.initialize(builder);
}
return new LauncherService() {
@Override
public CompletableFuture> start() {
CompletableFuture future = new CompletableFuture<>();
serviceGraph.startFuture()
.whenComplete(($, e) -> {
if (e == null) {
if (logger.isInfoEnabled()) {
logger.info("Effective ServiceGraph:\n\n{}", serviceGraph);
}
future.complete(null);
} else {
logger.error("Could not start ServiceGraph", e);
if (logger.isInfoEnabled()) {
logger.info("Effective ServiceGraph:\n\n{}", serviceGraph);
}
logger.warn("Stopping services of partially started ServiceGraph...");
serviceGraph.stopFuture()
.whenComplete(($2, e2) -> {
if (e2 != null) {
e.addSuppressed(e2);
}
future.completeExceptionally(e);
});
}
});
return future;
}
@Override
public CompletableFuture> stop() {
logger.info("Stopping ServiceGraph...");
return serviceGraph.stopFuture();
}
};
}
private void doStart(ServiceGraph serviceGraph, Injector injector) {
logger.trace("Initializing ServiceGraph ...");
WorkerPools workerPools = injector.peekInstance(WorkerPools.class);
List pools = workerPools != null ? workerPools.getWorkerPools() : List.of();
Map> instances = new HashMap<>();
Map> instanceDependencies = new HashMap<>();
IdentityHashMap