Skip to content

Commit

Permalink
feat: make health checks addable via SPI
Browse files Browse the repository at this point in the history
  • Loading branch information
s4heid committed Jul 7, 2022
1 parent 151cf6e commit 040618d
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 34 deletions.
11 changes: 10 additions & 1 deletion src/main/java/io/neonbee/NeonBee.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
Expand All @@ -45,6 +46,7 @@
import io.neonbee.entity.EntityModelManager;
import io.neonbee.entity.EntityWrapper;
import io.neonbee.health.HazelcastClusterHealthCheck;
import io.neonbee.health.HealthCheckProvider;
import io.neonbee.health.HealthCheckRegistry;
import io.neonbee.health.MemoryHealthCheck;
import io.neonbee.health.internal.HealthCheck;
Expand Down Expand Up @@ -328,7 +330,8 @@ private Future<Void> boot() {
*
* @return a Future
*/
private Future<Void> registerHealthChecks() {
@VisibleForTesting
Future<Void> registerHealthChecks() {
List<Future<HealthCheck>> healthChecks = new ArrayList<>();

if (Optional.ofNullable(config.getHealthConfig()).map(HealthConfig::isEnabled).orElse(true)) {
Expand All @@ -337,6 +340,12 @@ private Future<Void> registerHealthChecks() {
if (options.isClustered()) {
healthChecks.add(healthRegistry.register(new HazelcastClusterHealthCheck(this, clusterManager)));
}

ServiceLoader.load(HealthCheckProvider.class).forEach(provider -> provider.get(vertx).forEach(check -> {
if (!healthRegistry.getHealthChecks().containsKey(check.getId())) {
healthChecks.add(healthRegistry.register(check));
}
}));
}

return joinComposite(healthChecks).recover(v -> {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/neonbee/health/AbstractHealthCheck.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public AbstractHealthCheck(NeonBee neonBee) {
*
* @return a function which returns a handler with a Status
*/
abstract Function<NeonBee, Handler<Promise<Status>>> createProcedure();
public abstract Function<NeonBee, Handler<Promise<Status>>> createProcedure();

@Override
public long getRetentionTime() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public boolean isGlobal() {
}

@Override
Function<NeonBee, Handler<Promise<Status>>> createProcedure() {
public Function<NeonBee, Handler<Promise<Status>>> createProcedure() {
return neonBee -> healthCheckPromise -> neonBee.getVertx().executeBlocking(promise -> {
HazelcastInstance instance = clusterManager.getHazelcastInstance();
boolean lifecycleServiceRunning = instance.getLifecycleService().isRunning();
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/io/neonbee/health/HealthCheckProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.neonbee.health;

import java.util.List;

import io.vertx.core.Vertx;

/**
* This interface can be used to provide additional health checks to NeonBee. If you implement this interface, NeonBee
* discovers the implementing class and registers all health checks of the list to NeonBee's health check registry.
*/
@FunctionalInterface
public interface HealthCheckProvider {

/**
* Provide custom health checks that will be registered to NeonBee's health check registry.
*
* @param vertx the current Vert.x instance
* @return a succeeded future if registering was successful. A failed Future, otherwise.
*/
List<AbstractHealthCheck> get(Vertx vertx);
}
2 changes: 1 addition & 1 deletion src/main/java/io/neonbee/health/MemoryHealthCheck.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public boolean isGlobal() {
}

@Override
Function<NeonBee, Handler<Promise<Status>>> createProcedure() {
public Function<NeonBee, Handler<Promise<Status>>> createProcedure() {
return neonBee -> healthCheckPromise -> {
long usedMemory = memoryStats.getUsedHeap();
double memoryUsedOfTotalPercentage = (PERCENTAGE_MULTIPLIER * usedMemory) / memoryStats.getCommittedHeap();
Expand Down
83 changes: 83 additions & 0 deletions src/test/java/io/neonbee/NeonBeeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import static io.neonbee.NeonBeeProfile.STABLE;
import static io.neonbee.internal.helper.StringHelper.EMPTY;
import static io.neonbee.test.helper.OptionsHelper.defaultOptions;
import static io.neonbee.test.helper.ResourceHelper.TEST_RESOURCES;
import static io.vertx.core.Future.failedFuture;
import static io.vertx.core.Future.succeededFuture;
import static org.mockito.ArgumentMatchers.any;
Expand All @@ -22,8 +23,13 @@

import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
Expand All @@ -44,6 +50,13 @@
import org.mockito.Mockito;

import io.neonbee.config.NeonBeeConfig;
import io.neonbee.health.DummyHealthCheck;
import io.neonbee.health.DummyHealthCheckProvider;
import io.neonbee.health.HazelcastClusterHealthCheck;
import io.neonbee.health.HealthCheckProvider;
import io.neonbee.health.HealthCheckRegistry;
import io.neonbee.health.MemoryHealthCheck;
import io.neonbee.health.internal.HealthCheck;
import io.neonbee.internal.NeonBeeModuleJar;
import io.neonbee.internal.tracking.MessageDirection;
import io.neonbee.internal.tracking.TrackingDataLoggingStrategy;
Expand Down Expand Up @@ -79,6 +92,10 @@ protected void adaptOptions(TestInfo testInfo, NeonBeeOptions.Mutable options) {
options.addActiveProfile(CORE);
options.setIgnoreClassPath(false);
break;
case "testRegisterClusterHealthChecks":
options.setClustered(true);
options.setClusterConfigResource("hazelcast-local.xml");
break;
case "testDeployModule":
try {
options.setModuleJarPaths(
Expand Down Expand Up @@ -202,6 +219,72 @@ void testRegisterAndUnregisterLocalConsumer() {
assertThat(getNeonBee().isLocalConsumerAvailable(address)).isFalse();
}

@Test
@DisplayName("NeonBee should register all default health checks")
void testRegisterDefaultHealthChecks() {
Map<String, HealthCheck> registeredChecks = getNeonBee().getHealthCheckRegistry().getHealthChecks();
assertThat(registeredChecks.size()).isEqualTo(1);
assertThat(registeredChecks.containsKey("node." + getNeonBee().getNodeId() + "." + MemoryHealthCheck.NAME))
.isTrue();
}

@Test
@DisplayName("NeonBee should register all cluster + default health checks if started clustered")
void testRegisterClusterHealthChecks() {
Map<String, HealthCheck> registeredChecks = getNeonBee().getHealthCheckRegistry().getHealthChecks();
assertThat(registeredChecks.size()).isEqualTo(2);
assertThat(registeredChecks.containsKey(HazelcastClusterHealthCheck.NAME)).isTrue();
}

@Test
@Timeout(value = 1, timeUnit = TimeUnit.SECONDS)
@DisplayName("NeonBee should register all SPI-provided + default health checks")
void testRegisterSpiAndDefaultHealthChecks(VertxTestContext testContext) {
HealthCheckRegistry registry = getNeonBee().getHealthCheckRegistry();
Set<String> healthCheckMap = registry.getHealthChecks().keySet();
for (String checkId : healthCheckMap) {
registry.unregister(checkId);
}

runWithMetaInfService(HealthCheckProvider.class, DummyHealthCheckProvider.class.getName(), testContext, () -> {
getNeonBee().registerHealthChecks().onComplete(testContext.succeeding(v -> testContext.verify(() -> {
Map<String, HealthCheck> registeredChecks = registry.getHealthChecks();
assertThat(registeredChecks.size()).isEqualTo(2);
assertThat(
registeredChecks.containsKey("node." + getNeonBee().getNodeId() + "." + MemoryHealthCheck.NAME))
.isTrue();
assertThat(registeredChecks.containsKey(DummyHealthCheck.DUMMY_ID)).isTrue();
testContext.completeNow();
})));
});
}

private void runWithMetaInfService(Class<?> service, String content, VertxTestContext context, Runnable runnable) {
Path providerPath = TEST_RESOURCES.resolve("META-INF/services/" + service.getName());
ClassLoader classLoader;
try {
Files.write(providerPath, content.getBytes(StandardCharsets.UTF_8));
classLoader = new URLClassLoader(new URL[] { TEST_RESOURCES.resolve(".").toUri().toURL() });
} catch (IOException e) {
context.failNow(e);
return;
}
Thread thread = Thread.currentThread();
ClassLoader original = thread.getContextClassLoader();
thread.setContextClassLoader(classLoader);

try {
runnable.run();
} finally {
thread.setContextClassLoader(original);
try {
Files.deleteIfExists(providerPath);
} catch (IOException e) {
context.failNow(e);
}
}
}

@Test
@DisplayName("NeonBee should have a (unique) node id")
void testGetNodeId() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ void testConfigRetrievalFails(VertxTestContext testContext) {
private AbstractHealthCheck createDummyHealthCheck(boolean global, boolean ok, JsonObject data) {
return new AbstractHealthCheck(NeonBee.get(vertxMock)) {
@Override
Function<NeonBee, Handler<Promise<Status>>> createProcedure() {
public Function<NeonBee, Handler<Promise<Status>>> createProcedure() {
return nb -> p -> p.complete(new Status().setOk(ok).setData(data));
}

Expand Down
39 changes: 39 additions & 0 deletions src/test/java/io/neonbee/health/DummyHealthCheck.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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;

public class DummyHealthCheck extends AbstractHealthCheck {
/**
* Name of the health check.
*/
public static final String DUMMY_ID = "dummy";

/**
* Constructs an instance of {@link AbstractHealthCheck}.
*
* @param neonBee the current NeonBee instance
*/
public DummyHealthCheck(NeonBee neonBee) {
super(neonBee);
}

@Override
public Function<NeonBee, Handler<Promise<Status>>> createProcedure() {
return nb -> promise -> promise.complete(new Status().setOK());
}

@Override
public String getId() {
return DUMMY_ID;
}

@Override
public boolean isGlobal() {
return true;
}
}
13 changes: 13 additions & 0 deletions src/test/java/io/neonbee/health/DummyHealthCheckProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.neonbee.health;

import java.util.List;

import io.neonbee.NeonBee;
import io.vertx.core.Vertx;

public class DummyHealthCheckProvider implements HealthCheckProvider {
@Override
public List<AbstractHealthCheck> get(Vertx vertx) {
return List.of(new DummyHealthCheck(NeonBee.get(vertx)));
}
}
33 changes: 4 additions & 29 deletions src/test/java/io/neonbee/health/HealthCheckRegistryTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.neonbee.health;

import static com.google.common.truth.Truth.assertThat;
import static io.neonbee.health.DummyHealthCheck.DUMMY_ID;
import static io.neonbee.test.helper.OptionsHelper.defaultOptions;
import static io.neonbee.test.helper.ReflectionHelper.setValueOfPrivateField;
import static org.junit.jupiter.api.Assertions.assertThrows;
Expand Down Expand Up @@ -33,8 +34,6 @@
import io.neonbee.internal.verticle.HealthCheckVerticle;
import io.neonbee.test.helper.DeploymentHelper;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
Expand All @@ -47,34 +46,10 @@

@ExtendWith(VertxExtension.class)
class HealthCheckRegistryTest {

private static final String DUMMY_ID = "dummy-group.check-0";

private static final long RETENTION_TIME = 12L;

private NeonBee neonBee;

private static class DummyCheck extends AbstractHealthCheck {
DummyCheck(NeonBee neonBee) {
super(neonBee);
}

@Override
Function<NeonBee, Handler<Promise<Status>>> createProcedure() {
return nb -> p -> p.complete(new Status().setOK());
}

@Override
public String getId() {
return DUMMY_ID;
}

@Override
public boolean isGlobal() {
return true;
}
}

@BeforeEach
@Timeout(value = 2, timeUnit = TimeUnit.SECONDS)
void setUp(Vertx vertx) {
Expand Down Expand Up @@ -168,7 +143,7 @@ void testCustomConfigDisabled(Vertx vertx) throws HealthCheckException {
void testUnregister(VertxTestContext testContext) {
HealthCheckRegistry registry = neonBee.getHealthCheckRegistry();
registry.healthChecks = spy(registry.healthChecks);
AbstractHealthCheck check = new DummyCheck(neonBee);
AbstractHealthCheck check = new DummyHealthCheck(neonBee);

registry.register(check).compose(v -> {
testContext.verify(() -> {
Expand Down Expand Up @@ -234,7 +209,7 @@ public Future<JsonArray> retrieveData(DataQuery query, DataContext context) {
DeploymentHelper.undeployAllVerticlesOfClass(neonBee.getVertx(), HealthCheckVerticle.class)
.compose(v -> AsyncHelper.allComposite(
List.of(vertx.deployVerticle(healthCheckVerticle1), vertx.deployVerticle(healthCheckVerticle2),
neonBee.getHealthCheckRegistry().register(new DummyCheck(neonBee)))))
neonBee.getHealthCheckRegistry().register(new DummyHealthCheck(neonBee)))))
.onSuccess(v -> {
neonBee.getHealthCheckRegistry().collectHealthCheckResults()
.onComplete(testContext.succeeding(result -> testContext.verify(() -> {
Expand Down Expand Up @@ -266,7 +241,7 @@ Future<List<JsonObject>> getLocalHealthCheckResults() {
}
};

mock.register(new DummyCheck(neonBee)).compose(hc -> mock.collectHealthCheckResults())
mock.register(new DummyHealthCheck(neonBee)).compose(hc -> mock.collectHealthCheckResults())
.onComplete(testContext.succeeding(result -> testContext.verify(() -> {
Function<String, Long> matchingNameCount = id -> result.getJsonArray("checks").stream()
.filter(c -> ((JsonObject) c).getString("id").equals(id)).count();
Expand Down

0 comments on commit 040618d

Please sign in to comment.