Skip to content

Commit 84a6ad2

Browse files
committed
Provide a way to run HealthIndicators concurrently
see gh-2652
1 parent f3a138d commit 84a6ad2

File tree

8 files changed

+453
-2
lines changed

8 files changed

+453
-2
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.springframework.boot.actuate.health.HealthEndpoint;
2020
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
2121
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
22+
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
2223
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2324
import org.springframework.context.annotation.Configuration;
2425
import org.springframework.context.annotation.Import;
@@ -33,7 +34,7 @@
3334
*/
3435
@Configuration(proxyBeanMethods = false)
3536
@EnableConfigurationProperties({ HealthEndpointProperties.class, HealthIndicatorProperties.class })
36-
@AutoConfigureAfter(HealthIndicatorAutoConfiguration.class)
37+
@AutoConfigureAfter({ HealthIndicatorAutoConfiguration.class, TaskExecutionAutoConfiguration.class })
3738
@Import({ HealthEndpointConfiguration.class, HealthEndpointWebExtensionConfiguration.class })
3839
public class HealthEndpointAutoConfiguration {
3940

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.health;
1818

19+
import java.util.concurrent.Executor;
20+
21+
import org.springframework.beans.factory.ObjectProvider;
1922
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
23+
import org.springframework.boot.actuate.health.CompositeConcurrentHealthIndicator;
2024
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
2125
import org.springframework.boot.actuate.health.HealthAggregator;
2226
import org.springframework.boot.actuate.health.HealthEndpoint;
2327
import org.springframework.boot.actuate.health.HealthIndicatorRegistry;
2428
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
29+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
2530
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
2631
import org.springframework.context.annotation.Bean;
2732
import org.springframework.context.annotation.Configuration;
@@ -38,8 +43,25 @@ class HealthEndpointConfiguration {
3843

3944
@Bean
4045
@ConditionalOnMissingBean
46+
@ConditionalOnProperty(name = "management.endpoint.health.parallel.enabled", havingValue = "false",
47+
matchIfMissing = true)
4148
HealthEndpoint healthEndpoint(HealthAggregator healthAggregator, HealthIndicatorRegistry registry) {
4249
return new HealthEndpoint(new CompositeHealthIndicator(healthAggregator, registry));
4350
}
4451

52+
@Configuration(proxyBeanMethods = false)
53+
@ConditionalOnProperty(name = "management.endpoint.health.parallel.enabled", havingValue = "true")
54+
static class ParallelHealthEndpointConfiguration {
55+
56+
@Bean
57+
@ConditionalOnMissingBean
58+
HealthEndpoint healthEndpoint(HealthAggregator healthAggregator, HealthIndicatorRegistry registry,
59+
HealthEndpointProperties properties, @HealthExecutor ObjectProvider<Executor> healthExecutor,
60+
ObjectProvider<Executor> executor) {
61+
return new HealthEndpoint(new CompositeConcurrentHealthIndicator(healthAggregator, registry,
62+
healthExecutor.getIfAvailable(executor::getObject), properties.getParallel().getTimeout()));
63+
}
64+
65+
}
66+
4567
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2018 the original author or authors.
2+
* Copyright 2012-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.health;
1818

19+
import java.time.Duration;
1920
import java.util.HashSet;
2021
import java.util.Set;
2122

@@ -43,6 +44,11 @@ public class HealthEndpointProperties {
4344
*/
4445
private Set<String> roles = new HashSet<>();
4546

47+
/**
48+
* Parallel configuration.
49+
*/
50+
private final Parallel parallel = new Parallel();
51+
4652
public ShowDetails getShowDetails() {
4753
return this.showDetails;
4854
}
@@ -59,4 +65,41 @@ public void setRoles(Set<String> roles) {
5965
this.roles = roles;
6066
}
6167

68+
public Parallel getParallel() {
69+
return this.parallel;
70+
}
71+
72+
/**
73+
* Parallel properties.
74+
*/
75+
public static class Parallel {
76+
77+
/**
78+
* Whether health indicators should be called concurrently.
79+
*/
80+
private boolean enabled;
81+
82+
/**
83+
* Timeout to wait for the result from health indicators.
84+
*/
85+
private Duration timeout;
86+
87+
public boolean isEnabled() {
88+
return this.enabled;
89+
}
90+
91+
public void setEnabled(boolean enabled) {
92+
this.enabled = enabled;
93+
}
94+
95+
public Duration getTimeout() {
96+
return this.timeout;
97+
}
98+
99+
public void setTimeout(Duration timeout) {
100+
this.timeout = timeout;
101+
}
102+
103+
}
104+
62105
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.health;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
import java.util.concurrent.Executor;
25+
26+
import org.springframework.beans.factory.annotation.Qualifier;
27+
import org.springframework.boot.actuate.health.CompositeConcurrentHealthIndicator;
28+
29+
/**
30+
* Qualifier annotation for a {@link Executor} to be injected into
31+
* {@link HealthEndpointAutoConfiguration}. The {@code Executor} used for
32+
* {@link CompositeConcurrentHealthIndicator} in case if {@code parallel} is enabled.
33+
*
34+
* @author Dmytro Nosan
35+
* @since 2.2.0
36+
*/
37+
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
38+
@Retention(RetentionPolicy.RUNTIME)
39+
@Documented
40+
@Qualifier
41+
public @interface HealthExecutor {
42+
43+
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,28 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.health;
1818

19+
import java.time.Duration;
20+
import java.util.concurrent.Executor;
21+
import java.util.concurrent.ExecutorService;
22+
import java.util.concurrent.Executors;
23+
import java.util.concurrent.atomic.AtomicReference;
24+
25+
import org.junit.jupiter.api.Assertions;
1926
import org.junit.jupiter.api.Test;
2027
import reactor.core.publisher.Mono;
2128

29+
import org.springframework.boot.actuate.health.CompositeConcurrentHealthIndicator;
2230
import org.springframework.boot.actuate.health.Health;
2331
import org.springframework.boot.actuate.health.HealthEndpoint;
2432
import org.springframework.boot.actuate.health.HealthIndicator;
2533
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
2634
import org.springframework.boot.actuate.health.Status;
2735
import org.springframework.boot.autoconfigure.AutoConfigurations;
2836
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
37+
import org.springframework.context.annotation.Bean;
38+
import org.springframework.context.annotation.Configuration;
39+
import org.springframework.context.annotation.Primary;
40+
import org.springframework.test.util.ReflectionTestUtils;
2941

3042
import static org.assertj.core.api.Assertions.assertThat;
3143
import static org.mockito.BDDMockito.given;
@@ -91,6 +103,50 @@ void healthEndpointMergeRegularAndReactive() {
91103
});
92104
}
93105

106+
@Test
107+
void healthEndpointShouldCallHealthIndicatorsConcurrently() {
108+
this.contextRunner.withPropertyValues("management.endpoint.health.parallel.enabled=true")
109+
.withUserConfiguration(CustomExecutorConfiguration.class)
110+
.withBean("simpleHealthIndicator", HealthIndicator.class, this::simpleSlowHealthIndicator)
111+
.withBean("reactiveHealthIndicator", ReactiveHealthIndicator.class, this::reactiveSlowHealthIndicator)
112+
.run((context) -> {
113+
HealthIndicator indicator = context.getBean("simpleHealthIndicator", HealthIndicator.class);
114+
ReactiveHealthIndicator reactiveHealthIndicator = context.getBean("reactiveHealthIndicator",
115+
ReactiveHealthIndicator.class);
116+
verify(indicator, never()).health();
117+
verify(reactiveHealthIndicator, never()).health();
118+
HealthEndpoint healthEndpoint = context.getBean(HealthEndpoint.class);
119+
AtomicReference<Health> healthReference = new AtomicReference<>();
120+
Assertions.assertTimeout(Duration.ofMillis(300),
121+
() -> healthReference.set(healthEndpoint.health()));
122+
Health health = healthReference.get();
123+
assertThat(health.getStatus()).isEqualTo(Status.UP);
124+
assertThat(health.getDetails()).containsOnlyKeys("simple", "reactive");
125+
verify(indicator).health();
126+
verify(reactiveHealthIndicator).health();
127+
});
128+
}
129+
130+
@Test
131+
void healthEndpointShouldUseHealthExecutor() {
132+
this.contextRunner.withPropertyValues("management.endpoint.health.parallel.enabled=true")
133+
.withUserConfiguration(HealthExecutorConfiguration.class).run((context) -> {
134+
assertThat(context).hasSingleBean(HealthEndpoint.class).hasBean("healthExecutor");
135+
HealthEndpoint healthEndpoint = context.getBean(HealthEndpoint.class);
136+
Object healthIndicator = ReflectionTestUtils.getField(healthEndpoint, "healthIndicator");
137+
assertThat(healthIndicator).isInstanceOf(CompositeConcurrentHealthIndicator.class);
138+
assertThat(healthIndicator).hasFieldOrPropertyWithValue("executor",
139+
context.getBean("healthExecutor", Executor.class));
140+
});
141+
}
142+
143+
@Test
144+
void healthEndpointConfigurationShouldFailIfNoExecutor() {
145+
this.contextRunner.withPropertyValues("management.endpoint.health.parallel.enabled=true")
146+
.run((context) -> assertThat(context).getFailure()
147+
.hasMessageContaining("No qualifying bean of type 'java.util.concurrent.Executor'"));
148+
}
149+
94150
private HealthIndicator simpleHealthIndicator() {
95151
HealthIndicator mock = mock(HealthIndicator.class);
96152
given(mock.health()).willReturn(Health.status(Status.UP).build());
@@ -103,4 +159,49 @@ private ReactiveHealthIndicator reactiveHealthIndicator() {
103159
return mock;
104160
}
105161

162+
private HealthIndicator simpleSlowHealthIndicator() {
163+
HealthIndicator mock = mock(HealthIndicator.class);
164+
given(mock.health()).will((invocation) -> {
165+
Thread.sleep(250);
166+
return Health.status(Status.UP).build();
167+
});
168+
return mock;
169+
}
170+
171+
private ReactiveHealthIndicator reactiveSlowHealthIndicator() {
172+
ReactiveHealthIndicator mock = mock(ReactiveHealthIndicator.class);
173+
given(mock.health()).will((invocation) -> {
174+
Thread.sleep(250);
175+
return Mono.just(Health.status(Status.UP).build());
176+
});
177+
return mock;
178+
}
179+
180+
@Configuration(proxyBeanMethods = false)
181+
static class CustomExecutorConfiguration {
182+
183+
@Bean(destroyMethod = "shutdown")
184+
ExecutorService executor() {
185+
return Executors.newCachedThreadPool();
186+
}
187+
188+
}
189+
190+
@Configuration(proxyBeanMethods = false)
191+
static class HealthExecutorConfiguration {
192+
193+
@Bean
194+
@Primary
195+
Executor primaryExecutor() {
196+
return Runnable::run;
197+
}
198+
199+
@Bean
200+
@HealthExecutor
201+
Executor healthExecutor() {
202+
return Runnable::run;
203+
}
204+
205+
}
206+
106207
}

0 commit comments

Comments
 (0)