Skip to content

Commit

Permalink
Provide a way to run HealthIndicators concurrently
Browse files Browse the repository at this point in the history
  • Loading branch information
nosan committed Jul 24, 2019
1 parent f3a138d commit 84a6ad2
Show file tree
Hide file tree
Showing 8 changed files with 453 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
Expand All @@ -33,7 +34,7 @@
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ HealthEndpointProperties.class, HealthIndicatorProperties.class })
@AutoConfigureAfter(HealthIndicatorAutoConfiguration.class)
@AutoConfigureAfter({ HealthIndicatorAutoConfiguration.class, TaskExecutionAutoConfiguration.class })
@Import({ HealthEndpointConfiguration.class, HealthEndpointWebExtensionConfiguration.class })
public class HealthEndpointAutoConfiguration {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@

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

import java.util.concurrent.Executor;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.health.CompositeConcurrentHealthIndicator;
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
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.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -38,8 +43,25 @@ class HealthEndpointConfiguration {

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "management.endpoint.health.parallel.enabled", havingValue = "false",
matchIfMissing = true)
HealthEndpoint healthEndpoint(HealthAggregator healthAggregator, HealthIndicatorRegistry registry) {
return new HealthEndpoint(new CompositeHealthIndicator(healthAggregator, registry));
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "management.endpoint.health.parallel.enabled", havingValue = "true")
static class ParallelHealthEndpointConfiguration {

@Bean
@ConditionalOnMissingBean
HealthEndpoint healthEndpoint(HealthAggregator healthAggregator, HealthIndicatorRegistry registry,
HealthEndpointProperties properties, @HealthExecutor ObjectProvider<Executor> healthExecutor,
ObjectProvider<Executor> executor) {
return new HealthEndpoint(new CompositeConcurrentHealthIndicator(healthAggregator, registry,
healthExecutor.getIfAvailable(executor::getObject), properties.getParallel().getTimeout()));
}

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2018 the original author or authors.
* 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.
Expand All @@ -16,6 +16,7 @@

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

import java.time.Duration;
import java.util.HashSet;
import java.util.Set;

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

/**
* Parallel configuration.
*/
private final Parallel parallel = new Parallel();

public ShowDetails getShowDetails() {
return this.showDetails;
}
Expand All @@ -59,4 +65,41 @@ public void setRoles(Set<String> roles) {
this.roles = roles;
}

public Parallel getParallel() {
return this.parallel;
}

/**
* Parallel properties.
*/
public static class Parallel {

/**
* Whether health indicators should be called concurrently.
*/
private boolean enabled;

/**
* Timeout to wait for the result from health indicators.
*/
private Duration timeout;

public boolean isEnabled() {
return this.enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

public Duration getTimeout() {
return this.timeout;
}

public void setTimeout(Duration timeout) {
this.timeout = timeout;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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.autoconfigure.health;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.Executor;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.actuate.health.CompositeConcurrentHealthIndicator;

/**
* Qualifier annotation for a {@link Executor} to be injected into
* {@link HealthEndpointAutoConfiguration}. The {@code Executor} used for
* {@link CompositeConcurrentHealthIndicator} in case if {@code parallel} is enabled.
*
* @author Dmytro Nosan
* @since 2.2.0
*/
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier
public @interface HealthExecutor {

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,28 @@

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

import java.time.Duration;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;

import org.springframework.boot.actuate.health.CompositeConcurrentHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.test.util.ReflectionTestUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
Expand Down Expand Up @@ -91,6 +103,50 @@ void healthEndpointMergeRegularAndReactive() {
});
}

@Test
void healthEndpointShouldCallHealthIndicatorsConcurrently() {
this.contextRunner.withPropertyValues("management.endpoint.health.parallel.enabled=true")
.withUserConfiguration(CustomExecutorConfiguration.class)
.withBean("simpleHealthIndicator", HealthIndicator.class, this::simpleSlowHealthIndicator)
.withBean("reactiveHealthIndicator", ReactiveHealthIndicator.class, this::reactiveSlowHealthIndicator)
.run((context) -> {
HealthIndicator indicator = context.getBean("simpleHealthIndicator", HealthIndicator.class);
ReactiveHealthIndicator reactiveHealthIndicator = context.getBean("reactiveHealthIndicator",
ReactiveHealthIndicator.class);
verify(indicator, never()).health();
verify(reactiveHealthIndicator, never()).health();
HealthEndpoint healthEndpoint = context.getBean(HealthEndpoint.class);
AtomicReference<Health> healthReference = new AtomicReference<>();
Assertions.assertTimeout(Duration.ofMillis(300),
() -> healthReference.set(healthEndpoint.health()));
Health health = healthReference.get();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsOnlyKeys("simple", "reactive");
verify(indicator).health();
verify(reactiveHealthIndicator).health();
});
}

@Test
void healthEndpointShouldUseHealthExecutor() {
this.contextRunner.withPropertyValues("management.endpoint.health.parallel.enabled=true")
.withUserConfiguration(HealthExecutorConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(HealthEndpoint.class).hasBean("healthExecutor");
HealthEndpoint healthEndpoint = context.getBean(HealthEndpoint.class);
Object healthIndicator = ReflectionTestUtils.getField(healthEndpoint, "healthIndicator");
assertThat(healthIndicator).isInstanceOf(CompositeConcurrentHealthIndicator.class);
assertThat(healthIndicator).hasFieldOrPropertyWithValue("executor",
context.getBean("healthExecutor", Executor.class));
});
}

@Test
void healthEndpointConfigurationShouldFailIfNoExecutor() {
this.contextRunner.withPropertyValues("management.endpoint.health.parallel.enabled=true")
.run((context) -> assertThat(context).getFailure()
.hasMessageContaining("No qualifying bean of type 'java.util.concurrent.Executor'"));
}

private HealthIndicator simpleHealthIndicator() {
HealthIndicator mock = mock(HealthIndicator.class);
given(mock.health()).willReturn(Health.status(Status.UP).build());
Expand All @@ -103,4 +159,49 @@ private ReactiveHealthIndicator reactiveHealthIndicator() {
return mock;
}

private HealthIndicator simpleSlowHealthIndicator() {
HealthIndicator mock = mock(HealthIndicator.class);
given(mock.health()).will((invocation) -> {
Thread.sleep(250);
return Health.status(Status.UP).build();
});
return mock;
}

private ReactiveHealthIndicator reactiveSlowHealthIndicator() {
ReactiveHealthIndicator mock = mock(ReactiveHealthIndicator.class);
given(mock.health()).will((invocation) -> {
Thread.sleep(250);
return Mono.just(Health.status(Status.UP).build());
});
return mock;
}

@Configuration(proxyBeanMethods = false)
static class CustomExecutorConfiguration {

@Bean(destroyMethod = "shutdown")
ExecutorService executor() {
return Executors.newCachedThreadPool();
}

}

@Configuration(proxyBeanMethods = false)
static class HealthExecutorConfiguration {

@Bean
@Primary
Executor primaryExecutor() {
return Runnable::run;
}

@Bean
@HealthExecutor
Executor healthExecutor() {
return Runnable::run;
}

}

}
Loading

0 comments on commit 84a6ad2

Please sign in to comment.