From 5330089fa131e266f5ab4ca41984aa831507c674 Mon Sep 17 00:00:00 2001 From: Kristian Kraljic Date: Mon, 24 Jun 2024 16:11:56 +0200 Subject: [PATCH] feat: handling for failed deployments during boot Previously any failure during verticle deployment during boot could lead to the bootstrap of NeonBee failing / undeployment of all verticles. This commit introduces a new "bootDeploymentHandling" option in the NeonBeeConfig, which lets the user specify how to deal with failures during verticles / module deployments during boot. The change is compatible with how it is handled currently (FAIL_ON_ERROR), however also provides options to undeploy any failing modules (UNDEPLOY_FAILING) or even keeping modules that could only be partially deployed (KEEP_PARTIAL). --- src/main/java/io/neonbee/NeonBee.java | 64 +++++++++++++++---- .../java/io/neonbee/config/NeonBeeConfig.java | 48 ++++++++++++++ .../neonbee/internal/deploy/Deployables.java | 7 +- 3 files changed, 101 insertions(+), 18 deletions(-) diff --git a/src/main/java/io/neonbee/NeonBee.java b/src/main/java/io/neonbee/NeonBee.java index 506ab0b2..c032d6ef 100644 --- a/src/main/java/io/neonbee/NeonBee.java +++ b/src/main/java/io/neonbee/NeonBee.java @@ -4,6 +4,7 @@ import static io.neonbee.internal.deploy.DeployableVerticle.fromClass; import static io.neonbee.internal.deploy.DeployableVerticle.fromVerticle; import static io.neonbee.internal.deploy.Deployables.allTo; +import static io.neonbee.config.NeonBeeConfig.BootDeploymentHandling.*; import static io.neonbee.internal.deploy.Deployables.anyTo; import static io.neonbee.internal.deploy.Deployables.fromDeployables; import static io.neonbee.internal.helper.ConfigHelper.notFound; @@ -45,6 +46,7 @@ import io.neonbee.config.HealthConfig; import io.neonbee.config.NeonBeeConfig; import io.neonbee.config.ServerConfig; +import io.neonbee.config.NeonBeeConfig.BootDeploymentHandling; import io.neonbee.data.DataException; import io.neonbee.data.DataQuery; import io.neonbee.entity.EntityModelManager; @@ -566,21 +568,25 @@ private Future deploySystemVerticles() { List> requiredVerticles = new ArrayList<>(); requiredVerticles.add(fromClass(vertx, ConsolidationVerticle.class, new JsonObject().put("instances", 1))); requiredVerticles.add(fromClass(vertx, LoggerManagerVerticle.class)); - - List>> optionalVerticles = new ArrayList<>(); if (Optional.ofNullable(config.getHealthConfig()).map(HealthConfig::isEnabled).orElse(true)) { requiredVerticles.add(fromClass(vertx, HealthCheckVerticle.class)); } + + List>> optionalVerticles = new ArrayList<>(); optionalVerticles.add(deployableWatchVerticle(options.getModelsDirectory(), ModelRefreshVerticle::new)); optionalVerticles.add(deployableWatchVerticle(options.getModulesDirectory(), DeployerVerticle::new)); optionalVerticles.add(deployableRedeployEntitiesJobVerticle(options)); LOGGER.info("Deploying system verticles ..."); - return all(List.of(fromDeployables(requiredVerticles).compose(allTo(this)), - all(optionalVerticles).map(CompositeFuture::list).map(optionals -> { - return optionals.stream().map(Optional.class::cast).filter(Optional::isPresent).map(Optional::get) - .map(Deployable.class::cast).toList(); - }).map(Deployables::new).compose(anyTo(this)))).mapEmpty(); + return all(fromDeployables(requiredVerticles).compose(allTo(this)).onFailure(throwable -> { + LOGGER.error("Failed to deploy (some / all) required system verticle(s)", throwable); + }), all(optionalVerticles).map(CompositeFuture::list).map(optionals -> { + return optionals.stream().map(Optional.class::cast).filter(Optional::isPresent).map(Optional::get) + .map(Deployable.class::cast).toList(); + }).map(Deployables::new).compose(anyTo(this)).onFailure(throwable -> { + LOGGER.error("Failed to deploy (some / all) optional system verticle(s), bootstrap will continue", + throwable); + }).otherwiseEmpty()).mapEmpty(); } private Future> deployableWatchVerticle( @@ -621,7 +627,9 @@ private Future> deployableRedeployEntitiesJobVert private Future deployServerVerticle() { LOGGER.info("Deploying server verticle ..."); return fromClass(vertx, ServerVerticle.class, new JsonObject().put("instances", NUMBER_DEFAULT_INSTANCES)) - .compose(deployable -> deployable.deploy(this)).mapEmpty(); + .compose(deployable -> deployable.deploy(this)).onFailure(throwable -> { + LOGGER.error("Failed to deploy server verticle", throwable); + }).mapEmpty(); } /** @@ -638,11 +646,7 @@ private Future deployClassPathVerticles() { return scanForDeployableClasses(vertx).compose(deployableClasses -> fromDeployables(deployableClasses.stream() .filter(verticleClass -> filterByAutoDeployAndProfiles(verticleClass, options.getActiveProfiles())) .map(verticleClass -> fromClass(vertx, verticleClass)).collect(Collectors.toList()))) - .onSuccess(deployables -> { - if (LOGGER.isInfoEnabled()) { - LOGGER.info("Deploy class path verticle(s) {}.", deployables.getIdentifier()); - } - }).compose(allTo(this)).mapEmpty(); + .compose(handleBootDeployment("class path verticle(s)")); } @VisibleForTesting @@ -665,7 +669,39 @@ private Future deployModules() { LOGGER.info("Deploying module(s) ..."); return fromDeployables(moduleJarPaths.stream().map(moduleJarPath -> fromJar(vertx, moduleJarPath)) - .collect(Collectors.toList())).compose(allTo(this)).mapEmpty(); + .collect(Collectors.toList())).compose(handleBootDeployment("module(s)")); + } + + private Function> handleBootDeployment(String deploymentType) { + BootDeploymentHandling handling = config.getBootDeploymentHandling(); + return deployables -> { + // in case we should keep partial deployments, for every deployable that we are about to deploy + // set the keep partial deployment flag, so that in case there is an error we don't undeploy + if (handling == KEEP_PARTIAL) { + for (Deployable deployable : deployables.getDeployables()) { + if (deployable instanceof Deployables) { + ((Deployables) deployable).keepPartialDeployment(); + } + } + } + + return (handling == FAIL_ON_ERROR ? allTo(this) : anyTo(this)).apply(deployables) + .onSuccess(deployments -> { + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Successfully deployed all {} {}", + deploymentType, deployments.getDeploymentId()); + } + }).recover(throwable -> { + if (LOGGER.isErrorEnabled()) { + LOGGER.error("Failed to deploy (some / all) {}{}", + deploymentType, handling == FAIL_ON_ERROR ? "" : ", bootstrap will continue", + throwable); + } + + // abort the boot process if any class path verticle failed to deploy + return handling == FAIL_ON_ERROR ? failedFuture(throwable) : succeededFuture(); + }).mapEmpty(); + }; } @VisibleForTesting diff --git a/src/main/java/io/neonbee/config/NeonBeeConfig.java b/src/main/java/io/neonbee/config/NeonBeeConfig.java index 19012244..e6b78598 100644 --- a/src/main/java/io/neonbee/config/NeonBeeConfig.java +++ b/src/main/java/io/neonbee/config/NeonBeeConfig.java @@ -1,5 +1,6 @@ package io.neonbee.config; +import static io.neonbee.config.NeonBeeConfig.BootDeploymentHandling.FAIL_ON_ERROR; import static io.neonbee.internal.helper.ConfigHelper.notFound; import static io.neonbee.internal.helper.ConfigHelper.readConfig; import static io.neonbee.internal.helper.ConfigHelper.rephraseConfigNames; @@ -44,6 +45,31 @@ @DataObject @JsonGen(publicConverter = false) public class NeonBeeConfig { + /** + * How (non-system related) deployments like verticles and modules are handled during booting up {@link NeonBee}. + */ + public enum BootDeploymentHandling { + /** + * Abort the {@link NeonBee} boot, if any (non-system related) verticle / module deployment fails to be + * deployed. + */ + FAIL_ON_ERROR, + + /** + * Log any failure of a (non-system related) verticle / module deployments, but continue booting up + * {@link NeonBee} otherwise, while discarding / undeploying any partial deployments (i.e. if a module fails to + * deploy, all deployables related to that module will be undeployed again). + */ + UNDEPLOY_FAILING, + + /** + * Log any failure of a (non-system related) verticle / module deployments, but continue booting up + * {@link NeonBee} otherwise, while keeping partial deployments (i.e. if a module fails to deploy, some + * verticles might still stay deployed). + */ + KEEP_PARTIAL + } + /** * The default timeout for an event bus request. */ @@ -74,6 +100,8 @@ public class NeonBeeConfig { private int eventBusTimeout = DEFAULT_EVENT_BUS_TIMEOUT; + private BootDeploymentHandling bootDeploymentHandling = FAIL_ON_ERROR; + private int deploymentTimeout = DEFAULT_DEPLOYMENT_TIMEOUT; private Integer modelsDeploymentTimeout; @@ -258,6 +286,26 @@ public NeonBeeConfig setEventBusTimeout(int eventBusTimeout) { return this; } + /** + * Gets how {@link NeonBee} handles failures deploying verticles / modules during boot. + * + * @return the selected boot deployment handling method + */ + public BootDeploymentHandling getBootDeploymentHandling() { + return bootDeploymentHandling; + } + + /** + * Sets how {@link NeonBee} should handle failures deploying verticles / modules during boot. + * + * @param bootDeploymentHandling the selected boot deployment handling method + * @return the {@linkplain NeonBeeConfig} for fluent use + */ + public NeonBeeConfig setBootDeploymentHandling(BootDeploymentHandling bootDeploymentHandling) { + this.bootDeploymentHandling = bootDeploymentHandling; + return this; + } + /** * Returns the general deployment timeout for an individual deployment of any type in seconds. If unset / equal or * smaller than 0, no timeout applies to the deployment. diff --git a/src/main/java/io/neonbee/internal/deploy/Deployables.java b/src/main/java/io/neonbee/internal/deploy/Deployables.java index d0354c38..65da8c6f 100644 --- a/src/main/java/io/neonbee/internal/deploy/Deployables.java +++ b/src/main/java/io/neonbee/internal/deploy/Deployables.java @@ -167,10 +167,9 @@ protected Future undeploy(String deploymentId) { getDeployables().stream().map(deployable -> deployable.deploy(neonBee)).forEach(pendingDeployments::add); // when we should keep partial deployments use a joinComposite, so we wait for all deployments to finish - // independent if a single one fails or not. in case we should not keep partial deployments (default) use - // allComposite here, which will fail, when one deployment fails, and thus we can start undeploying all - // succeeded - // (or to be succeeded pending deployments) as unfortunately there is no way to cancel active deployments + // independent if a single one fails. in case we should not keep partial deployments (default) use allComposite + // here, which will fail, when one deployment fails, and thus we can start undeploying all succeeded (or to be + // succeeded pending deployments) as unfortunately there is no way to cancel active deployments (keepPartialDeployment ? Future.join(pendingDeployments) : Future.all(pendingDeployments)) .onComplete(deployPromise);