diff --git a/core/common/boot/src/main/java/org/eclipse/edc/boot/BootServicesExtension.java b/core/common/boot/src/main/java/org/eclipse/edc/boot/BootServicesExtension.java index f05be213fd4..f72afc3986b 100644 --- a/core/common/boot/src/main/java/org/eclipse/edc/boot/BootServicesExtension.java +++ b/core/common/boot/src/main/java/org/eclipse/edc/boot/BootServicesExtension.java @@ -15,13 +15,11 @@ package org.eclipse.edc.boot; import org.eclipse.edc.boot.apiversion.ApiVersionServiceImpl; -import org.eclipse.edc.boot.health.HealthCheckServiceConfiguration; import org.eclipse.edc.boot.health.HealthCheckServiceImpl; import org.eclipse.edc.boot.system.ExtensionLoader; import org.eclipse.edc.boot.vault.InMemoryVault; import org.eclipse.edc.runtime.metamodel.annotation.BaseExtension; import org.eclipse.edc.runtime.metamodel.annotation.Extension; -import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Provider; import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.security.Vault; @@ -33,7 +31,6 @@ import org.eclipse.edc.spi.telemetry.Telemetry; import java.time.Clock; -import java.time.Duration; @BaseExtension @@ -42,26 +39,12 @@ public class BootServicesExtension implements ServiceExtension { public static final String NAME = "Boot Services"; - @Setting - public static final String LIVENESS_PERIOD_SECONDS_SETTING = "edc.core.system.health.check.liveness-period"; - @Setting - public static final String STARTUP_PERIOD_SECONDS_SETTING = "edc.core.system.health.check.startup-period"; - @Setting - public static final String READINESS_PERIOD_SECONDS_SETTING = "edc.core.system.health.check.readiness-period"; - @Setting - public static final String THREADPOOL_SIZE_SETTING = "edc.core.system.health.check.threadpool-size"; @Setting(value = "Configures the participant id this runtime is operating on behalf of") public static final String PARTICIPANT_ID = "edc.participant.id"; @Setting(value = "Configures the runtime id", defaultValue = "") public static final String RUNTIME_ID = "edc.runtime.id"; - private static final long DEFAULT_DURATION = 60; - private static final int DEFAULT_TP_SIZE = 3; - - @Inject - private ExecutorInstrumentation instrumentation; - private HealthCheckServiceImpl healthCheckService; @Override @@ -71,20 +54,9 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { - var config = getHealthCheckConfig(context); - healthCheckService = new HealthCheckServiceImpl(config, instrumentation); + healthCheckService = new HealthCheckServiceImpl(); } - @Override - public void start() { - healthCheckService.start(); - } - - @Override - public void shutdown() { - healthCheckService.stop(); - ServiceExtension.super.shutdown(); - } @Provider public Clock clock() { @@ -122,15 +94,5 @@ public ApiVersionService apiVersionService() { return new ApiVersionServiceImpl(); } - private HealthCheckServiceConfiguration getHealthCheckConfig(ServiceExtensionContext context) { - return HealthCheckServiceConfiguration.Builder.newInstance() - .livenessPeriod(Duration.ofSeconds(context.getSetting(LIVENESS_PERIOD_SECONDS_SETTING, DEFAULT_DURATION))) - .startupStatusPeriod(Duration.ofSeconds(context.getSetting(STARTUP_PERIOD_SECONDS_SETTING, DEFAULT_DURATION))) - .readinessPeriod(Duration.ofSeconds(context.getSetting(READINESS_PERIOD_SECONDS_SETTING, DEFAULT_DURATION))) - .readinessPeriod(Duration.ofSeconds(context.getSetting(READINESS_PERIOD_SECONDS_SETTING, DEFAULT_DURATION))) - .threadPoolSize(context.getSetting(THREADPOOL_SIZE_SETTING, DEFAULT_TP_SIZE)) - .build(); - } - } diff --git a/core/common/boot/src/main/java/org/eclipse/edc/boot/system/runtime/BaseRuntime.java b/core/common/boot/src/main/java/org/eclipse/edc/boot/system/runtime/BaseRuntime.java index c2ac2a326f7..f7cd633ccc5 100644 --- a/core/common/boot/src/main/java/org/eclipse/edc/boot/system/runtime/BaseRuntime.java +++ b/core/common/boot/src/main/java/org/eclipse/edc/boot/system/runtime/BaseRuntime.java @@ -150,7 +150,7 @@ protected List> createExtensions(ServiceExt * this would likely need to be overridden. * * @param monitor a Monitor - * @param config the cofiguratiohn + * @param config the cofiguratiohn * @return a {@code ServiceExtensionContext} */ @NotNull @@ -184,13 +184,12 @@ private void boot(boolean addShutdownHook) { } if (context.hasService(HealthCheckService.class)) { - var startupStatus = new AtomicReference<>(HealthCheckResult.failed("Startup not complete")); + var statusbuilder = HealthCheckResult.Builder.newInstance().component("BaseRuntime"); + var startupStatus = new AtomicReference<>(statusbuilder.failure("Startup not complete").build()); var healthCheckService = context.getService(HealthCheckService.class); healthCheckService.addStartupStatusProvider(startupStatus::get); - startupStatus.set(HealthCheckResult.success()); - - healthCheckService.refresh(); + startupStatus.set(statusbuilder.success().build()); } } catch (Exception e) { diff --git a/core/common/boot/src/test/java/org/eclipse/edc/boot/system/runtime/BaseRuntimeTest.java b/core/common/boot/src/test/java/org/eclipse/edc/boot/system/runtime/BaseRuntimeTest.java index 6557c130368..a5ed4175199 100644 --- a/core/common/boot/src/test/java/org/eclipse/edc/boot/system/runtime/BaseRuntimeTest.java +++ b/core/common/boot/src/test/java/org/eclipse/edc/boot/system/runtime/BaseRuntimeTest.java @@ -47,6 +47,16 @@ public class BaseRuntimeTest { private final ServiceLocator serviceLocator = mock(); private final BaseRuntime runtime = new BaseRuntimeFixture(monitor, serviceLocator); + @NotNull + private static ServiceExtension registerService(Class serviceClass, HealthCheckService healthCheckService) { + return new ServiceExtension() { + @Override + public void initialize(ServiceExtensionContext context) { + context.registerService(serviceClass, healthCheckService); + } + }; + } + @Test void baseRuntime_shouldBoot() { when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(List.of(new BaseExtension())); @@ -76,7 +86,6 @@ void shouldSetStartupCheckProvider_whenHealthCheckServiceIsRegistered() { runtime.boot(); verify(healthCheckService).addStartupStatusProvider(any()); - verify(healthCheckService).refresh(); } @Test @@ -88,16 +97,6 @@ void shouldLoadConfiguration() { verify(serviceLocator).loadImplementors(ConfigurationExtension.class, false); } - @NotNull - private static ServiceExtension registerService(Class serviceClass, HealthCheckService healthCheckService) { - return new ServiceExtension() { - @Override - public void initialize(ServiceExtensionContext context) { - context.registerService(serviceClass, healthCheckService); - } - }; - } - private static class BaseRuntimeFixture extends BaseRuntime { private final Monitor monitor; diff --git a/core/common/lib/boot-lib/src/main/java/org/eclipse/edc/boot/health/HealthCheckServiceConfiguration.java b/core/common/lib/boot-lib/src/main/java/org/eclipse/edc/boot/health/HealthCheckServiceConfiguration.java deleted file mode 100644 index 4d8318633bb..00000000000 --- a/core/common/lib/boot-lib/src/main/java/org/eclipse/edc/boot/health/HealthCheckServiceConfiguration.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2022 Amadeus - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Amadeus - Initial implementation - * - */ - -package org.eclipse.edc.boot.health; - -import org.eclipse.edc.spi.system.health.LivenessProvider; -import org.eclipse.edc.spi.system.health.ReadinessProvider; -import org.eclipse.edc.spi.system.health.StartupStatusProvider; - -import java.time.Duration; - -public class HealthCheckServiceConfiguration { - public static final long DEFAULT_PERIOD_SECONDS = 60; - public static final int DEFAULT_THREADPOOL_SIZE = 3; - private int threadPoolSize = DEFAULT_THREADPOOL_SIZE; - private Duration readinessPeriod = Duration.ofSeconds(DEFAULT_PERIOD_SECONDS); - private Duration livenessPeriod = Duration.ofSeconds(DEFAULT_PERIOD_SECONDS); - private Duration startupStatusPeriod = Duration.ofSeconds(DEFAULT_PERIOD_SECONDS); - - /** - * how many threads should be used by the health check service for periodic polling - */ - public int getThreadPoolSize() { - return threadPoolSize; - } - - /** - * Time delay between before {@link ReadinessProvider}s are checked again. - * Defaults to 10 seconds. - */ - public Duration getReadinessPeriod() { - return readinessPeriod; - } - - /** - * Time delay between before {@link LivenessProvider}s are checked again. - * Defaults to 10 seconds. - */ - public Duration getLivenessPeriod() { - return livenessPeriod; - } - - /** - * Time delay between before {@link StartupStatusProvider}s are checked again. - * Defaults to 10 seconds. - */ - public Duration getStartupStatusPeriod() { - return startupStatusPeriod; - } - - public static final class Builder { - private final HealthCheckServiceConfiguration config; - - private Builder() { - config = new HealthCheckServiceConfiguration(); - } - - public static Builder newInstance() { - return new Builder(); - } - - public Builder readinessPeriod(Duration readinessPeriod) { - config.readinessPeriod = readinessPeriod; - return this; - } - - public Builder livenessPeriod(Duration livenessPeriod) { - config.livenessPeriod = livenessPeriod; - return this; - } - - public Builder startupStatusPeriod(Duration startupStatusPeriod) { - config.startupStatusPeriod = startupStatusPeriod; - return this; - } - - public Builder threadPoolSize(int threadPoolSize) { - config.threadPoolSize = threadPoolSize; - return this; - } - - public HealthCheckServiceConfiguration build() { - return config; - } - } -} diff --git a/core/common/lib/boot-lib/src/main/java/org/eclipse/edc/boot/health/HealthCheckServiceImpl.java b/core/common/lib/boot-lib/src/main/java/org/eclipse/edc/boot/health/HealthCheckServiceImpl.java index ea62083d154..aa329e25ba2 100644 --- a/core/common/lib/boot-lib/src/main/java/org/eclipse/edc/boot/health/HealthCheckServiceImpl.java +++ b/core/common/lib/boot-lib/src/main/java/org/eclipse/edc/boot/health/HealthCheckServiceImpl.java @@ -14,21 +14,17 @@ package org.eclipse.edc.boot.health; -import org.eclipse.edc.spi.system.ExecutorInstrumentation; import org.eclipse.edc.spi.system.health.HealthCheckResult; import org.eclipse.edc.spi.system.health.HealthCheckService; import org.eclipse.edc.spi.system.health.HealthStatus; import org.eclipse.edc.spi.system.health.LivenessProvider; import org.eclipse.edc.spi.system.health.ReadinessProvider; import org.eclipse.edc.spi.system.health.StartupStatusProvider; +import org.jetbrains.annotations.NotNull; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.function.Supplier; public class HealthCheckServiceImpl implements HealthCheckService { @@ -36,27 +32,11 @@ public class HealthCheckServiceImpl implements HealthCheckService { private final List readinessProviders; private final List startupStatusProviders; - private final Map cachedLivenessResults; - private final Map cachedReadinessResults; - private final Map cachedStartupStatus; - private final ScheduledExecutorService executor; - private final HealthCheckServiceConfiguration configuration; - - public HealthCheckServiceImpl(HealthCheckServiceConfiguration configuration, - ExecutorInstrumentation executorInstrumentation) { - this.configuration = configuration; + public HealthCheckServiceImpl() { readinessProviders = new CopyOnWriteArrayList<>(); livenessProviders = new CopyOnWriteArrayList<>(); startupStatusProviders = new CopyOnWriteArrayList<>(); - - cachedLivenessResults = new ConcurrentHashMap<>(); - cachedReadinessResults = new ConcurrentHashMap<>(); - cachedStartupStatus = new ConcurrentHashMap<>(); - - executor = executorInstrumentation.instrument( - Executors.newScheduledThreadPool(configuration.getThreadPoolSize()), - HealthCheckService.class.getSimpleName()); } @Override @@ -76,57 +56,26 @@ public void addStartupStatusProvider(StartupStatusProvider provider) { @Override public HealthStatus isLive() { - return new HealthStatus(cachedLivenessResults.values()); + return new HealthStatus(livenessProviders.stream().map(getSilent()).toList()); } @Override public HealthStatus isReady() { - return new HealthStatus(cachedReadinessResults.values()); + return new HealthStatus(readinessProviders.stream().map(getSilent()).toList()); } @Override public HealthStatus getStartupStatus() { - return new HealthStatus(cachedStartupStatus.values()); + return new HealthStatus(startupStatusProviders.stream().map(getSilent()).toList()); } - @Override - public void refresh() { - executor.execute(this::queryReadiness); - executor.execute(this::queryLiveness); - executor.execute(this::queryStartupStatus); + private @NotNull Function, HealthCheckResult> getSilent() { + return supplier -> { + try { + return supplier.get(); + } catch (Exception e) { + return HealthCheckResult.Builder.newInstance().component(supplier.getClass().getName()).failure(e.getMessage()).build(); + } + }; } - - public void stop() { - if (!executor.isShutdown()) { - executor.shutdownNow(); - } - } - - public void start() { - //todo: maybe providers should provide their desired timeout instead of a global config? - executor.scheduleAtFixedRate(this::queryReadiness, 0, configuration.getReadinessPeriod().toMillis(), TimeUnit.MILLISECONDS); - executor.scheduleAtFixedRate(this::queryLiveness, 0, configuration.getLivenessPeriod().toMillis(), TimeUnit.MILLISECONDS); - executor.scheduleAtFixedRate(this::queryStartupStatus, 0, configuration.getStartupStatusPeriod().toMillis(), TimeUnit.MILLISECONDS); - } - - private void queryReadiness() { - readinessProviders.parallelStream().forEach(provider -> updateCache(provider, cachedReadinessResults)); - } - - private void queryLiveness() { - livenessProviders.parallelStream().forEach(provider -> updateCache(provider, cachedLivenessResults)); - } - - private void queryStartupStatus() { - startupStatusProviders.parallelStream().forEach(provider -> updateCache(provider, cachedStartupStatus)); - } - - private > void updateCache(T provider, Map cache) { - try { - cache.put(provider, provider.get()); - } catch (Exception ex) { - cache.put(provider, HealthCheckResult.failed(ex.getMessage())); - } - } - } diff --git a/core/common/lib/boot-lib/src/test/java/org/eclipse/edc/boot/health/HealthCheckServiceImplTest.java b/core/common/lib/boot-lib/src/test/java/org/eclipse/edc/boot/health/HealthCheckServiceImplTest.java index 1570761cbce..018d034999b 100644 --- a/core/common/lib/boot-lib/src/test/java/org/eclipse/edc/boot/health/HealthCheckServiceImplTest.java +++ b/core/common/lib/boot-lib/src/test/java/org/eclipse/edc/boot/health/HealthCheckServiceImplTest.java @@ -14,18 +14,13 @@ package org.eclipse.edc.boot.health; -import org.awaitility.Awaitility; -import org.eclipse.edc.spi.system.ExecutorInstrumentation; import org.eclipse.edc.spi.system.health.HealthCheckResult; import org.eclipse.edc.spi.system.health.LivenessProvider; import org.eclipse.edc.spi.system.health.ReadinessProvider; import org.eclipse.edc.spi.system.health.StartupStatusProvider; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.time.Duration; - import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; @@ -35,195 +30,123 @@ class HealthCheckServiceImplTest { - private static final Duration PERIOD = Duration.ofMillis(500); - private static final Duration POLL_INTERVAL = Duration.ofMillis(50); - private static final Duration AWAIT_TIMEOUT = Duration.ofSeconds(10); + private final HealthCheckResult.Builder statusBuilder = HealthCheckResult.Builder.newInstance().component("test status"); private HealthCheckServiceImpl service; @BeforeEach void setup() { - var config = HealthCheckServiceConfiguration.Builder.newInstance() - .livenessPeriod(PERIOD) - .readinessPeriod(PERIOD) - .startupStatusPeriod(PERIOD) - .build(); - service = new HealthCheckServiceImpl(config, ExecutorInstrumentation.noop()); - service.start(); - } - - @AfterEach - void tearDown() { - service.stop(); + service = new HealthCheckServiceImpl(); } @Test void isLive() { - LivenessProvider lpm = mock(LivenessProvider.class); + var lpm = mock(LivenessProvider.class); when(lpm.get()).thenReturn(successResult()); service.addLivenessProvider(lpm); - - Awaitility.await().pollInterval(POLL_INTERVAL) - .atMost(AWAIT_TIMEOUT) - .untilAsserted(() -> { - assertThat(service.isLive().isHealthy()).isTrue(); - verify(lpm, atLeastOnce()).get(); - verifyNoMoreInteractions(lpm); - }); + assertThat(service.isLive().isHealthy()).isTrue(); + verify(lpm, atLeastOnce()).get(); + verifyNoMoreInteractions(lpm); } @Test void isLive_throwsException() { - LivenessProvider lpm = mock(LivenessProvider.class); - when(lpm.get()).thenReturn(successResult()).thenThrow(new RuntimeException("test exception")); + var lpm = mock(LivenessProvider.class); + when(lpm.get()).thenThrow(new RuntimeException("test exception")); service.addLivenessProvider(lpm); - Awaitility.await().pollInterval(POLL_INTERVAL) - .atMost(AWAIT_TIMEOUT) - .untilAsserted(() -> { - assertThat(service.isLive().isHealthy()).isFalse(); - verify(lpm, atLeastOnce()).get(); - verifyNoMoreInteractions(lpm); - }); + assertThat(service.isLive().isHealthy()).isFalse(); + verify(lpm, atLeastOnce()).get(); + verifyNoMoreInteractions(lpm); } @Test void isLive_failed() { - LivenessProvider lpm = mock(LivenessProvider.class); + var lpm = mock(LivenessProvider.class); when(lpm.get()).thenReturn(failedResult()); service.addLivenessProvider(lpm); - - Awaitility.await().pollInterval(POLL_INTERVAL) - .atMost(AWAIT_TIMEOUT) - .untilAsserted(() -> { - assertThat(service.isLive().isHealthy()).isFalse(); - verify(lpm, atLeastOnce()).get(); - verifyNoMoreInteractions(lpm); - }); + assertThat(service.isLive().isHealthy()).isFalse(); + verify(lpm, atLeastOnce()).get(); + verifyNoMoreInteractions(lpm); } @Test void isReady() { - ReadinessProvider provider = mock(ReadinessProvider.class); + var provider = mock(ReadinessProvider.class); when(provider.get()).thenReturn(successResult()); service.addReadinessProvider(provider); - Awaitility.await().pollInterval(POLL_INTERVAL) - .atMost(AWAIT_TIMEOUT) - .untilAsserted(() -> { - assertThat(service.isReady().isHealthy()).isTrue(); + assertThat(service.isReady().isHealthy()).isTrue(); - verify(provider, atLeastOnce()).get(); - verifyNoMoreInteractions(provider); - }); + verify(provider, atLeastOnce()).get(); + verifyNoMoreInteractions(provider); } @Test void isReady_throwsException() { - ReadinessProvider provider = mock(ReadinessProvider.class); - when(provider.get()).thenReturn(successResult()).thenThrow(new RuntimeException("test-exception")); + var provider = mock(ReadinessProvider.class); + when(provider.get()).thenThrow(new RuntimeException("test-exception")); service.addReadinessProvider(provider); - Awaitility.await().pollInterval(POLL_INTERVAL) - .atMost(AWAIT_TIMEOUT) - .untilAsserted(() -> { - assertThat(service.isReady().isHealthy()).isFalse(); - - verify(provider, atLeastOnce()).get(); - verifyNoMoreInteractions(provider); - }); + assertThat(service.isReady().isHealthy()).isFalse(); + verify(provider, atLeastOnce()).get(); + verifyNoMoreInteractions(provider); } @Test void isReady_failed() { - ReadinessProvider provider = mock(ReadinessProvider.class); + var provider = mock(ReadinessProvider.class); when(provider.get()).thenReturn(failedResult()); service.addReadinessProvider(provider); - Awaitility.await().pollInterval(POLL_INTERVAL) - .atMost(AWAIT_TIMEOUT) - .untilAsserted(() -> { - assertThat(service.isReady().isHealthy()).isFalse(); - - verify(provider, atLeastOnce()).get(); - verifyNoMoreInteractions(provider); - }); + assertThat(service.isReady().isHealthy()).isFalse(); + verify(provider, atLeastOnce()).get(); + verifyNoMoreInteractions(provider); } @Test void hasStartupFinished() { - StartupStatusProvider provider = mock(StartupStatusProvider.class); + var provider = mock(StartupStatusProvider.class); when(provider.get()).thenReturn(successResult()); service.addStartupStatusProvider(provider); - Awaitility.await().pollInterval(POLL_INTERVAL) - .atMost(AWAIT_TIMEOUT) - .untilAsserted(() -> { - assertThat(service.getStartupStatus().isHealthy()).isTrue(); - - verify(provider, atLeastOnce()).get(); - verifyNoMoreInteractions(provider); - }); - - } - - @Test - void cacheCanBeRefreshed() { - StartupStatusProvider provider = mock(StartupStatusProvider.class); - when(provider.get()).thenReturn(failedResult(), successResult()); - service.addStartupStatusProvider(provider); - - service.refresh(); - - Awaitility.await().pollInterval(POLL_INTERVAL) - .atMost(PERIOD.multipliedBy(2)) - .untilAsserted(() -> { - assertThat(service.getStartupStatus().isHealthy()).isTrue(); + assertThat(service.getStartupStatus().isHealthy()).isTrue(); - verify(provider, atLeastOnce()).get(); - verifyNoMoreInteractions(provider); - }); + verify(provider, atLeastOnce()).get(); + verifyNoMoreInteractions(provider); } @Test void hasStartupFinished_throwsException() { - StartupStatusProvider provider = mock(StartupStatusProvider.class); - when(provider.get()).thenReturn(successResult()).thenThrow(new RuntimeException("test-exception")); + var provider = mock(StartupStatusProvider.class); + when(provider.get()).thenThrow(new RuntimeException("test-exception")); service.addStartupStatusProvider(provider); - Awaitility.await().pollInterval(POLL_INTERVAL) - .atMost(AWAIT_TIMEOUT) - .untilAsserted(() -> { - assertThat(service.getStartupStatus().isHealthy()).isFalse(); + assertThat(service.getStartupStatus().isHealthy()).isFalse(); - verify(provider, atLeastOnce()).get(); - verifyNoMoreInteractions(provider); - }); + verify(provider, atLeastOnce()).get(); + verifyNoMoreInteractions(provider); } @Test void hasStartupFinished_failed() { - StartupStatusProvider provider = mock(StartupStatusProvider.class); + var provider = mock(StartupStatusProvider.class); when(provider.get()).thenReturn(failedResult()); service.addStartupStatusProvider(provider); - Awaitility.await().pollInterval(POLL_INTERVAL) - .atMost(AWAIT_TIMEOUT) - .untilAsserted(() -> { - assertThat(service.getStartupStatus().isHealthy()).isFalse(); + assertThat(service.getStartupStatus().isHealthy()).isFalse(); - verify(provider, atLeastOnce()).get(); - verifyNoMoreInteractions(provider); - }); + verify(provider, atLeastOnce()).get(); + verifyNoMoreInteractions(provider); } private HealthCheckResult failedResult() { - return HealthCheckResult.failed("test-error"); + return statusBuilder.failure("test-error").build(); } private HealthCheckResult successResult() { - return HealthCheckResult.success(); + return statusBuilder.success().build(); } } \ No newline at end of file diff --git a/extensions/common/api/api-observability/src/test/java/org/eclipse/edc/api/observability/ObservabilityApiControllerTest.java b/extensions/common/api/api-observability/src/test/java/org/eclipse/edc/api/observability/ObservabilityApiControllerTest.java index baa3c378d56..8d6fba4fdb1 100644 --- a/extensions/common/api/api-observability/src/test/java/org/eclipse/edc/api/observability/ObservabilityApiControllerTest.java +++ b/extensions/common/api/api-observability/src/test/java/org/eclipse/edc/api/observability/ObservabilityApiControllerTest.java @@ -22,6 +22,11 @@ import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; import org.junit.jupiter.api.Test; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + import static io.restassured.RestAssured.given; import static io.restassured.http.ContentType.JSON; import static org.mockito.Mockito.mock; @@ -37,7 +42,7 @@ class ObservabilityApiControllerTest extends RestControllerTestBase { @Test void checkHealth() { - when(healthCheckService.getStartupStatus()).thenReturn(new HealthStatus(HealthCheckResult.success())); + when(healthCheckService.getStartupStatus()).thenReturn(new HealthStatus(successResult())); baseRequest() .get("/health") @@ -51,7 +56,7 @@ void checkHealth() { @Test void checkHealth_mixedResults() { - when(healthCheckService.getStartupStatus()).thenReturn(new HealthStatus(HealthCheckResult.success(), HealthCheckResult.failed("test failure"))); + when(healthCheckService.getStartupStatus()).thenReturn(new HealthStatus(Stream.concat(successResult().stream(), failedResult().stream()).toList())); baseRequest() .get("/health") @@ -79,7 +84,7 @@ void checkHealth_noProviders() { @Test void getLiveness() { - when(healthCheckService.isLive()).thenReturn(new HealthStatus(HealthCheckResult.success())); + when(healthCheckService.isLive()).thenReturn(new HealthStatus(successResult())); baseRequest() .get("/liveness") @@ -93,7 +98,7 @@ void getLiveness() { @Test void getLiveness_mixedResults() { - when(healthCheckService.isLive()).thenReturn(new HealthStatus(HealthCheckResult.success(), HealthCheckResult.failed("test failure"))); + when(healthCheckService.isLive()).thenReturn(new HealthStatus(Stream.concat(successResult().stream(), failedResult().stream()).toList())); baseRequest() .get("/liveness") @@ -121,7 +126,7 @@ void getLiveness_noProviders() { @Test void getReadiness() { - when(healthCheckService.isReady()).thenReturn(new HealthStatus(HealthCheckResult.success())); + when(healthCheckService.isReady()).thenReturn(new HealthStatus(successResult())); baseRequest() .get("/readiness") @@ -135,7 +140,7 @@ void getReadiness() { @Test void getReadiness_mixedResults() { - when(healthCheckService.isReady()).thenReturn(new HealthStatus(HealthCheckResult.success(), HealthCheckResult.failed("test failure"))); + when(healthCheckService.isReady()).thenReturn(new HealthStatus(Stream.concat(successResult().stream(), failedResult().stream()).toList())); baseRequest() .get("/readiness") @@ -163,7 +168,7 @@ void getReadiness_noProvider() { @Test void getStartup() { - when(healthCheckService.getStartupStatus()).thenReturn(new HealthStatus(HealthCheckResult.success())); + when(healthCheckService.getStartupStatus()).thenReturn(new HealthStatus(successResult())); baseRequest() .get("/startup") @@ -177,7 +182,8 @@ void getStartup() { @Test void getStartup_mixedResults() { - when(healthCheckService.getStartupStatus()).thenReturn(new HealthStatus(HealthCheckResult.success(), HealthCheckResult.failed("test failure"))); + + when(healthCheckService.getStartupStatus()).thenReturn(new HealthStatus(Stream.concat(successResult().stream(), failedResult().stream()).toList())); baseRequest() .get("/startup") @@ -208,6 +214,14 @@ protected Object controller() { return new ObservabilityApiController(healthCheckService); } + private List failedResult() { + return Collections.singletonList(HealthCheckResult.Builder.newInstance().component("test component").failure("test failure").build()); + } + + private Collection successResult() { + return Collections.singletonList(HealthCheckResult.Builder.newInstance().component("test component").success().build()); + } + private RequestSpecification baseRequest() { return given() .baseUri("http://localhost:" + port + "/check") diff --git a/extensions/common/vault/vault-hashicorp/src/main/java/org/eclipse/edc/vault/hashicorp/health/HashicorpVaultHealthCheck.java b/extensions/common/vault/vault-hashicorp/src/main/java/org/eclipse/edc/vault/hashicorp/health/HashicorpVaultHealthCheck.java index d7ed25f2e4a..b76b570b8fb 100644 --- a/extensions/common/vault/vault-hashicorp/src/main/java/org/eclipse/edc/vault/hashicorp/health/HashicorpVaultHealthCheck.java +++ b/extensions/common/vault/vault-hashicorp/src/main/java/org/eclipse/edc/vault/hashicorp/health/HashicorpVaultHealthCheck.java @@ -45,11 +45,12 @@ public HealthCheckResult get() { .doHealthCheck() .merge(client.isTokenRenewable()) .flatMap(result -> { + var statusBuilder = HealthCheckResult.Builder.newInstance().component("HashicorpVault"); if (result.succeeded()) { - return HealthCheckResult.success(); + return statusBuilder.success().build(); } else { monitor.debug("Vault health check failed with reason(s): " + result.getFailureDetail()); - return HealthCheckResult.failed(result.getFailureMessages()); + return statusBuilder.failure(result.getFailureMessages()).build(); } }).forComponent(HashicorpVaultHealthExtension.NAME); } diff --git a/extensions/data-plane/data-plane-self-registration/src/main/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtension.java b/extensions/data-plane/data-plane-self-registration/src/main/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtension.java index 2bd1776cb8a..dba124090fd 100644 --- a/extensions/data-plane/data-plane-self-registration/src/main/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtension.java +++ b/extensions/data-plane/data-plane-self-registration/src/main/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtension.java @@ -24,10 +24,17 @@ import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.health.HealthCheckResult; +import org.eclipse.edc.spi.system.health.HealthCheckService; +import org.eclipse.edc.spi.system.health.LivenessProvider; +import org.eclipse.edc.spi.system.health.ReadinessProvider; +import org.eclipse.edc.spi.system.health.StartupStatusProvider; import org.eclipse.edc.spi.types.domain.transfer.FlowType; import org.jetbrains.annotations.NotNull; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static java.util.stream.Collectors.toSet; @@ -39,18 +46,18 @@ public class DataplaneSelfRegistrationExtension implements ServiceExtension { public static final String NAME = "Dataplane Self Registration"; - + private final AtomicBoolean isRegistered = new AtomicBoolean(false); + private final AtomicReference registrationError = new AtomicReference<>("Data plane self registration not complete"); @Inject private DataPlaneSelectorService dataPlaneSelectorService; - @Inject private ControlApiUrl controlApiUrl; - @Inject private PipelineService pipelineService; - @Inject private PublicEndpointGeneratorService publicEndpointGeneratorService; + @Inject + private HealthCheckService healthCheckService; private ServiceExtensionContext context; @Override @@ -78,20 +85,44 @@ public void start() { .allowedTransferType(transferTypes.collect(toSet())) .build(); + + // register the data plane + var monitor = context.getMonitor().withPrefix("DataPlaneHealthCheck"); + var check = new DataPlaneHealthCheck(); + healthCheckService.addReadinessProvider(check); + healthCheckService.addLivenessProvider(check); + healthCheckService.addStartupStatusProvider(check); + + monitor.debug("Initiate data plane registration."); dataPlaneSelectorService.addInstance(instance) - .onSuccess(it -> context.getMonitor().info("data-plane registered to control-plane")) - .orElseThrow(f -> new EdcException("Cannot register data-plane to the control-plane: " + f.getFailureDetail())); + .onSuccess(it -> { + monitor.info("data plane registered to control plane"); + isRegistered.set(true); + }) + .onFailure(f -> registrationError.set(f.getFailureDetail())) + .orElseThrow(f -> new EdcException("Cannot register data plane to the control plane: " + f.getFailureDetail())); } @Override public void shutdown() { dataPlaneSelectorService.delete(context.getRuntimeId()) - .onSuccess(it -> context.getMonitor().info("data-plane successfully unregistered")) - .onFailure(failure -> context.getMonitor().severe("error during data-plane un-registration. %s: %s" + .onSuccess(it -> context.getMonitor().info("data plane successfully unregistered")) + .onFailure(failure -> context.getMonitor().severe("error during data plane de-registration. %s: %s" .formatted(failure.getReason(), failure.getFailureDetail()))); } private @NotNull Stream toTransferTypes(FlowType pull, Set types) { return types.stream().map(it -> "%s-%s".formatted(it, pull)); } + + private class DataPlaneHealthCheck implements LivenessProvider, ReadinessProvider, StartupStatusProvider { + + @Override + public HealthCheckResult get() { + return HealthCheckResult.Builder.newInstance() + .component(NAME) + .success(isRegistered.get(), registrationError.get()) + .build(); + } + } } diff --git a/extensions/data-plane/data-plane-self-registration/src/test/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtensionTest.java b/extensions/data-plane/data-plane-self-registration/src/test/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtensionTest.java index 2623671e1cb..a28a1429ff1 100644 --- a/extensions/data-plane/data-plane-self-registration/src/test/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtensionTest.java +++ b/extensions/data-plane/data-plane-self-registration/src/test/java/org/eclipse/edc/connector/dataplane/registration/DataplaneSelfRegistrationExtensionTest.java @@ -21,8 +21,10 @@ import org.eclipse.edc.connector.dataplane.spi.pipeline.PipelineService; import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.health.HealthCheckService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -36,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -47,6 +50,7 @@ class DataplaneSelfRegistrationExtensionTest { private final ControlApiUrl controlApiUrl = mock(); private final PipelineService pipelineService = mock(); private final PublicEndpointGeneratorService publicEndpointGeneratorService = mock(); + private final HealthCheckService healthCheckService = mock(); @BeforeEach void setUp(ServiceExtensionContext context) { @@ -54,6 +58,10 @@ void setUp(ServiceExtensionContext context) { context.registerService(ControlApiUrl.class, controlApiUrl); context.registerService(PipelineService.class, pipelineService); context.registerService(PublicEndpointGeneratorService.class, publicEndpointGeneratorService); + var monitor = mock(Monitor.class); + when(monitor.withPrefix(anyString())).thenReturn(mock()); + context.registerService(Monitor.class, monitor); + context.registerService(HealthCheckService.class, healthCheckService); } @Test @@ -77,6 +85,10 @@ void shouldRegisterInstanceAtStartup(DataplaneSelfRegistrationExtension extensio assertThat(dataPlaneInstance.getAllowedDestTypes()).containsExactlyInAnyOrder("sinkType", "anotherSinkType"); assertThat(dataPlaneInstance.getAllowedTransferTypes()) .containsExactlyInAnyOrder("pullDestType-PULL", "anotherPullDestType-PULL", "sinkType-PUSH", "anotherSinkType-PUSH"); + + verify(healthCheckService).addStartupStatusProvider(any()); + verify(healthCheckService).addLivenessProvider(any()); + verify(healthCheckService).addReadinessProvider(any()); } @Test @@ -90,7 +102,7 @@ void shouldNotStart_whenRegistrationFails(DataplaneSelfRegistrationExtension ext } @Test - void shouldUnregisterInstanceAtStartup(DataplaneSelfRegistrationExtension extension, ServiceExtensionContext context) { + void shouldUnregisterInstanceAtShutdown(DataplaneSelfRegistrationExtension extension, ServiceExtensionContext context) { when(context.getRuntimeId()).thenReturn("runtimeId"); when(dataPlaneSelectorService.delete(any())).thenReturn(ServiceResult.success()); extension.initialize(context); diff --git a/spi/common/boot-spi/src/main/java/org/eclipse/edc/spi/system/health/HealthCheckResult.java b/spi/common/boot-spi/src/main/java/org/eclipse/edc/spi/system/health/HealthCheckResult.java index 91eca1ce682..8b5df56e8f3 100644 --- a/spi/common/boot-spi/src/main/java/org/eclipse/edc/spi/system/health/HealthCheckResult.java +++ b/spi/common/boot-spi/src/main/java/org/eclipse/edc/spi/system/health/HealthCheckResult.java @@ -20,6 +20,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -35,21 +37,32 @@ private HealthCheckResult(boolean successful, Failure failure) { super(successful, failure); } + /* + * @deprecated Use the Builder instead + */ + @Deprecated(since = "0.7.0") public static HealthCheckResult success() { return new HealthCheckResult(true, null); } + /* + * @deprecated Use the Builder instead + */ + @Deprecated(since = "0.7.0") public static HealthCheckResult failed(String... errors) { var errorList = Stream.of(errors).filter(Objects::nonNull).collect(Collectors.toList()); return new HealthCheckResult(false, new Failure(errorList)); } + /* + * @deprecated Use the Builder instead + */ + @Deprecated(since = "0.7.0") public static HealthCheckResult failed(List errors) { return new HealthCheckResult(false, new Failure(errors)); } @JsonProperty("isHealthy") - @Override public @NotNull Boolean getContent() { return super.getContent(); @@ -98,7 +111,11 @@ public Builder success(boolean success, String... errors) { } public Builder failure(String... errors) { - var errorList = Stream.of(errors).filter(Objects::nonNull).collect(Collectors.toList()); + return failure(Arrays.asList(errors)); + } + + public Builder failure(Collection errors) { + var errorList = errors.stream().filter(Objects::nonNull).toList(); failure = new Failure(errorList); success = false; return this; @@ -111,8 +128,7 @@ public Builder component(String component) { public HealthCheckResult build() { var hc = new HealthCheckResult(success, failure); - hc.component = component; - + hc.component = Objects.requireNonNull(component, "Component name cannot be null"); return hc; } } diff --git a/spi/common/boot-spi/src/main/java/org/eclipse/edc/spi/system/health/HealthCheckService.java b/spi/common/boot-spi/src/main/java/org/eclipse/edc/spi/system/health/HealthCheckService.java index ee845c00e52..9770fa845f5 100644 --- a/spi/common/boot-spi/src/main/java/org/eclipse/edc/spi/system/health/HealthCheckService.java +++ b/spi/common/boot-spi/src/main/java/org/eclipse/edc/spi/system/health/HealthCheckService.java @@ -38,6 +38,4 @@ public interface HealthCheckService { HealthStatus isReady(); HealthStatus getStartupStatus(); - - void refresh(); }