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();
+ }
+}