This is an automated email from the ASF dual-hosted git repository. jayblanc pushed a commit to branch make-healthcheck-concurrent in repository https://gitbox.apache.org/repos/asf/unomi.git
commit 087aeaf14cb90aa1b05d3c938145dc5eab8b566a Author: Jérôme Blanchard <[email protected]> AuthorDate: Tue Dec 23 17:48:47 2025 +0100 feat: ensure that healthchecks calls can be performed concurrently and add a 1s cache for performance or DoS issues. --- .../unomi/healthcheck/HealthCheckService.java | 86 ++++++++++++++-------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckService.java b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckService.java index 9da585e14..f7da22e85 100644 --- a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckService.java +++ b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckService.java @@ -28,7 +28,6 @@ import org.slf4j.LoggerFactory; import javax.servlet.ServletException; import java.util.*; import java.util.concurrent.*; -import java.util.stream.Collectors; import static org.apache.unomi.healthcheck.HealthCheckConfig.CONFIG_AUTH_REALM; @@ -42,8 +41,12 @@ public class HealthCheckService { private static final Logger LOGGER = LoggerFactory.getLogger(HealthCheckService.class.getName()); private final List<HealthCheckProvider> providers = new ArrayList<>(); + private final Object cacheLock = new Object(); + private volatile long cacheTimestamp = 0L; + private volatile List<HealthCheckResponse> healthCache = Collections.emptyList(); + private volatile boolean initialized = false; + private volatile boolean busy = false; private ExecutorService executor; - private boolean busy = false; private boolean registered = false; @Reference @@ -58,7 +61,7 @@ public class HealthCheckService { @Activate public void activate() throws ServletException, NamespaceException { LOGGER.info("Activating healthcheck service..."); - executor = Executors.newSingleThreadExecutor(); + executor = Executors.newCachedThreadPool(); if (!registered) { setConfig(config); } @@ -114,37 +117,60 @@ public class HealthCheckService { providers.remove(provider); } - public List<HealthCheckResponse> check() throws RejectedExecutionException { - if (config !=null && config.isEnabled()) { - LOGGER.debug("Health check called"); - if (busy) { - throw new RejectedExecutionException("Health check already in progress"); - } else { - try { - busy = true; - List<HealthCheckResponse> health = new ArrayList<>(); - health.add(HealthCheckResponse.live("karaf")); - for (HealthCheckProvider provider : providers.stream().filter(p -> config.getEnabledProviders().contains(p.name())).collect(Collectors.toList())) { - Future<HealthCheckResponse> future = executor.submit(provider::execute); - try { - HealthCheckResponse response = future.get(config.getTimeout(), TimeUnit.MILLISECONDS); - health.add(response); - } catch (TimeoutException e) { - future.cancel(true); - health.add(provider.timeout()); - } catch (Exception e) { - LOGGER.error("Error while executing health check", e); - } + public List<HealthCheckResponse> check() { + if (config == null || !config.isEnabled()) { + LOGGER.info("Healthcheck service is disabled"); + return Collections.emptyList(); + } + if (!initialized) { + synchronized (cacheLock) { + if (!initialized) { + refreshCacheSync(); + initialized = true; + } + } + } else if (shouldRefreshCache()) { + if (!busy) { + synchronized (cacheLock) { + if (!busy) { + busy = true; + executor.submit(() -> { + try { + refreshCacheSync(); + } finally { + busy = false; + } + }); } - return health; - } finally { - busy = false; } } - } else { - LOGGER.info("Healthcheck service is disabled"); - return Collections.emptyList(); } + return healthCache; + } + + private boolean shouldRefreshCache() { + return healthCache.isEmpty() || (System.currentTimeMillis() - cacheTimestamp) > 1000; } + private void refreshCacheSync() { + try { + List<HealthCheckResponse> health = new ArrayList<>(); + health.add(HealthCheckResponse.live("karaf")); + for (HealthCheckProvider provider : providers.stream() + .filter(p -> config.getEnabledProviders().contains(p.name())) + .toList()) { + try { + HealthCheckResponse response = provider.execute(); + health.add(response); + } catch (Exception e) { + LOGGER.error("Error while executing health check", e); + health.add(provider.timeout()); + } + } + healthCache = health; + cacheTimestamp = System.currentTimeMillis(); + } catch (Exception e) { + LOGGER.error("Error refreshing health cache", e); + } + } }
