io.zeebe.servicecontainer.impl.ServiceController Maven / Gradle / Ivy
/*
* Copyright © 2017 camunda services GmbH ([email protected])
*
* 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.zeebe.servicecontainer.impl;
import io.zeebe.servicecontainer.CompositeServiceBuilder;
import io.zeebe.servicecontainer.Injector;
import io.zeebe.servicecontainer.Service;
import io.zeebe.servicecontainer.ServiceBuilder;
import io.zeebe.servicecontainer.ServiceGroupReference;
import io.zeebe.servicecontainer.ServiceInterruptedException;
import io.zeebe.servicecontainer.ServiceName;
import io.zeebe.servicecontainer.ServiceStartContext;
import io.zeebe.servicecontainer.ServiceStopContext;
import io.zeebe.servicecontainer.impl.ServiceEvent.ServiceEventType;
import io.zeebe.util.sched.Actor;
import io.zeebe.util.sched.ActorScheduler;
import io.zeebe.util.sched.channel.ConcurrentQueueChannel;
import io.zeebe.util.sched.future.ActorFuture;
import io.zeebe.util.sched.future.CompletableActorFuture;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.agrona.concurrent.ManyToOneConcurrentLinkedQueue;
import org.slf4j.Logger;
@SuppressWarnings("rawtypes")
public class ServiceController extends Actor {
public static final Logger LOG = Loggers.SERVICE_CONTAINER_LOGGER;
public static final boolean IS_TRACE_ENABLED = LOG.isTraceEnabled();
private final AwaitDependenciesStartedState awaitDependenciesStartedState =
new AwaitDependenciesStartedState();
private final AwaitStartState awaitStartState = new AwaitStartState();
private final StartedState startedState = new StartedState();
private final AwaitDependentsStopped awaitDependentsStopped = new AwaitDependentsStopped();
private final AwaitStopState awaitStopState = new AwaitStopState();
private final RemovedState removedState = new RemovedState();
private final ConcurrentQueueChannel channel =
new ConcurrentQueueChannel<>(new ManyToOneConcurrentLinkedQueue<>());
private final ServiceContainerImpl container;
private final ServiceName name;
private final ServiceName> groupName;
private final Service service;
private final Set> dependencies;
private final Map, Collection>> injectors;
private final Map, ServiceGroupReference>> injectedReferences;
private final CompletableActorFuture stopFuture = new CompletableActorFuture<>();
private final CompletableActorFuture startFuture;
private List resolvedDependencies;
private StartContextImpl startContext;
private StopContextImpl stopContext;
private Consumer state = awaitDependenciesStartedState;
public ServiceController(
ServiceBuilder> builder,
ServiceContainerImpl serviceContainer,
CompletableActorFuture startFuture) {
this.container = serviceContainer;
this.startFuture = startFuture;
this.service = builder.getService();
this.name = builder.getName();
this.groupName = builder.getGroupName();
this.injectors = builder.getInjectedDependencies();
this.dependencies = builder.getDependencies();
this.injectedReferences = builder.getInjectedReferences();
}
@Override
public String getName() {
return "service-controller";
}
@Override
protected void onActorStarted() {
actor.consume(channel, this::onServiceEvent);
container.getChannel().add(new ServiceEvent(ServiceEventType.SERVICE_INSTALLED, this));
}
@Override
protected void onActorClosed() {
stopFuture.complete(null);
}
private void onServiceEvent() {
final ServiceEvent event = channel.poll();
if (event != null) {
if (IS_TRACE_ENABLED) {
LOG.trace("Got {} in state {}", event, state.getClass().getSimpleName());
}
state.accept(event);
} else {
actor.yield();
}
}
private void logIgnoringEvent(final ServiceEvent event) {
LOG.warn("Ignoring event {} in state {}", event.getType(), state.getClass().getSimpleName());
}
@SuppressWarnings("unchecked")
class AwaitDependenciesStartedState implements Consumer {
@Override
public void accept(ServiceEvent evt) {
switch (evt.getType()) {
case DEPENDENCIES_AVAILABLE:
onDependenciesAvailable(evt);
break;
case DEPENDENCIES_UNAVAILABLE:
onDependenciesUnAvailable(evt);
break;
case SERVICE_STOPPING:
onStopping();
break;
default:
logIgnoringEvent(evt);
break;
}
}
private void onDependenciesUnAvailable(ServiceEvent evt) {
state = removedState;
fireEvent(ServiceEventType.SERVICE_REMOVED);
}
private void onStopping() {
state = removedState;
fireEvent(ServiceEventType.SERVICE_REMOVED);
}
public void onDependenciesAvailable(ServiceEvent evt) {
resolvedDependencies = (List) evt.getPayload();
// inject dependencies
for (ServiceController serviceController : resolvedDependencies) {
final Collection> injectos =
injectors.getOrDefault(serviceController.name, Collections.emptyList());
for (Injector injector : injectos) {
injector.inject(serviceController.service.get());
injector.setInjectedServiceName(serviceController.name);
}
}
// invoke start
state = awaitStartState;
startContext = new StartContextImpl();
try {
service.start(startContext);
if (startContext.action != null) {
actor.runBlocking(startContext.action, startContext);
}
if (!startContext.isAsync()) {
fireEvent(ServiceEventType.SERVICE_STARTED);
}
} catch (Exception e) {
fireEvent(ServiceEventType.SERVICE_START_FAILED, e);
}
}
}
class AwaitStartState implements Consumer {
boolean stopAfterStarted = false;
@Override
public void accept(ServiceEvent t) {
switch (t.getType()) {
case SERVICE_STARTED:
onStarted();
break;
case SERVICE_START_FAILED:
onStartFailed((Throwable) t.getPayload());
break;
case DEPENDENCIES_UNAVAILABLE:
case DEPENDENTS_STOPPED:
case SERVICE_STOPPING:
if (startContext.isInterruptible()) {
startFuture.completeExceptionally(
new ServiceInterruptedException(String.format("Service %s was interrupted", name)));
invokeStop(true);
} else {
stopAfterStarted = true;
}
break;
default:
logIgnoringEvent(t);
break;
}
}
@SuppressWarnings("unchecked")
public void onStarted() {
if (stopAfterStarted) {
startFuture.completeExceptionally(
new RuntimeException(
String.format("Could not start service %s" + " removed while starting", name)));
invokeStop(false);
} else {
state = startedState;
startFuture.complete(getService().get());
}
}
public void onStartFailed(Throwable t) {
LOG.error("Service failed to start while in AwaitStartState", t);
startFuture.completeExceptionally(t);
state = awaitStopState;
fireEvent(ServiceEventType.SERVICE_STOPPED);
}
}
class StartedState implements Consumer {
@Override
public void accept(ServiceEvent t) {
switch (t.getType()) {
case DEPENDENCIES_UNAVAILABLE:
onDependenciesUnavailable();
break;
case SERVICE_STOPPING:
onServiceStopping();
break;
default:
logIgnoringEvent(t);
break;
}
}
public void onDependenciesUnavailable() {
fireEvent(ServiceEventType.SERVICE_STOPPING);
state = awaitDependentsStopped;
}
public void onServiceStopping() {
state = awaitDependentsStopped;
}
}
class AwaitDependentsStopped implements Consumer {
@Override
public void accept(ServiceEvent t) {
if (t.getType() == ServiceEventType.DEPENDENTS_STOPPED) {
invokeStop(false);
}
}
}
class AwaitStopState implements Consumer {
@Override
public void accept(ServiceEvent t) {
if (t.getType() == ServiceEventType.SERVICE_STOPPED) {
injectors.values().stream().flatMap(Collection::stream).forEach(i -> i.uninject());
fireEvent(ServiceEventType.SERVICE_REMOVED);
state = removedState;
}
}
}
class RemovedState implements Consumer {
@Override
public void accept(ServiceEvent t) {
if (t.getType() == ServiceEventType.SERVICE_REMOVED) {
actor.close();
}
}
}
private void invokeStop(boolean interrupted) {
state = awaitStopState;
if (startContext != null) {
startContext.invalidate();
}
stopContext = new StopContextImpl();
stopContext.wasInterrupted = interrupted;
try {
service.stop(stopContext);
if (stopContext.action != null) {
actor.runBlocking(stopContext.action, stopContext);
}
if (!stopContext.isAsync()) {
fireEvent(ServiceEventType.SERVICE_STOPPED);
}
} catch (Throwable t) {
LOG.error("Exception while stopping service {}: {}", this, t);
fireEvent(ServiceEventType.SERVICE_STOPPED);
}
}
class StartContextImpl implements ServiceStartContext, Consumer {
final Set> dependentServices = new HashSet<>();
boolean isValid = true;
boolean isAsync = false;
boolean isInterruptible = false;
boolean stopOnCompletion = false;
Runnable action;
public void invalidate() {
isValid = false;
startContext = null;
}
@Override
public ServiceName> getServiceName() {
return name;
}
@Override
@SuppressWarnings("unchecked")
public S getService(ServiceName name) {
validCheck();
dependencyCheck(name);
return (S) resolvedDependencies.stream().filter((c) -> c.name.equals(name)).findFirst();
}
@Override
public S getService(String name, Class type) {
validCheck();
return getService(ServiceName.newServiceName(name, type));
}
@Override
public String getName() {
return name.getName();
}
@Override
@SuppressWarnings("unchecked")
public ServiceBuilder createService(ServiceName name, Service service) {
validCheck();
dependentServices.add(name);
return new ServiceBuilder<>(name, service, container).dependency(ServiceController.this.name);
}
@Override
public CompositeServiceBuilder createComposite(ServiceName name) {
validCheck();
dependentServices.add(name);
return new CompositeServiceBuilder(name, container, ServiceController.this.name);
}
@Override
public ActorFuture removeService(ServiceName name) {
validCheck();
if (!dependentServices.contains(name)) {
final Optional contoller =
resolvedDependencies.stream().filter((c) -> c.name.equals(name)).findFirst();
if (!contoller.isPresent()) {
final String errorMessage =
String.format(
"Cannot remove service '%s' from context '%s'. Can only remove dependencies and services started through this context.",
name, ServiceController.this.name);
return CompletableActorFuture.completedExceptionally(
new IllegalArgumentException(errorMessage));
}
}
return container.removeService(name);
}
@Override
public void async(ActorFuture> future, boolean interruptible) {
validCheck();
notAsyncCheck();
isAsync = true;
isInterruptible = interruptible;
actor.runOnCompletion(future, (v, t) -> accept(t));
}
@Override
public void run(Runnable action) {
validCheck();
notAsyncCheck();
isAsync = true;
this.action = action;
}
void validCheck() {
if (!isValid) {
throw new IllegalStateException("Service Context is invalid");
}
}
void dependencyCheck(ServiceName> name) {
if (!dependencies.contains(name)) {
final String errorMessage =
String.format(
"Cannot get service '%s' from context '%s'. Requested Service is not a dependency.",
name, ServiceController.this.name);
throw new IllegalArgumentException(errorMessage);
}
}
boolean isAsync() {
validCheck();
return isAsync;
}
boolean isInterruptible() {
validCheck();
return isInterruptible;
}
private void notAsyncCheck() {
if (isAsync) {
throw new IllegalStateException(
"Context is already async. Cannnot call asyc() more than once.");
}
}
@Override
public void accept(Throwable u) {
if (u == null) {
fireEvent(ServiceEventType.SERVICE_STARTED);
} else {
fireEvent(ServiceEventType.SERVICE_START_FAILED, u);
}
}
@Override
public ActorScheduler getScheduler() {
validCheck();
return container.getActorScheduler();
}
@Override
public boolean hasService(ServiceName name) {
validCheck();
return container.hasService(name);
}
}
class StopContextImpl implements ServiceStopContext, Consumer {
boolean isValid = true;
boolean isAsync = false;
Runnable action;
boolean wasInterrupted = false;
@Override
public boolean wasInterrupted() {
return wasInterrupted;
}
@Override
public void async(ActorFuture> future) {
validCheck();
notAsyncCheck();
isAsync = true;
actor.runOnCompletion(future, (v, t) -> accept(t));
}
@Override
public void run(Runnable action) {
validCheck();
notAsyncCheck();
isAsync = true;
this.action = action;
}
void validCheck() {
if (!isValid) {
throw new IllegalStateException("Service Context is invalid");
}
}
boolean isAsync() {
validCheck();
return isAsync;
}
private void notAsyncCheck() {
if (isAsync) {
throw new IllegalStateException(
"Context is already async. Cannnot call asyc() more than once.");
}
}
@Override
public void accept(Throwable u) {
fireEvent(ServiceEventType.SERVICE_STOPPED);
}
}
// API & Cmds ////////////////////////////////////////////////
@Override
public String toString() {
return String.format("%s in %s", name, state.getClass().getSimpleName());
}
private void fireEvent(ServiceEventType evtType) {
fireEvent(evtType, null);
}
private void fireEvent(ServiceEventType evtType, Object payload) {
final ServiceEvent event = new ServiceEvent(evtType, this, payload);
channel.add(event);
container.getChannel().add(event);
}
public ConcurrentQueueChannel getChannel() {
return channel;
}
public Set> getDependencies() {
return dependencies;
}
public ActorFuture remove() {
actor.run(
() -> {
fireEvent(ServiceEventType.SERVICE_STOPPING);
});
return stopFuture;
}
public ServiceName> getGroupName() {
return groupName;
}
public ServiceName> getServiceName() {
return name;
}
public Map, ServiceGroupReference>> getInjectedReferences() {
return injectedReferences;
}
public Service getService() {
return service;
}
public void addReferencedValue(ServiceGroupReference ref, ServiceName name, Object value) {
actor.call(
() -> {
invoke(ref.getAddHandler(), name, value);
});
}
public void removeReferencedValue(ServiceGroupReference ref, ServiceName name, Object value) {
actor.call(
() -> {
invoke(ref.getRemoveHandler(), name, value);
});
}
@SuppressWarnings("unchecked")
private static void invoke(BiConsumer consumer, ServiceName name, Object value) {
consumer.accept(name, value);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy