diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java index 8e2cfde2a438..890bc8e0339c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthIndicatorRegistry; +import org.springframework.boot.actuate.health.HealthIndicatorStrategy; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.context.annotation.Bean; @@ -38,8 +39,9 @@ class HealthEndpointConfiguration { @Bean @ConditionalOnMissingBean - HealthEndpoint healthEndpoint(HealthAggregator healthAggregator, HealthIndicatorRegistry registry) { - return new HealthEndpoint(new CompositeHealthIndicator(healthAggregator, registry)); + HealthEndpoint healthEndpoint(HealthAggregator healthAggregator, HealthIndicatorRegistry registry, + HealthIndicatorStrategy healthIndicatorStrategy) { + return new HealthEndpoint(new CompositeHealthIndicator(healthAggregator, registry, healthIndicatorStrategy)); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java index b680c9d22092..4ee400f6f425 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java @@ -21,9 +21,11 @@ import reactor.core.publisher.Flux; import org.springframework.boot.actuate.health.ApplicationHealthIndicator; +import org.springframework.boot.actuate.health.DefaultHealthIndicatorStrategy; import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.HealthIndicatorRegistry; +import org.springframework.boot.actuate.health.HealthIndicatorStrategy; import org.springframework.boot.actuate.health.OrderedHealthAggregator; import org.springframework.boot.actuate.health.ReactiveHealthIndicator; import org.springframework.boot.actuate.health.ReactiveHealthIndicatorRegistry; @@ -71,6 +73,12 @@ public HealthIndicatorRegistry healthIndicatorRegistry(ApplicationContext applic return HealthIndicatorRegistryBeans.get(applicationContext); } + @Bean + @ConditionalOnMissingBean + public HealthIndicatorStrategy healthIndicatorStrategy() { + return new DefaultHealthIndicatorStrategy(); + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Flux.class) static class ReactiveHealthIndicatorConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfigurationTests.java index c020e1665a08..04c32c2c91b4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfigurationTests.java @@ -16,15 +16,18 @@ package org.springframework.boot.actuate.autoconfigure.health; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.health.ApplicationHealthIndicator; +import org.springframework.boot.actuate.health.DefaultHealthIndicatorStrategy; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.HealthIndicatorStrategy; import org.springframework.boot.actuate.health.OrderedHealthAggregator; import org.springframework.boot.actuate.health.Status; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -81,6 +84,18 @@ void runShouldCreateOrderedHealthAggregator() { .isInstanceOf(OrderedHealthAggregator.class)); } + @Test + void shouldCreateDefaultHealthIndicatorStrategy() { + this.contextRunner.run((context) -> assertThat(context).getBean(HealthIndicatorStrategy.class) + .isInstanceOf(DefaultHealthIndicatorStrategy.class)); + } + + @Test + void shouldNotCreateDefaultHealthIndicatorCustomStrategyPresent() { + this.contextRunner.withBean(CustomHealthIndicatorStrategy.class).run((context) -> assertThat(context) + .getBean(HealthIndicatorStrategy.class).isInstanceOf(CustomHealthIndicatorStrategy.class)); + } + @Test void runWhenHasCustomOrderPropertyShouldCreateOrderedHealthAggregator() { this.contextRunner.withPropertyValues("management.health.status.order:UP,DOWN").run((context) -> { @@ -111,6 +126,15 @@ HealthIndicator customHealthIndicator() { } + static class CustomHealthIndicatorStrategy implements HealthIndicatorStrategy { + + @Override + public Map doHealth(Map healthIndicators) { + return Collections.emptyMap(); + } + + } + static class CustomHealthIndicator implements HealthIndicator { @Override diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealthIndicator.java index e86b5672581c..97b0fff00100 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealthIndicator.java @@ -16,11 +16,12 @@ package org.springframework.boot.actuate.health; -import java.util.LinkedHashMap; import java.util.Map; /** - * {@link HealthIndicator} that returns health indications from all registered delegates. + * {@link HealthIndicator} that returns health indications from all registered delegates + * using the {@link HealthIndicatorStrategy} and aggregates the result via + * {@link HealthAggregator} into a final one. * * @author Tyler J. Frederick * @author Phillip Webb @@ -33,6 +34,8 @@ public class CompositeHealthIndicator implements HealthIndicator { private final HealthAggregator aggregator; + private final HealthIndicatorStrategy strategy; + /** * Create a new {@link CompositeHealthIndicator} from the specified indicators. * @param healthAggregator the health aggregator @@ -50,8 +53,23 @@ public CompositeHealthIndicator(HealthAggregator healthAggregator, Map healths = new LinkedHashMap<>(); - for (Map.Entry entry : this.registry.getAll().entrySet()) { - healths.put(entry.getKey(), entry.getValue().health()); - } + Map healths = this.strategy.doHealth(this.registry.getAll()); return this.aggregator.aggregate(healths); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ConcurrentlyHealthIndicatorStrategy.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ConcurrentlyHealthIndicatorStrategy.java new file mode 100644 index 000000000000..d9a293facbfe --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ConcurrentlyHealthIndicatorStrategy.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.health; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * + * {@link HealthIndicatorStrategy} that returns health indications from all + * {@link HealthIndicator} instances concurrently. + * + * @author Dmytro Nosan + * @since 2.2.0 + */ +public class ConcurrentlyHealthIndicatorStrategy implements HealthIndicatorStrategy { + + private static final Health UNKNOWN = Health.unknown().build(); + + private final Executor executor; + + private final Long timeout; + + private final Health timeoutHealth; + + /** + * Returns a new {@link ConcurrentlyHealthIndicatorStrategy} with no timeout. + * @param executor the executor to submit {@link HealthIndicator HealthIndicators} on. + */ + public ConcurrentlyHealthIndicatorStrategy(Executor executor) { + this.executor = executor; + this.timeout = null; + this.timeoutHealth = null; + } + + /** + * Returns a new {@link ConcurrentlyHealthIndicatorStrategy} with timeout. + * @param executor the executor to submit {@link HealthIndicator HealthIndicators} on. + * @param timeout number of milliseconds to wait before using the + * {@code timeoutHealth} + * @param timeoutHealth the {@link Health} to use if an health indicator reached the + * {@code timeout}. Defaults to {@code unknown} status. + */ + public ConcurrentlyHealthIndicatorStrategy(Executor executor, long timeout, Health timeoutHealth) { + this.executor = executor; + this.timeout = timeout; + this.timeoutHealth = (timeoutHealth != null) ? timeoutHealth : UNKNOWN; + } + + @Override + public Map doHealth(Map healthIndicators) { + Map> healthsFutures = new LinkedHashMap<>(); + for (Map.Entry entry : healthIndicators.entrySet()) { + healthsFutures.put(entry.getKey(), CompletableFuture.supplyAsync(entry.getValue()::health, this.executor)); + } + Map healths = new LinkedHashMap<>(); + for (Map.Entry> entry : healthsFutures.entrySet()) { + healths.put(entry.getKey(), getHealth(entry.getValue(), this.timeout, this.timeoutHealth)); + } + return healths; + } + + private static Health getHealth(Future healthFuture, Long timeout, Health timeoutHealth) { + try { + return (timeout != null) ? healthFuture.get(timeout, TimeUnit.MILLISECONDS) : healthFuture.get(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return Health.unknown().withException(ex).build(); + } + catch (TimeoutException ex) { + return timeoutHealth; + } + catch (ExecutionException ex) { + Throwable cause = ex.getCause(); + return Health.down((cause instanceof Exception) ? ((Exception) cause) : ex).build(); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorStrategy.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorStrategy.java new file mode 100644 index 000000000000..f48b3cd58f2c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorStrategy.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.health; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * + * {@link HealthIndicatorStrategy} that returns health indications from all + * {@link HealthIndicator} instances sequentially. + * + * @author Dmytro Nosan + * @since 2.2.0 + */ +public class DefaultHealthIndicatorStrategy implements HealthIndicatorStrategy { + + @Override + public Map doHealth(Map healthIndicators) { + Map healths = new LinkedHashMap<>(); + for (Map.Entry entry : healthIndicators.entrySet()) { + healths.put(entry.getKey(), entry.getValue().health()); + } + return healths; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorStrategy.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorStrategy.java new file mode 100644 index 000000000000..505056ea360a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorStrategy.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.health; + +import java.util.Map; + +/** + * Strategy interface used to define how {@link HealthIndicator} instances should be + * called. + * + * @author Dmytro Nosan + * @since 2.2.0 + * @see DefaultHealthIndicatorStrategy + * @see ConcurrentlyHealthIndicatorStrategy + */ +@FunctionalInterface +public interface HealthIndicatorStrategy { + + /** + * Calls the given {@link HealthIndicator} instances. + * @param healthIndicators the {@link HealthIndicator} instances that should be called + * @return the health indications from all {@link HealthIndicator} instances + */ + Map doHealth(Map healthIndicators); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ConcurrentlyHealthIndicatorStrategyTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ConcurrentlyHealthIndicatorStrategyTests.java new file mode 100644 index 000000000000..bac95168b4e8 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ConcurrentlyHealthIndicatorStrategyTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.health; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConcurrentlyHealthIndicatorStrategy}. + * + * @author Dmytro Nosan + */ +class ConcurrentlyHealthIndicatorStrategyTests { + + private static final ExecutorService executor = Executors.newCachedThreadPool(); + + @AfterAll + static void shutdownExecutor() { + executor.shutdown(); + } + + @Test + @Timeout(2) + void testStrategy() { + Map indicators = new HashMap<>(); + indicators.put("slow-1", new TimeoutHealthIndicator(1500, Status.UP)); + indicators.put("slow-2", new TimeoutHealthIndicator(1500, Status.UP)); + indicators.put("error", new ErrorHealthIndicator()); + Map health = createStrategy().doHealth(indicators); + assertThat(health).containsOnlyKeys("slow-1", "slow-2", "error"); + assertThat(health.get("slow-1")).isEqualTo(Health.up().build()); + assertThat(health.get("slow-2")).isEqualTo(Health.up().build()); + assertThat(health.get("error")).isEqualTo(Health.down(new UnsupportedOperationException()).build()); + } + + @Test + void testTimeoutReachedDefaultFallback() { + Map indicators = new HashMap<>(); + indicators.put("slow", new TimeoutHealthIndicator(250, Status.UP)); + indicators.put("fast", new TimeoutHealthIndicator(10, Status.UP)); + Map health = createStrategy(200, null).doHealth(indicators); + assertThat(health).containsOnlyKeys("slow", "fast"); + assertThat(health.get("slow")).isEqualTo(Health.unknown().build()); + assertThat(health.get("fast")).isEqualTo(Health.up().build()); + } + + @Test + void testTimeoutReachedCustomTimeoutFallback() { + Map indicators = new HashMap<>(); + indicators.put("slow", new TimeoutHealthIndicator(250, Status.UP)); + indicators.put("fast", new TimeoutHealthIndicator(10, Status.UP)); + Map health = createStrategy(200, Health.down().build()).doHealth(indicators); + assertThat(health).containsOnlyKeys("slow", "fast"); + assertThat(health.get("slow")).isEqualTo(Health.down().build()); + assertThat(health.get("fast")).isEqualTo(Health.up().build()); + } + + @Test + void testInterrupted() throws InterruptedException { + Map indicators = new HashMap<>(); + indicators.put("slow", new TimeoutHealthIndicator(750, Status.UP)); + indicators.put("fast", new TimeoutHealthIndicator(250, Status.UP)); + AtomicReference> healthReference = new AtomicReference<>(); + Thread thread = new Thread(() -> healthReference.set(createStrategy().doHealth(indicators))); + thread.start(); + thread.join(100); + thread.interrupt(); + thread.join(); + Map health = healthReference.get(); + assertThat(health).containsOnlyKeys("slow", "fast"); + Health unknown = Health.unknown().withException(new InterruptedException()).build(); + assertThat(health.get("slow")).isEqualTo(unknown); + assertThat(health.get("fast")).isEqualTo(unknown); + } + + private HealthIndicatorStrategy createStrategy(long timeout, Health timeoutHealth) { + return new ConcurrentlyHealthIndicatorStrategy(executor, timeout, timeoutHealth); + } + + private HealthIndicatorStrategy createStrategy() { + return new ConcurrentlyHealthIndicatorStrategy(executor); + } + + private static final class ErrorHealthIndicator implements HealthIndicator { + + @Override + public Health health() { + throw new UnsupportedOperationException(); + } + + } + + private static final class TimeoutHealthIndicator implements HealthIndicator { + + private final long timeout; + + private final Status status; + + TimeoutHealthIndicator(long timeout, Status status) { + this.timeout = timeout; + this.status = status; + } + + @Override + public Health health() { + ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + try { + return executorService + .schedule(() -> Health.status(this.status).build(), this.timeout, TimeUnit.MILLISECONDS).get(); + } + catch (Exception ex) { + // never + throw new RuntimeException(ex); + } + finally { + executorService.shutdown(); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorStrategyTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorStrategyTests.java new file mode 100644 index 000000000000..1a53b4c5362b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorStrategyTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.health; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DefaultHealthIndicatorStrategy}. + * + * @author Dmytro Nosan + */ +class DefaultHealthIndicatorStrategyTests { + + private final HealthIndicator one = mock(HealthIndicator.class); + + private final HealthIndicator two = mock(HealthIndicator.class); + + @BeforeEach + void setup() { + given(this.one.health()).willReturn(new Health.Builder().unknown().build()); + given(this.two.health()).willReturn(new Health.Builder().up().build()); + } + + @Test + void testStrategy() { + Map indicators = new HashMap<>(); + indicators.put("one", this.one); + indicators.put("two", this.two); + Map health = new DefaultHealthIndicatorStrategy().doHealth(indicators); + assertThat(health).containsOnlyKeys("one", "two"); + assertThat(health).containsEntry("one", new Health.Builder().unknown().build()); + assertThat(health).containsEntry("two", new Health.Builder().up().build()); + } + +}