diff --git a/src/main/java/io/neonbee/NeonBee.java b/src/main/java/io/neonbee/NeonBee.java index 4dac5313..d28f8bb5 100644 --- a/src/main/java/io/neonbee/NeonBee.java +++ b/src/main/java/io/neonbee/NeonBee.java @@ -27,6 +27,7 @@ import java.util.TimeZone; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -52,6 +53,7 @@ import io.neonbee.health.HealthCheckProvider; import io.neonbee.health.HealthCheckRegistry; import io.neonbee.health.MemoryHealthCheck; +import io.neonbee.health.NeonBeeStartHealthCheck; import io.neonbee.health.internal.HealthCheck; import io.neonbee.hook.HookRegistry; import io.neonbee.hook.HookType; @@ -188,6 +190,8 @@ public class NeonBee { private final CompositeMeterRegistry compositeMeterRegistry; + private final AtomicBoolean started = new AtomicBoolean(); + /** * Convenience method for returning the current NeonBee instance. *

@@ -301,6 +305,7 @@ static Future create(Function> vertxFactory Future neonBeeFuture = configFuture.map(loadedConfig -> { return new NeonBee(vertx, options, loadedConfig, compositeMeterRegistry); }); + // boot NeonBee, on failure close Vert.x return neonBeeFuture.compose(NeonBee::boot).recover(closeVertx).compose(unused -> neonBeeFuture); } catch (Throwable t) { @@ -373,8 +378,10 @@ private Future boot() { // further synchronous initializations which should happen before verticles are getting deployed }).compose(nothing -> all(initializeSharedMaps(), decorateEventBus(), createMicrometerRegistries())) - .compose(nothing -> all(deployVerticles(), deployModules())) // deployment of verticles & modules .compose(nothing -> registerHealthChecks()) + .compose(nothing -> all(deployVerticles(), deployModules())) // deployment of verticles & modules + // startup & booting procedure has completed, set started to true and call the after startup hook(s) + .onSuccess(nothing -> started.set(true)) .compose(nothing -> hookRegistry.executeHooks(HookType.AFTER_STARTUP)) .onSuccess(result -> LOGGER.info("Successfully booted NeonBee (ID: {}})!", nodeId)).mapEmpty(); } @@ -382,13 +389,14 @@ private Future boot() { /** * Registers default NeonBee health checks to the {@link HealthCheckRegistry}. * - * @return a Future + * @return a future to indicate if all health checks have been registered */ @VisibleForTesting Future registerHealthChecks() { List> healthChecks = new ArrayList<>(); if (Optional.ofNullable(config.getHealthConfig()).map(HealthConfig::isEnabled).orElse(true)) { + healthChecks.add(healthRegistry.register(new NeonBeeStartHealthCheck(this))); healthChecks.add(healthRegistry.register(new MemoryHealthCheck(this))); healthChecks.add(healthRegistry.register(new EventLoopHealthCheck(this))); @@ -852,6 +860,15 @@ public HealthCheckRegistry getHealthCheckRegistry() { return healthRegistry; } + /** + * Indicating if the starting boot sequence of NeonBee has completed. + * + * @return true if NeonBee is started + */ + public boolean isStarted() { + return started.get(); + } + /** * Hidden marker function interface, that indicates to the boot-stage that an own Vert.x instance was created, and * we must be held responsible to close it again. diff --git a/src/main/java/io/neonbee/health/NeonBeeStartHealthCheck.java b/src/main/java/io/neonbee/health/NeonBeeStartHealthCheck.java new file mode 100644 index 00000000..78a37177 --- /dev/null +++ b/src/main/java/io/neonbee/health/NeonBeeStartHealthCheck.java @@ -0,0 +1,44 @@ +package io.neonbee.health; + +import java.util.function.Function; + +import io.neonbee.NeonBee; +import io.vertx.core.Handler; +import io.vertx.core.Promise; +import io.vertx.ext.healthchecks.Status; + +/** + * A health check in order for NeonBee to only become healthy if the booting procedure succeeded. + */ +public class NeonBeeStartHealthCheck extends AbstractHealthCheck { + /** + * Name of the health check. + */ + public static final String NAME = "neonbee.start"; + + /** + * Constructs an instance of {@link NeonBeeStartHealthCheck}. + * + * @param neonBee the current NeonBee instance + */ + public NeonBeeStartHealthCheck(NeonBee neonBee) { + super(neonBee); + } + + @Override + public String getId() { + return NAME; + } + + @Override + public boolean isGlobal() { + return false; + } + + @Override + public Function>> createProcedure() { + return neonBee -> healthCheckPromise -> { + healthCheckPromise.complete(new Status().setOk(neonBee.isStarted())); + }; + } +} diff --git a/src/test/java/io/neonbee/NeonBeeTest.java b/src/test/java/io/neonbee/NeonBeeTest.java index 4cf782bf..90f21138 100644 --- a/src/test/java/io/neonbee/NeonBeeTest.java +++ b/src/test/java/io/neonbee/NeonBeeTest.java @@ -64,6 +64,7 @@ import io.neonbee.health.HealthCheckProvider; import io.neonbee.health.HealthCheckRegistry; import io.neonbee.health.MemoryHealthCheck; +import io.neonbee.health.NeonBeeStartHealthCheck; import io.neonbee.health.internal.HealthCheck; import io.neonbee.internal.NeonBeeModuleJar; import io.neonbee.internal.ReplyInboundInterceptor; @@ -235,10 +236,11 @@ void testRegisterAndUnregisterLocalConsumer() { @DisplayName("NeonBee should register all default health checks") void testRegisterDefaultHealthChecks() { Map registeredChecks = getNeonBee().getHealthCheckRegistry().getHealthChecks(); - assertThat(registeredChecks.size()).isEqualTo(2); + assertThat(registeredChecks.size()).isEqualTo(3); String nodePrefix = "node." + getNeonBee().getNodeId() + "."; - assertThat(registeredChecks.containsKey(nodePrefix + EventLoopHealthCheck.NAME)).isTrue(); - assertThat(registeredChecks.containsKey(nodePrefix + MemoryHealthCheck.NAME)).isTrue(); + assertThat(registeredChecks).containsKey(nodePrefix + NeonBeeStartHealthCheck.NAME); + assertThat(registeredChecks).containsKey(nodePrefix + EventLoopHealthCheck.NAME); + assertThat(registeredChecks).containsKey(nodePrefix + MemoryHealthCheck.NAME); } @Test @@ -250,8 +252,8 @@ void testRegisterClusterHealthChecks(VertxTestContext testContext) { .onSuccess(newVertx -> vertx = newVertx), HAZELCAST.factory(), options, null) .onComplete(testContext.succeeding(neonBee -> testContext.verify(() -> { Map registeredChecks = neonBee.getHealthCheckRegistry().getHealthChecks(); - assertThat(registeredChecks.size()).isEqualTo(3); - assertThat(registeredChecks.containsKey(HazelcastClusterHealthCheck.NAME)).isTrue(); + assertThat(registeredChecks.size()).isEqualTo(4); + assertThat(registeredChecks).containsKey(HazelcastClusterHealthCheck.NAME); testContext.completeNow(); }))); } @@ -267,8 +269,8 @@ void testRegisterSpiAndDefaultHealthChecks(VertxTestContext testContext) { runWithMetaInfService(HealthCheckProvider.class, DummyHealthCheckProvider.class.getName(), testContext, () -> { getNeonBee().registerHealthChecks().onComplete(testContext.succeeding(v -> testContext.verify(() -> { Map registeredChecks = registry.getHealthChecks(); - assertThat(registeredChecks.size()).isEqualTo(3); - assertThat(registeredChecks.containsKey(DummyHealthCheck.DUMMY_ID)).isTrue(); + assertThat(registeredChecks.size()).isEqualTo(4); + assertThat(registeredChecks).containsKey(DummyHealthCheck.DUMMY_ID); testContext.completeNow(); }))); }); diff --git a/src/test/java/io/neonbee/health/NeonBeeStartHealthCheckTest.java b/src/test/java/io/neonbee/health/NeonBeeStartHealthCheckTest.java new file mode 100644 index 00000000..38976bde --- /dev/null +++ b/src/test/java/io/neonbee/health/NeonBeeStartHealthCheckTest.java @@ -0,0 +1,31 @@ +package io.neonbee.health; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.neonbee.NeonBee; +import io.vertx.core.Promise; +import io.vertx.ext.healthchecks.Status; + +class NeonBeeStartHealthCheckTest { + @Test + @DisplayName("Should return true if NeonBee was started") + void testStarted() { + NeonBee neonBeeMock = mock(NeonBee.class); + NeonBeeStartHealthCheck healthCheck = new NeonBeeStartHealthCheck(neonBeeMock); + + when(neonBeeMock.isStarted()).thenReturn(false); + Promise promise1 = Promise.promise(); + healthCheck.createProcedure().apply(neonBeeMock).handle(promise1); + assertThat(promise1.future().result().isOk()).isFalse(); + + when(neonBeeMock.isStarted()).thenReturn(true); + Promise promise2 = Promise.promise(); + healthCheck.createProcedure().apply(neonBeeMock).handle(promise2); + assertThat(promise2.future().result().isOk()).isTrue(); + } +}