Skip to content

Commit 40a2aa0

Browse files
committed
Introduce strategy for running HealthIndicators
1 parent acbb4e6 commit 40a2aa0

File tree

6 files changed

+293
-8
lines changed

6 files changed

+293
-8
lines changed

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/HealthEndpoint.java

+25-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 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.
@@ -19,6 +19,7 @@
1919
import java.util.Map;
2020

2121
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
22+
import org.springframework.boot.actuate.health.ExecutorServiceHealthIndicatorRunner;
2223
import org.springframework.boot.actuate.health.Health;
2324
import org.springframework.boot.actuate.health.HealthAggregator;
2425
import org.springframework.boot.actuate.health.HealthIndicator;
@@ -31,6 +32,7 @@
3132
* @author Dave Syer
3233
* @author Christian Dupuis
3334
* @author Andy Wilkinson
35+
* @author Vedran Pavic
3436
*/
3537
@ConfigurationProperties(prefix = "endpoints.health", ignoreUnknownFields = true)
3638
public class HealthEndpoint extends AbstractEndpoint<Health> {
@@ -42,6 +44,16 @@ public class HealthEndpoint extends AbstractEndpoint<Health> {
4244
*/
4345
private long timeToLive = 1000;
4446

47+
/**
48+
* Flag to enable parallel invocation of health indicators.
49+
*/
50+
private boolean runInParallel = false;
51+
52+
/**
53+
* Number of threads to use for parallel invocation of health indicators.
54+
*/
55+
private int threadCount = 2;
56+
4557
/**
4658
* Create a new {@link HealthIndicator} instance.
4759
* @param healthAggregator the health aggregator
@@ -52,8 +64,10 @@ public HealthEndpoint(HealthAggregator healthAggregator,
5264
super("health", false);
5365
Assert.notNull(healthAggregator, "HealthAggregator must not be null");
5466
Assert.notNull(healthIndicators, "HealthIndicators must not be null");
55-
CompositeHealthIndicator healthIndicator = new CompositeHealthIndicator(
56-
healthAggregator);
67+
CompositeHealthIndicator healthIndicator = this.runInParallel ?
68+
new CompositeHealthIndicator(healthAggregator,
69+
new ExecutorServiceHealthIndicatorRunner(this.threadCount)) :
70+
new CompositeHealthIndicator(healthAggregator);
5771
for (Map.Entry<String, HealthIndicator> entry : healthIndicators.entrySet()) {
5872
healthIndicator.addHealthIndicator(getKey(entry.getKey()), entry.getValue());
5973
}
@@ -73,6 +87,14 @@ public void setTimeToLive(long ttl) {
7387
this.timeToLive = ttl;
7488
}
7589

90+
public void setRunInParallel(boolean runInParallel) {
91+
this.runInParallel = runInParallel;
92+
}
93+
94+
public void setThreadCount(int threadCount) {
95+
this.threadCount = threadCount;
96+
}
97+
7698
/**
7799
* Invoke all {@link HealthIndicator} delegates and collect their health information.
78100
*/

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealthIndicator.java

+46-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 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.
@@ -27,6 +27,7 @@
2727
* @author Tyler J. Frederick
2828
* @author Phillip Webb
2929
* @author Christian Dupuis
30+
* @author Vedran Pavic
3031
* @since 1.1.0
3132
*/
3233
public class CompositeHealthIndicator implements HealthIndicator {
@@ -35,6 +36,8 @@ public class CompositeHealthIndicator implements HealthIndicator {
3536

3637
private final HealthAggregator healthAggregator;
3738

39+
private final HealthIndicatorRunner healthIndicatorRunner;
40+
3841
/**
3942
* Create a new {@link CompositeHealthIndicator}.
4043
* @param healthAggregator the health aggregator
@@ -51,10 +54,35 @@ public CompositeHealthIndicator(HealthAggregator healthAggregator) {
5154
*/
5255
public CompositeHealthIndicator(HealthAggregator healthAggregator,
5356
Map<String, HealthIndicator> indicators) {
57+
this(healthAggregator, indicators, new SimpleHealthIndicatorRunner());
58+
}
59+
/**
60+
* Create a new {@link CompositeHealthIndicator}.
61+
* @param healthAggregator the health aggregator
62+
* @param healthIndicatorRunner the health indicator runner
63+
*/
64+
public CompositeHealthIndicator(HealthAggregator healthAggregator,
65+
HealthIndicatorRunner healthIndicatorRunner) {
66+
this(healthAggregator, new LinkedHashMap<String, HealthIndicator>(),
67+
healthIndicatorRunner);
68+
}
69+
70+
/**
71+
* Create a new {@link CompositeHealthIndicator} from the specified indicators.
72+
* @param healthAggregator the health aggregator
73+
* @param indicators a map of {@link HealthIndicator}s with the key being used as an
74+
* indicator name.
75+
* @param healthIndicatorRunner the health indicator runner
76+
*/
77+
public CompositeHealthIndicator(HealthAggregator healthAggregator,
78+
Map<String, HealthIndicator> indicators,
79+
HealthIndicatorRunner healthIndicatorRunner) {
5480
Assert.notNull(healthAggregator, "HealthAggregator must not be null");
5581
Assert.notNull(indicators, "Indicators must not be null");
82+
Assert.notNull(healthIndicatorRunner, "HealthIndicatorRunner must not be null");
5683
this.indicators = new LinkedHashMap<String, HealthIndicator>(indicators);
5784
this.healthAggregator = healthAggregator;
85+
this.healthIndicatorRunner = healthIndicatorRunner;
5886
}
5987

6088
public void addHealthIndicator(String name, HealthIndicator indicator) {
@@ -63,11 +91,24 @@ public void addHealthIndicator(String name, HealthIndicator indicator) {
6391

6492
@Override
6593
public Health health() {
66-
Map<String, Health> healths = new LinkedHashMap<String, Health>();
67-
for (Map.Entry<String, HealthIndicator> entry : this.indicators.entrySet()) {
68-
healths.put(entry.getKey(), entry.getValue().health());
69-
}
94+
Map<String, Health> healths = this.healthIndicatorRunner.run(this.indicators);
7095
return this.healthAggregator.aggregate(healths);
7196
}
7297

98+
/**
99+
* {@link HealthIndicatorRunner} for sequential execution of {@link HealthIndicator}s.
100+
*/
101+
private static class SimpleHealthIndicatorRunner implements HealthIndicatorRunner {
102+
103+
@Override
104+
public Map<String, Health> run(Map<String, HealthIndicator> healthIndicators) {
105+
Map<String, Health> healths = new LinkedHashMap<String, Health>();
106+
for (Map.Entry<String, HealthIndicator> entry : healthIndicators.entrySet()) {
107+
healths.put(entry.getKey(), entry.getValue().health());
108+
}
109+
return healths;
110+
}
111+
112+
}
113+
73114
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2012-2016 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+
* http://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.health;
18+
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import java.util.concurrent.Callable;
22+
import java.util.concurrent.ExecutorService;
23+
import java.util.concurrent.Executors;
24+
import java.util.concurrent.Future;
25+
import java.util.concurrent.ThreadFactory;
26+
import java.util.concurrent.atomic.AtomicInteger;
27+
28+
import org.apache.commons.logging.Log;
29+
import org.apache.commons.logging.LogFactory;
30+
31+
import org.springframework.util.Assert;
32+
import org.springframework.util.ClassUtils;
33+
34+
/**
35+
* Default implementation of {@link HealthIndicatorRunner}.
36+
*
37+
* @author Vedran Pavic
38+
*/
39+
public class ExecutorServiceHealthIndicatorRunner implements HealthIndicatorRunner {
40+
41+
private static final Log logger = LogFactory.getLog(ExecutorServiceHealthIndicatorRunner.class);
42+
43+
private final ExecutorService executor;
44+
45+
/**
46+
* Create a {@link ExecutorServiceHealthIndicatorRunner} instance.
47+
* @param threadCount thread count used to run {@link HealthIndicator}s
48+
*/
49+
public ExecutorServiceHealthIndicatorRunner(int threadCount) {
50+
Assert.isTrue(threadCount > 1, "ThreadCount must be greater than one");
51+
this.executor = Executors.newFixedThreadPool(threadCount, new WorkerThreadFactory());
52+
}
53+
54+
@Override
55+
public Map<String, Health> run(Map<String, HealthIndicator> healthIndicators) {
56+
Map<String, Health> healths = new HashMap<String, Health>();
57+
Assert.notNull(this.executor, "Executor must not be null");
58+
Map<String, Future<Health>> futures = new HashMap<String, Future<Health>>();
59+
for (final Map.Entry<String, HealthIndicator> entry : healthIndicators.entrySet()) {
60+
Future<Health> future = this.executor.submit(new Callable<Health>() {
61+
@Override
62+
public Health call() throws Exception {
63+
return entry.getValue().health();
64+
}
65+
});
66+
futures.put(entry.getKey(), future);
67+
}
68+
for (Map.Entry<String, Future<Health>> entry : futures.entrySet()) {
69+
try {
70+
healths.put(entry.getKey(), entry.getValue().get());
71+
}
72+
catch (Exception e) {
73+
logger.warn("Error invoking health indicator '" + entry.getKey() + "'", e);
74+
healths.put(entry.getKey(), Health.down(e).build());
75+
}
76+
}
77+
return healths;
78+
}
79+
80+
/**
81+
* {@link ThreadFactory} to create the worker threads.
82+
*/
83+
private static class WorkerThreadFactory implements ThreadFactory {
84+
85+
private final AtomicInteger threadNumber = new AtomicInteger(1);
86+
87+
@Override
88+
public Thread newThread(Runnable r) {
89+
Thread thread = new Thread(r);
90+
thread.setName(ClassUtils.getShortName(getClass()) +
91+
"-" + this.threadNumber.getAndIncrement());
92+
return thread;
93+
}
94+
95+
}
96+
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2012-2016 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+
* http://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.health;
18+
19+
import java.util.Map;
20+
21+
/**
22+
* Strategy interface used by {@link CompositeHealthIndicator} to invoke
23+
* {@link HealthIndicator} instances.
24+
* <p>
25+
* This is useful for customization of invocation in scenarios with many
26+
* {@link HealthIndicator} instances in the system and/or resource demanding ones.
27+
*
28+
* @author Vedran Pavic
29+
*/
30+
public interface HealthIndicatorRunner {
31+
32+
Map<String, Health> run(Map<String, HealthIndicator> healthIndicators);
33+
34+
}

spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/HealthEndpointTests.java

+10
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
*
3838
* @author Phillip Webb
3939
* @author Christian Dupuis
40+
* @author Vedran Pavic
4041
*/
4142
public class HealthEndpointTests extends AbstractEndpointTests<HealthEndpoint> {
4243

@@ -50,6 +51,15 @@ public void invoke() throws Exception {
5051
assertThat(getEndpointBean().invoke().getStatus(), equalTo(Status.UNKNOWN));
5152
}
5253

54+
@Test
55+
public void invokeInParallel() throws Exception {
56+
HealthEndpoint healthEndpoint = getEndpointBean();
57+
healthEndpoint.setRunInParallel(true);
58+
healthEndpoint.setThreadCount(2);
59+
// As FINE isn't configured in the order we get UNKNOWN
60+
assertThat(healthEndpoint.invoke().getStatus(), equalTo(Status.UNKNOWN));
61+
}
62+
5363
@Configuration
5464
@EnableConfigurationProperties
5565
public static class Config {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2012-2016 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+
* http://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.health;
18+
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
import org.junit.Before;
23+
import org.junit.Rule;
24+
import org.junit.Test;
25+
import org.junit.rules.ExpectedException;
26+
import org.junit.runner.RunWith;
27+
import org.mockito.Mock;
28+
import org.mockito.runners.MockitoJUnitRunner;
29+
30+
import static org.hamcrest.CoreMatchers.equalTo;
31+
import static org.junit.Assert.assertThat;
32+
import static org.mockito.BDDMockito.given;
33+
34+
/**
35+
* Tests for {@link ExecutorServiceHealthIndicatorRunner}.
36+
*
37+
* @author Vedran Pavic
38+
*/
39+
@RunWith(MockitoJUnitRunner.class)
40+
public class ExecutorServiceHealthIndicatorRunnerTests {
41+
42+
@Rule
43+
public ExpectedException thrown = ExpectedException.none();
44+
45+
@Mock
46+
private HealthIndicator one;
47+
48+
@Mock
49+
private HealthIndicator two;
50+
51+
private Map<String, HealthIndicator> healthIndicators;
52+
53+
private ExecutorServiceHealthIndicatorRunner healthIndicatorRunner;
54+
55+
@Before
56+
public void setup() {
57+
given(this.one.health()).willReturn(new Health.Builder().up().build());
58+
given(this.two.health()).willReturn(new Health.Builder().unknown().build());
59+
60+
this.healthIndicators = new HashMap<String, HealthIndicator>(2);
61+
this.healthIndicators.put("one", this.one);
62+
this.healthIndicators.put("two", this.two);
63+
}
64+
65+
@Test
66+
public void createAndRun() {
67+
this.healthIndicatorRunner = new ExecutorServiceHealthIndicatorRunner(2);
68+
Map<String, Health> healths = this.healthIndicatorRunner.run(this.healthIndicators);
69+
assertThat(healths.size(), equalTo(2));
70+
assertThat(healths.get("one").getStatus(), equalTo(Status.UP));
71+
assertThat(healths.get("two").getStatus(), equalTo(Status.UNKNOWN));
72+
}
73+
74+
@Test
75+
public void createWithInvalidThreadCount() {
76+
this.thrown.expect(IllegalArgumentException.class);
77+
this.thrown.expectMessage("ThreadCount must be greater than one");
78+
this.healthIndicatorRunner = new ExecutorServiceHealthIndicatorRunner(0);
79+
}
80+
81+
}

0 commit comments

Comments
 (0)