Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make health checks addable via SPI #140

Merged
merged 1 commit into from
Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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