Skip to content

Introduce strategy for running HealthIndicators #5066

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
import org.springframework.boot.actuate.health.HealthAggregator;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.HealthIndicatorRunner;
import org.springframework.core.ResolvableType;

/**
Expand All @@ -31,19 +32,26 @@
* @param <H> the health indicator type
* @param <S> the bean source type
* @author Stephane Nicoll
* @author Vedran Pavic
* @since 2.0.0
*/
public abstract class CompositeHealthIndicatorConfiguration<H extends HealthIndicator, S> {

@Autowired
private HealthAggregator healthAggregator;

@Autowired(required = false)
private HealthIndicatorRunner healthIndicatorRunner;

protected HealthIndicator createHealthIndicator(Map<String, S> beans) {
if (beans.size() == 1) {
return createHealthIndicator(beans.values().iterator().next());
}
CompositeHealthIndicator composite = new CompositeHealthIndicator(
this.healthAggregator);
if (this.healthIndicatorRunner != null) {
composite.setHealthIndicatorRunner(this.healthIndicatorRunner);
}
beans.forEach((name, source) -> composite.addHealthIndicator(name,
createHealthIndicator(source)));
return composite;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2012-2018 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
*
* http://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.Future;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.util.Assert;

/**
* A {@link HealthIndicatorRunner} implementation that uses {@link AsyncTaskExecutor} to
* invoke {@link HealthIndicator} instances.
*
* @author Vedran Pavic
* @since 2.1.0
*/
public class AsyncHealthIndicatorRunner implements HealthIndicatorRunner {

private static final Log logger = LogFactory.getLog(AsyncHealthIndicatorRunner.class);

private final AsyncTaskExecutor taskExecutor;

/**
* Create an {@link AsyncHealthIndicatorRunner} instance.
* @param taskExecutor task executor used to run {@link HealthIndicator}s
*/
public AsyncHealthIndicatorRunner(AsyncTaskExecutor taskExecutor) {
Assert.notNull(taskExecutor, "TaskExecutor must not be null");
this.taskExecutor = taskExecutor;
}

@Override
public Map<String, Health> run(Map<String, HealthIndicator> healthIndicators) {
Map<String, Health> healths = new HashMap<>(healthIndicators.size());
Map<String, Future<Health>> futures = new HashMap<>();
for (final Map.Entry<String, HealthIndicator> entry : healthIndicators
.entrySet()) {
Future<Health> future = this.taskExecutor
.submit(() -> entry.getValue().health());
futures.put(entry.getKey(), future);
}
for (Map.Entry<String, Future<Health>> entry : futures.entrySet()) {
try {
healths.put(entry.getKey(), entry.getValue().get());
}
catch (Exception e) {
logger.warn("Error invoking health indicator '" + entry.getKey() + "'",
e);
healths.put(entry.getKey(), Health.down(e).build());
}
}
return healths;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
Expand Down Expand Up @@ -27,6 +27,7 @@
* @author Tyler J. Frederick
* @author Phillip Webb
* @author Christian Dupuis
* @author Vedran Pavic
* @since 1.1.0
*/
public class CompositeHealthIndicator implements HealthIndicator {
Expand All @@ -35,6 +36,8 @@ public class CompositeHealthIndicator implements HealthIndicator {

private final HealthAggregator healthAggregator;

private HealthIndicatorRunner healthIndicatorRunner = new SequentialHealthIndicatorRunner();

/**
* Create a new {@link CompositeHealthIndicator}.
* @param healthAggregator the health aggregator
Expand All @@ -61,13 +64,34 @@ public void addHealthIndicator(String name, HealthIndicator indicator) {
this.indicators.put(name, indicator);
}

/**
* Set the health indicator runner to invoke the health indicators.
* @param healthIndicatorRunner the health indicator runner
*/
public void setHealthIndicatorRunner(HealthIndicatorRunner healthIndicatorRunner) {
Assert.notNull(healthIndicatorRunner, "HealthIndicatorRunner must not be null");
this.healthIndicatorRunner = healthIndicatorRunner;
}

@Override
public Health health() {
Map<String, Health> healths = new LinkedHashMap<>();
for (Map.Entry<String, HealthIndicator> entry : this.indicators.entrySet()) {
healths.put(entry.getKey(), entry.getValue().health());
}
Map<String, Health> healths = this.healthIndicatorRunner.run(this.indicators);
return this.healthAggregator.aggregate(healths);
}

/**
* {@link HealthIndicatorRunner} for sequential execution of {@link HealthIndicator}s.
*/
private static class SequentialHealthIndicatorRunner
implements HealthIndicatorRunner {

@Override
public Map<String, Health> run(Map<String, HealthIndicator> healthIndicators) {
Map<String, Health> healths = new LinkedHashMap<>();
healthIndicators.forEach((key, value) -> healths.put(key, value.health()));
return healths;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2012-2018 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
*
* http://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 by {@link CompositeHealthIndicator} to invoke
* {@link HealthIndicator} instances.
* <p>
* This is useful for customization of invocation in scenarios with many
* {@link HealthIndicator} instances in the system and/or resource demanding ones.
*
* @author Vedran Pavic
* @since 2.1.0
*/
public interface HealthIndicatorRunner {

Map<String, Health> run(Map<String, HealthIndicator> healthIndicators);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2012-2018 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
*
* http://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.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;

/**
* Tests for {@link AsyncHealthIndicatorRunner}.
*
* @author Vedran Pavic
*/
public class AsyncHealthIndicatorRunnerTests {

@Rule
public ExpectedException thrown = ExpectedException.none();

private HealthIndicator one = mock(HealthIndicator.class);

private HealthIndicator two = mock(HealthIndicator.class);

@Before
public void setUp() {
given(this.one.health()).willReturn(new Health.Builder().up().build());
given(this.two.health()).willReturn(new Health.Builder().unknown().build());
}

@Test
public void createAndRun() {
Map<String, HealthIndicator> indicators = new HashMap<>();
indicators.put("one", this.one);
indicators.put("two", this.two);
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setMaxPoolSize(2);
taskExecutor.afterPropertiesSet();
HealthIndicatorRunner healthIndicatorRunner = new AsyncHealthIndicatorRunner(
taskExecutor);
Map<String, Health> healths = healthIndicatorRunner.run(indicators);
assertThat(healths.size()).isEqualTo(2);
assertThat(healths.get("one").getStatus()).isEqualTo(Status.UP);
assertThat(healths.get("two").getStatus()).isEqualTo(Status.UNKNOWN);
}

@Test
public void createWithNullTaskExecutor() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("TaskExecutor must not be null");
new AsyncHealthIndicatorRunner(null);
}

}