io.camunda.zeebe.scheduler.startup.StartupProcess Maven / Gradle / Ivy
The newest version!
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
* one or more contributor license agreements. See the NOTICE file distributed
* with this work for additional information regarding copyright ownership.
* Licensed under the Camunda License 1.0. You may not use this file
* except in compliance with the Camunda License 1.0.
*/
package io.camunda.zeebe.scheduler.startup;
import static java.util.Collections.singletonList;
import io.camunda.zeebe.scheduler.ConcurrencyControl;
import io.camunda.zeebe.scheduler.future.ActorFuture;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Executes a number of steps in a startup/shutdown process.
*
* On startup, steps are executed in the given order. If any step completes exceptionally, then
* the subsequent steps are not executed and the startup future completes exceptionally. However, no
* shutdown is triggered by this class. This can be done by the caller.
*
*
On shutdown, steps are executed in reverse order. If any shutdown step completes
* exceptionally, subsequent steps will be executed and the exceptions of all steps are collected as
* suppressed exceptions
*
*
Error handling
*
*
* - Exceptions that occur during startup/shutdown are propagated via the {@code ActorFuture}.
* They are also wrapped.
*
- If the exceptions is related to a certain step, it is wrapped in a {@code
* StartupProcessStepException}, which is then added as suppressed exception to a {@code
* StartupProcessException}, which is returned by the future
*
- If the exception is not related to a step (e.g. startup is aborted due to concurrent
* shutdown), this is propagated as a plain {@code StartupProcessException}
*
- {@code}IllegalStateExceptions are not propagated via the future but thrown directly to the
* caller
*
*
* Callers of this class must obey the following contract:
*
*
* - Shutdown must not be called before startup
*
- Startup must be called at most once
*
- Shutdown may be called more than once. The first call will trigger the shutdown and any
* subsequent calls do nothing
*
- Shutdown may be called before the future of startup has completed. In that case, it will
* complete the current running startup step, cancel all subsequent startup step, complete the
* startup future with an exception and start the shutdown from the step that last completed
*
*
* @param the startup/shutdown context
*/
public final class StartupProcess {
private final Logger logger;
private final Queue> steps;
private final Deque> startedSteps = new ArrayDeque<>();
private boolean startupCalled = false;
private ActorFuture shutdownFuture;
private ActorFuture startupFuture;
/**
* Constructs the startup process
*
* @param steps the steps to execute; must not be {@code null}
*/
public StartupProcess(final List> steps) {
this(LoggerFactory.getLogger(StartupProcess.class), steps);
}
/**
* Constructs the startup process
*
* @param logger the logger to use for messages related to the startup process; must not be {@code
* null}
* @param steps the steps to execute; must not be {@code null}
*/
public StartupProcess(final Logger logger, final List extends StartupStep> steps) {
this.steps = new ArrayDeque<>(Objects.requireNonNull(steps));
this.logger = Objects.requireNonNull(logger);
}
/**
* Executes the startup logic
*
* @param concurrencyControl the startup context at the start of this step
* @param context the startup context at the start of this step
* @return future with startup context at the end of this step
*/
public ActorFuture startup(
final ConcurrencyControl concurrencyControl, final CONTEXT context) {
final var result = concurrencyControl.createFuture();
concurrencyControl.run(() -> startupSynchronized(concurrencyControl, context, result));
return result;
}
/**
* Executes the shutdown logic
*
* @param context the shutdown context at the start of this step
* @return future with the shutdown context at the end of this step.
*/
public ActorFuture shutdown(
final ConcurrencyControl concurrencyControl, final CONTEXT context) {
final var result = concurrencyControl.createFuture();
concurrencyControl.run(() -> shutdownSynchronized(concurrencyControl, context, result));
return result;
}
private void startupSynchronized(
final ConcurrencyControl concurrencyControl,
final CONTEXT context,
final ActorFuture startupFuture) {
logger.debug("Startup was called with context: {}", context);
if (startupCalled) {
throw new IllegalStateException("startup(...) must only be called once");
}
startupCalled = true;
this.startupFuture = startupFuture;
// reset future when we are done
concurrencyControl.runOnCompletion(startupFuture, (result, error) -> this.startupFuture = null);
final var stepsToStart = new ArrayDeque<>(steps);
proceedWithStartupSynchronized(concurrencyControl, stepsToStart, context, startupFuture);
}
private void proceedWithStartupSynchronized(
final ConcurrencyControl concurrencyControl,
final Queue> stepsToStart,
final CONTEXT context,
final ActorFuture startupFuture) {
if (stepsToStart.isEmpty()) {
startupFuture.complete(context);
logger.debug("Finished startup process");
} else if (shutdownFuture != null) {
logger.info("Aborting startup process because shutdown was called");
startupFuture.completeExceptionally(
new StartupProcessShutdownException(
"Aborting startup process because shutdown was called"));
} else {
final var stepToStart = stepsToStart.poll();
startedSteps.push(stepToStart);
logCurrentStepSynchronized("Startup", stepToStart);
final var stepStartupFuture = stepToStart.startup(context);
concurrencyControl.runOnCompletion(
stepStartupFuture,
(contextReturnedByStep, error) -> {
if (error != null) {
completeStartupFutureExceptionallySynchronized(startupFuture, stepToStart, error);
} else {
proceedWithStartupSynchronized(
concurrencyControl, stepsToStart, contextReturnedByStep, startupFuture);
}
});
}
}
private void completeStartupFutureExceptionallySynchronized(
final ActorFuture startupFuture,
final StartupStep stepToStart,
final Throwable error) {
logger.warn(
"Aborting startup process due to exception during step " + stepToStart.getName(), error);
startupFuture.completeExceptionally(
aggregateExceptionsSynchronized(
"Startup",
singletonList(new StartupProcessStepException(stepToStart.getName(), error))));
}
private void shutdownSynchronized(
final ConcurrencyControl concurrencyControl,
final CONTEXT context,
final ActorFuture resultFuture) {
logger.debug("Shutdown was called with context: {}", context);
if (shutdownFuture == null) {
shutdownFuture = resultFuture;
if (startupFuture != null) {
concurrencyControl.runOnCompletion(
startupFuture,
(contextReturnedByStartup, error) -> {
final var contextForShutdown = error == null ? contextReturnedByStartup : context;
proceedWithShutdownSynchronized(
concurrencyControl, contextForShutdown, shutdownFuture, new ArrayList<>());
});
} else {
proceedWithShutdownSynchronized(
concurrencyControl, context, shutdownFuture, new ArrayList<>());
}
} else {
logger.info("Shutdown already in progress");
concurrencyControl.runOnCompletion(
shutdownFuture,
(contextReturnedByShutdown, error) -> {
if (error != null) {
resultFuture.completeExceptionally(error);
} else {
resultFuture.complete(contextReturnedByShutdown);
}
});
}
}
private void proceedWithShutdownSynchronized(
final ConcurrencyControl concurrencyControl,
final CONTEXT context,
final ActorFuture shutdownFuture,
final List collectedExceptions) {
if (startedSteps.isEmpty()) {
completeShutdownFutureSynchronized(context, shutdownFuture, collectedExceptions);
} else {
final var stepToShutdown = startedSteps.pop();
logCurrentStepSynchronized("Shutdown", stepToShutdown);
final var shutdownStepFuture = stepToShutdown.shutdown(context);
concurrencyControl.runOnCompletion(
shutdownStepFuture,
(contextReturnedByShutdown, error) -> {
final CONTEXT contextToUse;
if (error != null) {
collectedExceptions.add(
new StartupProcessStepException(stepToShutdown.getName(), error));
contextToUse = context;
} else {
contextToUse = contextReturnedByShutdown;
}
proceedWithShutdownSynchronized(
concurrencyControl, contextToUse, shutdownFuture, collectedExceptions);
});
}
}
private void completeShutdownFutureSynchronized(
final CONTEXT context,
final ActorFuture shutdownFuture,
final List collectedExceptions) {
if (collectedExceptions.isEmpty()) {
shutdownFuture.complete(context);
logger.debug("Finished shutdown process");
} else {
final var umbrellaException =
aggregateExceptionsSynchronized("Shutdown", collectedExceptions);
shutdownFuture.completeExceptionally(umbrellaException);
logger.warn(umbrellaException.getMessage(), umbrellaException);
}
}
private Throwable aggregateExceptionsSynchronized(
final String operation, final List exceptions) {
final var failedSteps =
exceptions.stream()
.map(StartupProcessStepException::getStepName)
.collect(Collectors.toList());
final var message =
String.format(
"%s failed in the following steps: %s. See suppressed exceptions for details.",
operation, failedSteps);
final var exception = new StartupProcessException(message);
exceptions.forEach(exception::addSuppressed);
return exception;
}
private void logCurrentStepSynchronized(final String process, final StartupStep step) {
logger.info(process + " " + step.getName());
}
}