Add Spring gRPC server health check support

Add integration between Spring Boot's health module and gRPC
health checking service support.

Closes gh-49048

Co-authored-by: Chris Bono <chris.bono@broadcom.com>
This commit is contained in:
Phillip Webb
2026-03-19 15:12:31 -07:00
co-authored by Chris Bono
parent b650f3c558
commit 67ac57fb77
24 changed files with 2780 additions and 0 deletions
@@ -29,6 +29,9 @@ dependencies {
api("org.springframework.grpc:spring-grpc-core")
optional(project(":core:spring-boot-autoconfigure"))
optional(project(":module:spring-boot-health"))
optional("com.fasterxml.jackson.core:jackson-annotations")
optional("io.projectreactor:reactor-core")
optional("io.grpc:grpc-servlet-jakarta")
optional("io.grpc:grpc-services")
optional("io.grpc:grpc-netty")
@@ -0,0 +1,59 @@
/*
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponent;
import org.springframework.boot.grpc.server.health.StatusAggregator;
import org.springframework.boot.grpc.server.health.StatusMapper;
import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembership;
/**
* Auto-configured {@link HealthCheckedGrpcComponent}.
*
* @author Phillip Webb
*/
class AutoConfiguredHealthCheckedGrpcComponent implements HealthCheckedGrpcComponent {
private final HealthContributorMembership membership;
private final StatusAggregator statusAggregator;
private final StatusMapper statusMapper;
AutoConfiguredHealthCheckedGrpcComponent(HealthContributorMembership membership, StatusAggregator statusAggregator,
StatusMapper statusMapper) {
this.membership = membership;
this.statusAggregator = statusAggregator;
this.statusMapper = statusMapper;
}
@Override
public boolean isMember(String healthContributorName) {
return this.membership.isMember(healthContributorName);
}
@Override
public StatusAggregator getStatusAggregator() {
return this.statusAggregator;
}
@Override
public StatusMapper getStatusMapper() {
return this.statusMapper;
}
}
@@ -0,0 +1,144 @@
/*
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Supplier;
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthProperties.Service;
import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthProperties.Status;
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponent;
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponents;
import org.springframework.boot.grpc.server.health.StatusAggregator;
import org.springframework.boot.grpc.server.health.StatusMapper;
import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembership;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
/**
* Auto-configured {@link HealthCheckedGrpcComponents}.
*
* @author Phillip Webb
*/
class AutoConfiguredHealthCheckedGrpcComponents implements HealthCheckedGrpcComponents {
private final HealthCheckedGrpcComponent server;
private final Map<String, HealthCheckedGrpcComponent> services;
/**
* Create a new {@link AutoConfiguredHealthCheckedGrpcComponents} instance.
* @param applicationContext the application context used to check for override beans
* @param properties the grpc server health properties
*/
AutoConfiguredHealthCheckedGrpcComponents(ApplicationContext applicationContext,
GrpcServerHealthProperties properties) {
ListableBeanFactory beanFactory = (applicationContext instanceof ConfigurableApplicationContext configurableContext)
? configurableContext.getBeanFactory() : applicationContext;
StatusAggregator statusAggregator = getNonQualifiedBean(beanFactory, StatusAggregator.class,
() -> StatusAggregator.of(properties.getStatus().getOrder()));
StatusMapper statusMapper = getNonQualifiedBean(beanFactory, StatusMapper.class,
() -> StatusMapper.of(properties.getStatus().getMapping()));
this.server = new AutoConfiguredHealthCheckedGrpcComponent(HealthContributorMembership.always(),
statusAggregator, statusMapper);
this.services = createServices(properties.getService(), beanFactory, statusAggregator, statusMapper);
}
private Map<String, HealthCheckedGrpcComponent> createServices(Map<String, Service> serviceProperties,
BeanFactory beanFactory, StatusAggregator defaultStatusAggregator, StatusMapper defaultStatusMapper) {
Map<String, HealthCheckedGrpcComponent> services = new TreeMap<>();
serviceProperties.forEach((serviceName, service) -> {
Status status = service.getStatus();
StatusAggregator statusAggregator = getQualifiedBean(beanFactory, StatusAggregator.class, serviceName,
() -> createStatusAggregator(status.getOrder(), defaultStatusAggregator));
StatusMapper statusMapper = getQualifiedBean(beanFactory, StatusMapper.class, serviceName,
() -> createStatusMapper(status.getMapping(), defaultStatusMapper));
HealthContributorMembership membership = HealthContributorMembership.byIncludeExclude(service.getInclude(),
service.getExclude());
services.put(serviceName,
new AutoConfiguredHealthCheckedGrpcComponent(membership, statusAggregator, statusMapper));
});
return Collections.unmodifiableMap(services);
}
private StatusAggregator createStatusAggregator(List<String> order, StatusAggregator defaultStatusAggregator) {
return (!CollectionUtils.isEmpty(order)) ? StatusAggregator.of(order) : defaultStatusAggregator;
}
private StatusMapper createStatusMapper(Map<String, ServingStatus> mapping, StatusMapper defaultStatusMapper) {
return (!CollectionUtils.isEmpty(mapping)) ? StatusMapper.of(mapping) : defaultStatusMapper;
}
private <T> T getNonQualifiedBean(ListableBeanFactory beanFactory, Class<T> type, Supplier<T> fallback) {
List<String> candidates = new ArrayList<>();
for (String beanName : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) {
String[] aliases = beanFactory.getAliases(beanName);
if (!BeanFactoryAnnotationUtils.isQualifierMatch(
(qualifier) -> !qualifier.equals(beanName) && !ObjectUtils.containsElement(aliases, qualifier),
beanName, beanFactory)) {
candidates.add(beanName);
}
}
if (candidates.isEmpty()) {
return fallback.get();
}
if (candidates.size() == 1) {
return beanFactory.getBean(candidates.get(0), type);
}
return beanFactory.getBean(type);
}
private <T> T getQualifiedBean(BeanFactory beanFactory, Class<T> type, String qualifier, Supplier<T> fallback) {
try {
return BeanFactoryAnnotationUtils.qualifiedBeanOfType(beanFactory, type, qualifier);
}
catch (NoSuchBeanDefinitionException ex) {
return fallback.get();
}
}
@Override
public @Nullable HealthCheckedGrpcComponent getServer() {
return this.server;
}
@Override
public Set<String> getServiceNames() {
return this.services.keySet();
}
@Override
public @Nullable HealthCheckedGrpcComponent getService(String serviceName) {
return this.services.get(serviceName);
}
}
@@ -0,0 +1,142 @@
/*
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
import io.grpc.BindableService;
import io.grpc.Grpc;
import io.grpc.protobuf.services.HealthStatusManager;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration.NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition;
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponents;
import org.springframework.boot.grpc.server.health.StatusAggregator;
import org.springframework.boot.grpc.server.health.StatusMapper;
import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembershipValidator;
import org.springframework.boot.health.registry.HealthContributorRegistry;
import org.springframework.boot.health.registry.ReactiveHealthContributorRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.server.GrpcServerFactory;
/**
* {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side health service.
*
* @author Daniel Theuke
* @author Chris Bono
* @author Phillip Webb
* @since 4.1.0
*/
@AutoConfiguration(
afterName = "org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration")
@ConditionalOnClass({ GrpcServerFactory.class, Grpc.class, HealthStatusManager.class })
@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true)
@Conditional(NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition.class)
@EnableConfigurationProperties(GrpcServerHealthProperties.class)
public final class GrpcServerHealthAutoConfiguration {
@Bean(destroyMethod = "enterTerminalState")
@ConditionalOnMissingBean
HealthStatusManager grpcServerHealthStatusManager() {
return new HealthStatusManager();
}
@Bean
BindableService grpcServerHealthService(HealthStatusManager healthStatusManager) {
return healthStatusManager.getHealthService();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(type = "org.springframework.boot.health.registry.HealthContributorRegistry")
static class GrpcServerHealthContributorConfiguration {
static final String VALIDATE_MEMBERSHIP_PROPERTY = "spring.grpc.server.health.services.validate-membership";
@Bean
@ConditionalOnMissingBean
StatusAggregator grpcServerHealthStatusAggregator(GrpcServerHealthProperties properties) {
return StatusAggregator.of(properties.getStatus().getOrder());
}
@Bean
@ConditionalOnMissingBean
StatusMapper grpcServerHealthHttpCodeStatusMapper(GrpcServerHealthProperties properties) {
return StatusMapper.of(properties.getStatus().getMapping());
}
@Bean
@ConditionalOnMissingBean(HealthCheckedGrpcComponents.class)
AutoConfiguredHealthCheckedGrpcComponents grpcServerHealthCheckedGrpcComponents(
ApplicationContext applicationContext, GrpcServerHealthProperties properties) {
return new AutoConfiguredHealthCheckedGrpcComponents(applicationContext, properties);
}
@Bean
@ConditionalOnMissingBean
GrpcServerHealth grpcServerHealth(HealthContributorRegistry healthContributorRegistry,
ObjectProvider<ReactiveHealthContributorRegistry> reactiveHealthContributorRegistry,
HealthCheckedGrpcComponents healthCheckedGrpcComponents) {
return new GrpcServerHealth(healthContributorRegistry, reactiveHealthContributorRegistry.getIfAvailable(),
healthCheckedGrpcComponents);
}
@Bean
@ConditionalOnBooleanProperty(name = VALIDATE_MEMBERSHIP_PROPERTY, matchIfMissing = true)
HealthContributorMembershipValidator grpcServerHealthServiceMembershipValidator(
GrpcServerHealthProperties properties, HealthContributorRegistry healthContributorRegistry,
ObjectProvider<ReactiveHealthContributorRegistry> reactiveHealthContributorRegistry) {
return new HealthContributorMembershipValidator(healthContributorRegistry,
reactiveHealthContributorRegistry.getIfAvailable(), VALIDATE_MEMBERSHIP_PROPERTY,
(members) -> properties.getService().forEach((serviceName, service) -> {
String property = "spring.grpc.server.health.service." + serviceName;
members.member(property + ".include".formatted(serviceName), service.getInclude());
members.member(property + ".exclude".formatted(serviceName), service.getExclude());
}));
}
}
static class NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition extends AnyNestedCondition {
NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnBean(BindableService.class)
@ConditionalOnBooleanProperty(name = "spring.grpc.server.health.enabled", matchIfMissing = true)
static class NotDisabledAndHasBindableService {
}
@ConditionalOnBooleanProperty(name = "spring.grpc.server.health.enabled")
static class ExplicitlyEnabled {
}
}
}
@@ -0,0 +1,248 @@
/*
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.util.CollectionUtils;
/**
* {@link ConfigurationProperties Properties} for Spring gRPC server health.
*
* @author Phillip Webb
* @author Chris Bono
* @since 4.1.0
*/
@ConfigurationProperties("spring.grpc.server.health")
public class GrpcServerHealthProperties {
/**
* Whether to auto-configure Health feature on the gRPC server.
*/
private @Nullable Boolean enabled;
/**
* Whether to include the overall server health.
*/
private boolean includeOverallHealth = true;
/**
* Properties that apply to all services.
*/
private final Services services = new Services();
/**
* Service specific health reporting.
*/
private final Map<String, Service> service = new LinkedHashMap<>();
/**
* Status configuration.
*/
private final Status status = new Status();
/**
* Schedule configuration.
*/
private final Schedule schedule = new Schedule();
public @Nullable Boolean getEnabled() {
return this.enabled;
}
public void setEnabled(@Nullable Boolean enabled) {
this.enabled = enabled;
}
public boolean isIncludeOverallHealth() {
return this.includeOverallHealth;
}
public void setIncludeOverallHealth(boolean includeOverallHealth) {
this.includeOverallHealth = includeOverallHealth;
}
public Services getServices() {
return this.services;
}
public Map<String, Service> getService() {
return this.service;
}
public Status getStatus() {
return this.status;
}
public Schedule getSchedule() {
return this.schedule;
}
/**
* Properties applying to all services.
*/
public static class Services {
/**
* Whether to validate health membership for services on startup. Validation fails
* if a service includes or excludes a health contributor that does not exist.
*/
private boolean validateMembership = true;
public boolean isValidateMembership() {
return this.validateMembership;
}
public void setValidateMembership(boolean validateMembership) {
this.validateMembership = validateMembership;
}
}
/**
* A health for a specific service.
*/
public static class Service {
/**
* Health indicator IDs that should be included or '*' for all.
*/
private @Nullable Set<String> include;
/**
* Health indicator IDs that should be excluded or '*' for all.
*/
private @Nullable Set<String> exclude;
/**
* Status configuration.
*/
@NestedConfigurationProperty
private final Status status = new Status();
public Status getStatus() {
return this.status;
}
public @Nullable Set<String> getInclude() {
return this.include;
}
public void setInclude(@Nullable Set<String> include) {
this.include = include;
}
public @Nullable Set<String> getExclude() {
return this.exclude;
}
public void setExclude(@Nullable Set<String> exclude) {
this.exclude = exclude;
}
}
/**
* Status properties for the group.
*/
public static class Status {
/**
* List of health statuses in order of severity.
*/
private List<String> order = new ArrayList<>();
/**
* Mapping of health statuses to gRPC service status. By default, registered
* health statuses map to sensible defaults (for example, UP maps to SERVING).
*/
private final Map<String, ServingStatus> mapping = new HashMap<>();
public List<String> getOrder() {
return this.order;
}
public void setOrder(List<String> statusOrder) {
if (!CollectionUtils.isEmpty(statusOrder)) {
this.order = statusOrder;
}
}
public Map<String, ServingStatus> getMapping() {
return this.mapping;
}
}
/**
* Health task scheduling.
*/
public static class Schedule {
/**
* Whether to schedule updates to gRPC server health based on application health.
*/
private boolean enabled = true;
/**
* How often to update the health status.
*/
private Duration period = Duration.ofSeconds(5);
/**
* The initial delay before updating the health status the very first time.
*/
private Duration delay = Duration.ofSeconds(5);
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Duration getPeriod() {
return this.period;
}
public void setPeriod(Duration period) {
this.period = period;
}
public Duration getDelay() {
return this.delay;
}
public void setDelay(Duration delay) {
this.delay = delay;
}
}
}
@@ -0,0 +1,69 @@
/*
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ScheduledFuture;
import io.grpc.protobuf.services.HealthStatusManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
import org.springframework.context.ApplicationListener;
import org.springframework.core.log.LogMessage;
import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.function.SingletonSupplier;
/**
* Schedules gRPC health updates one the gRPC server has been started.
*
* @author Phillip Webb
* @author Chris Bono
*/
class GrpcServerHealthScheduler implements ApplicationListener<GrpcServerStartedEvent> {
private static final Log logger = LogFactory.getLog(GrpcServerHealthScheduler.class);
private final SingletonSupplier<ScheduledFuture<?>> scheduleHealth;
GrpcServerHealthScheduler(GrpcServerHealth grpcServerHealth, HealthStatusManager grpcServerHealthStatusManager,
TaskScheduler taskScheduler, Duration period, Duration delay) {
this(Clock.systemDefaultZone(), grpcServerHealth, grpcServerHealthStatusManager, taskScheduler, period, delay);
}
GrpcServerHealthScheduler(Clock clock, GrpcServerHealth grpcServerHealth,
HealthStatusManager grpcServerHealthStatusManager, TaskScheduler taskScheduler, Duration period,
Duration delay) {
this.scheduleHealth = SingletonSupplier.of(() -> {
logger.debug(LogMessage
.of(() -> "Scheduling gRPC server health updates every %s seconds (after a delay of %s seconds)"
.formatted((period.toMillis() / 1000.0), delay.toMillis() / 1000.0)));
Runnable task = () -> grpcServerHealth.update(grpcServerHealthStatusManager);
return taskScheduler.scheduleAtFixedRate(task, Instant.now(clock).plus(delay), period);
});
}
@Override
public void onApplicationEvent(GrpcServerStartedEvent event) {
this.scheduleHealth.get();
}
}
@@ -0,0 +1,58 @@
/*
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
import io.grpc.Grpc;
import io.grpc.protobuf.services.HealthStatusManager;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.task.DefaultTaskSchedulerConfiguration;
import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthProperties.Schedule;
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.grpc.server.GrpcServerFactory;
import org.springframework.scheduling.TaskScheduler;
/**
* {@link EnableAutoConfiguration Auto-configuration} to invoke {@link GrpcServerHealth}
* updates using a {@link TaskScheduler}.
*
* @author Phillip Webb
* @since 4.1.0
*/
@AutoConfiguration(after = GrpcServerHealthAutoConfiguration.class)
@ConditionalOnBooleanProperty(name = "spring.grpc.server.health.schedule.enabled", matchIfMissing = true)
@Import(DefaultTaskSchedulerConfiguration.class)
@ConditionalOnClass({ GrpcServerFactory.class, Grpc.class, HealthStatusManager.class })
public final class GrpcServerHealthSchedulerAutoConfiguration {
@Bean
@ConditionalOnBean({ TaskScheduler.class, GrpcServerHealth.class })
GrpcServerHealthScheduler grpcServerHealthScheduler(GrpcServerHealth grpcServerHealth,
HealthStatusManager grpcServerHealthStatusManager, TaskScheduler taskScheduler,
GrpcServerHealthProperties properties) {
Schedule schedule = properties.getSchedule();
return new GrpcServerHealthScheduler(grpcServerHealth, grpcServerHealthStatusManager, taskScheduler,
schedule.getPeriod(), schedule.getDelay());
}
}
@@ -0,0 +1,23 @@
/*
* Copyright 2012-present 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.
*/
/**
* Auto-configuration for gRPC server health checks.
*/
@NullMarked
package org.springframework.boot.grpc.server.autoconfigure.health;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,122 @@
/*
* Copyright 2012-present 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.grpc.server.health;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
import io.grpc.protobuf.services.HealthStatusManager;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.health.contributor.CompositeHealthContributor;
import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.HealthContributors;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.boot.health.contributor.Status;
import org.springframework.boot.health.registry.HealthContributorRegistry;
import org.springframework.boot.health.registry.ReactiveHealthContributorRegistry;
import org.springframework.util.Assert;
/**
* Provides health information for a gRPC server that can be used to update a
* {@link HealthStatusManager}.
*
* @author Phillip Webb
* @since 4.1.0
*/
public class GrpcServerHealth {
private HealthContributorRegistry registry;
private @Nullable ReactiveHealthContributorRegistry fallbackRegistry;
private HealthCheckedGrpcComponents components;
/**
* Create a new {@link GrpcServerHealth} instance.
* @param registry the health contributor registry
* @param fallbackRegistry the fallback registry or {@code null}
* @param components the components used to provide the server health
*/
public GrpcServerHealth(HealthContributorRegistry registry,
@Nullable ReactiveHealthContributorRegistry fallbackRegistry, HealthCheckedGrpcComponents components) {
Assert.notNull(registry, "'registry' must not be null");
Assert.notNull(components, "'components' must not be null");
this.registry = registry;
this.fallbackRegistry = fallbackRegistry;
this.components = components;
}
public void update(HealthStatusManager manager) {
update(manager::setStatus);
}
public void update(BiConsumer<String, ServingStatus> updator) {
Cache cache = new Cache();
HealthCheckedGrpcComponent serverComponent = this.components.getServer();
if (serverComponent != null) {
updator.accept("", getServingStatus(cache, serverComponent));
}
for (String serviceName : this.components.getServiceNames()) {
HealthCheckedGrpcComponent serviceComponent = this.components.getService(serviceName);
if (!serviceName.isEmpty() && serviceComponent != null) {
updator.accept(serviceName, getServingStatus(cache, serviceComponent));
}
}
}
private ServingStatus getServingStatus(Cache cache, HealthCheckedGrpcComponent component) {
Set<Status> statuses = new LinkedHashSet<>();
collectStatuses(cache, component, statuses, this.registry, "");
if (this.fallbackRegistry != null) {
collectStatuses(cache, component, statuses, this.fallbackRegistry.asHealthContributors(), "");
}
Status status = component.getStatusAggregator().getAggregateStatus(statuses);
return component.getStatusMapper().getServingStatus(status);
}
private void collectStatuses(Cache cache, HealthCheckedGrpcComponent component, Set<Status> statuses,
HealthContributors contributors, String prefix) {
for (HealthContributors.Entry entry : contributors) {
String name = (prefix.isEmpty()) ? entry.name() : prefix + "/" + entry.name();
if (entry.contributor() instanceof CompositeHealthContributor composite) {
collectStatuses(cache, component, statuses, composite, name);
}
else if (component.isMember(name)) {
Health health = cache.getHealth(name, (HealthIndicator) entry.contributor());
if (health != null) {
statuses.add(health.getStatus());
}
}
}
}
class Cache {
private final Map<String, Health> health = new HashMap<>();
Health getHealth(String name, HealthIndicator indicator) {
return this.health.computeIfAbsent(name, (key) -> indicator.health(false));
}
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2012-present 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.grpc.server.health;
/**
* A single gRPC component that can be checked for health.
*
* @author Phillip Webb
* @since 4.1.0
*/
public interface HealthCheckedGrpcComponent {
/**
* Returns {@code true} if the given health contributor is a member of this component.
* @param healthContributorName the contributor name
* @return {@code true} if the contributor is a member
*/
boolean isMember(String healthContributorName);
/**
* Returns the status aggregator that should be used for this component.
* @return the status aggregator
*/
StatusAggregator getStatusAggregator();
/**
* Returns the {@link StatusMapper} that should be used for this component.
* @return the status mapper
*/
StatusMapper getStatusMapper();
}
@@ -0,0 +1,54 @@
/*
* Copyright 2012-present 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.grpc.server.health;
import java.util.Set;
import org.jspecify.annotations.Nullable;
/**
* A collection of {@link HealthCheckedGrpcComponent components} used to check the health
* of a gRPC server.
*
* @author Phillip Webb
* @since 4.1.0
* @see GrpcServerHealth
*/
public interface HealthCheckedGrpcComponents {
/**
* Return the component that represents the overall server health or {@code null} if
* no overall health should be reported.
* @return the server component or {@code null}
*/
@Nullable HealthCheckedGrpcComponent getServer();
/**
* Return the names of the services that contribute health checks.
* @return the service names
*/
Set<String> getServiceNames();
/**
* Return the component for the service with the specified name or {@code null} if the
* name is not known.
* @param serviceName the name of the service
* @return the service component or {@code null}
*/
@Nullable HealthCheckedGrpcComponent getService(String serviceName);
}
@@ -0,0 +1,79 @@
/*
* Copyright 2012-present 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.grpc.server.health;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.health.contributor.Status;
import org.springframework.lang.Contract;
/**
* Simple {@link StatusMapper} backed by map of {@link Status#getCode() status code} to
* {@link ServingStatus}.
*
* @author Phillip Webb
*/
class SimpleServingStatusMapper implements StatusMapper {
static final SimpleServingStatusMapper DEFAULT_MAPPINGS;
static {
Map<String, ServingStatus> mappings = new HashMap<>();
mappings.put(Status.DOWN.getCode(), ServingStatus.NOT_SERVING);
mappings.put(Status.OUT_OF_SERVICE.getCode(), ServingStatus.NOT_SERVING);
mappings.put(Status.UNKNOWN.getCode(), ServingStatus.UNKNOWN);
DEFAULT_MAPPINGS = new SimpleServingStatusMapper(mappings);
}
private final Map<String, ServingStatus> mappings;
SimpleServingStatusMapper(Map<String, ServingStatus> mappings) {
this.mappings = getUniformMappings(mappings);
}
@Override
public ServingStatus getServingStatus(Status status) {
String code = getUniformCode(status.getCode());
return this.mappings.getOrDefault(code, ServingStatus.SERVING);
}
private static Map<String, ServingStatus> getUniformMappings(Map<String, ServingStatus> mappings) {
Map<String, ServingStatus> result = new LinkedHashMap<>();
for (Map.Entry<String, ServingStatus> entry : mappings.entrySet()) {
String code = getUniformCode(entry.getKey());
if (code != null) {
result.putIfAbsent(code, entry.getValue());
}
}
return Collections.unmodifiableMap(result);
}
@Contract("!null -> !null")
private static @Nullable String getUniformCode(@Nullable String code) {
return (code != null) ? code.codePoints()
.filter(Character::isLetterOrDigit)
.map(Character::toLowerCase)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString() : null;
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2012-present 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.grpc.server.health;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.health.contributor.Status;
import org.springframework.lang.Contract;
/**
* {@link StatusAggregator} backed by an ordered status list.
*
* @author Phillip Webb
*/
class SimpleStatusAggregator implements StatusAggregator {
static final SimpleStatusAggregator DEFAULT_ORDER = new SimpleStatusAggregator(
Status.DEFAULT_ORDER.stream().map(Status::getCode));
private final List<String> order;
private final Comparator<Status> comparator = Comparator.comparingInt(this::orderIndex)
.thenComparing(Status::getCode);
SimpleStatusAggregator(Stream<String> order) {
this.order = order.map(SimpleStatusAggregator::getUniformCode).toList();
}
@Override
public Status getAggregateStatus(Set<Status> statuses) {
return statuses.stream().filter(this::contains).min(this.comparator).orElse(Status.UNKNOWN);
}
private boolean contains(Status status) {
return this.order.contains(getUniformCode(status));
}
private int orderIndex(Status status) {
return this.order.indexOf(getUniformCode(status));
}
private static @Nullable String getUniformCode(Status status) {
return getUniformCode(status.getCode());
}
@Contract("!null -> !null")
private static @Nullable String getUniformCode(@Nullable String code) {
return (code != null) ? code.codePoints()
.filter(Character::isLetterOrDigit)
.map(Character::toLowerCase)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString() : null;
}
}
@@ -0,0 +1,95 @@
/*
* Copyright 2012-present 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.grpc.server.health;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.Status;
import org.springframework.util.ObjectUtils;
/**
* Strategy used to aggregate {@link Status} instances.
* <p>
* This is required in order to combine subsystem states expressed through
* {@link Health#getStatus()} into one state for the gRPC health.
*
* @author Phillip Webb
* @since 4.1.0
*/
@FunctionalInterface
public interface StatusAggregator {
/**
* Return the aggregate status for the given set of statuses.
* @param statuses the statuses to aggregate
* @return the aggregate status
*/
default Status getAggregateStatus(Status... statuses) {
return getAggregateStatus(new LinkedHashSet<>(Arrays.asList(statuses)));
}
/**
* Return the aggregate status for the given set of statuses.
* @param statuses the statuses to aggregate
* @return the aggregate status
*/
Status getAggregateStatus(Set<Status> statuses);
/**
* Return a {@link StatusAggregator} backed by the given ordered status list.
* @param order the status order
* @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is
* provided
*/
static StatusAggregator of(Status... order) {
return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(Arrays.stream(order).map(Status::getCode))
: getDefault();
}
/**
* Return a {@link StatusAggregator} backed by the given ordered status list.
* @param order the status order
* @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is
* provided
*/
static StatusAggregator of(String... order) {
return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(Arrays.stream(order)) : getDefault();
}
/**
* Return a {@link StatusAggregator} backed by the given ordered status list.
* @param order the status order
* @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is
* provided
*/
static StatusAggregator of(List<String> order) {
return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(order.stream()) : getDefault();
}
/**
* Return {@link StatusAggregator} instance using default ordering rules.
* @return a {@code StatusAggregator} with default ordering rules.
*/
static StatusAggregator getDefault() {
return SimpleStatusAggregator.DEFAULT_ORDER;
}
}
@@ -0,0 +1,62 @@
/*
* Copyright 2012-present 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.grpc.server.health;
import java.util.Map;
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.health.contributor.Status;
import org.springframework.util.CollectionUtils;
/**
* Strategy used to map a {@link Status health status} to a gRPC {@link ServingStatus}.
*
* @author Phillip Webb
* @since 4.1.0
*/
@FunctionalInterface
public interface StatusMapper {
/**
* Return the HTTP status code that corresponds to the given {@link Status health
* status}.
* @param status the health status to map
* @return the corresponding HTTP status code
*/
ServingStatus getServingStatus(Status status);
/**
* Create a new {@link StatusMapper} with the specified mappings.
* @param mappings the mappings to use or {@code null} to use the default mappings
* @return a {@link StatusMapper} or {@link #getDefault()}
*/
static StatusMapper of(@Nullable Map<String, ServingStatus> mappings) {
return CollectionUtils.isEmpty(mappings) ? SimpleServingStatusMapper.DEFAULT_MAPPINGS
: new SimpleServingStatusMapper(mappings);
}
/**
* Return an {@link StatusMapper} instance using default mappings.
* @return a mapper using default mappings
*/
static StatusMapper getDefault() {
return SimpleServingStatusMapper.DEFAULT_MAPPINGS;
}
}
@@ -0,0 +1,23 @@
/*
* Copyright 2012-present 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.
*/
/**
* Support for gRPC server health checks.
*/
@NullMarked
package org.springframework.boot.grpc.server.health;
import org.jspecify.annotations.NullMarked;
@@ -1,2 +1,4 @@
org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration
org.springframework.boot.grpc.server.autoconfigure.GrpcServerServicesAutoConfiguration
org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration
org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthSchedulerAutoConfiguration
@@ -0,0 +1,75 @@
/*
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.grpc.server.health.StatusAggregator;
import org.springframework.boot.grpc.server.health.StatusMapper;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link AutoConfiguredHealthCheckedGrpcComponent}.
*
* @author Phillip Webb
*/
@ExtendWith(MockitoExtension.class)
class AutoConfiguredHealthCheckedGrpcComponentTests {
@Mock
@SuppressWarnings("NullAway.Init")
private StatusAggregator statusAggregator;
@Mock
@SuppressWarnings("NullAway.Init")
private StatusMapper statusMapper;
@Test
void isMemberWhenMembershipMatchesAcceptsTrue() {
AutoConfiguredHealthCheckedGrpcComponent component = new AutoConfiguredHealthCheckedGrpcComponent(
(name) -> name.startsWith("a"), this.statusAggregator, this.statusMapper);
assertThat(component.isMember("albert")).isTrue();
assertThat(component.isMember("arnold")).isTrue();
}
@Test
void isMemberWhenMembershipRejectsReturnsTrue() {
AutoConfiguredHealthCheckedGrpcComponent component = new AutoConfiguredHealthCheckedGrpcComponent(
(name) -> name.startsWith("a"), this.statusAggregator, this.statusMapper);
assertThat(component.isMember("bert")).isFalse();
assertThat(component.isMember("ernie")).isFalse();
}
@Test
void getStatusAggregatorReturnsStatusAggregator() {
AutoConfiguredHealthCheckedGrpcComponent component = new AutoConfiguredHealthCheckedGrpcComponent(
(name) -> true, this.statusAggregator, this.statusMapper);
assertThat(component.getStatusAggregator()).isSameAs(this.statusAggregator);
}
@Test
void getStatusMapperReturnsHttpCodeStatusMapper() {
AutoConfiguredHealthCheckedGrpcComponent component = new AutoConfiguredHealthCheckedGrpcComponent(
(name) -> true, this.statusAggregator, this.statusMapper);
assertThat(component.getStatusMapper()).isSameAs(this.statusMapper);
}
}
@@ -0,0 +1,437 @@
/*
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
import java.util.Collections;
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponent;
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponents;
import org.springframework.boot.grpc.server.health.StatusAggregator;
import org.springframework.boot.grpc.server.health.StatusMapper;
import org.springframework.boot.health.contributor.Status;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link AutoConfiguredHealthCheckedGrpcComponent}.
*
* @author Phillip Webb
*/
class AutoConfiguredHealthCheckedGrpcComponentsTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(AutoConfiguredHealthCheckedGrpcComponentsTestConfiguration.class));
@Test
void getServerMatchesAllMembers() {
this.contextRunner.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent server = components.getServer();
assertThat(server).isNotNull();
assertThat(server.isMember("a")).isTrue();
assertThat(server.isMember("b")).isTrue();
assertThat(server.isMember("C")).isTrue();
});
}
@Test
void getServiceNamesReturnsServiceNames() {
this.contextRunner
.withPropertyValues("spring.grpc.server.health.service.a.include=*",
"spring.grpc.server.health.service.b.include=*")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
assertThat(components.getServiceNames()).containsExactlyInAnyOrder("a", "b");
});
}
@Test
void getServiceWhenServiceExistsReturnsService() {
this.contextRunner.withPropertyValues("spring.grpc.server.health.service.a.include=*").run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent component = components.getService("a");
assertThat(component).isNotNull();
});
}
@Test
void getServiceWhenServiceDoesNotExistReturnsNull() {
this.contextRunner.withPropertyValues("spring.grpc.server.health.service.a.include=*").run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent component = components.getService("b");
assertThat(component).isNull();
});
}
@Test
void createWhenNoDefinedBeansAdaptsProperties() {
this.contextRunner
.withPropertyValues("spring.grpc.server.health.status.order=up,down",
"spring.grpc.server.health.status.mapping.down=serving")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent server = components.getServer();
assertThat(server).isNotNull();
assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN))
.isEqualTo(Status.UP);
assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING);
});
}
@Test
void createWhenHasStatusAggregatorBeanReturnsInstanceWithAggregatorUsedForAllServices() {
this.contextRunner.withUserConfiguration(CustomStatusAggregatorConfiguration.class)
.withPropertyValues("spring.grpc.server.health.status.order=up,down",
"spring.grpc.server.health.service.a.include=*")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent server = components.getServer();
HealthCheckedGrpcComponent serviceA = components.getService("a");
assertThat(server).isNotNull();
assertThat(serviceA).isNotNull();
assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
.isEqualTo(Status.UNKNOWN);
assertThat(serviceA).isNotNull();
assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
.isEqualTo(Status.UNKNOWN);
});
}
@Test
void createWhenHasStatusAggregatorBeanAndServiceSpecificPropertyReturnsInstanceThatUsesBeanOnlyForUnconfiguredServices() {
this.contextRunner.withUserConfiguration(CustomStatusAggregatorConfiguration.class)
.withPropertyValues("spring.grpc.server.health.service.a.include=*",
"spring.grpc.server.health.service.a.status.order=up,down",
"spring.grpc.server.health.service.b.include=*")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent server = components.getServer();
HealthCheckedGrpcComponent serviceA = components.getService("a");
HealthCheckedGrpcComponent serviceB = components.getService("b");
assertThat(server).isNotNull();
assertThat(serviceA).isNotNull();
assertThat(serviceB).isNotNull();
assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
.isEqualTo(Status.UNKNOWN);
assertThat(serviceA).isNotNull();
assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
.isEqualTo(Status.UP);
assertThat(serviceB).isNotNull();
assertThat(serviceB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
.isEqualTo(Status.UNKNOWN);
});
}
@Test
void createWhenHasStatusAggregatorPropertyReturnsInstanceWithPropertyUsedForAllServices() {
this.contextRunner
.withPropertyValues("spring.grpc.server.health.status.order=up,down",
"spring.grpc.server.health.service.a.include=*")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent server = components.getServer();
HealthCheckedGrpcComponent serviceA = components.getService("a");
assertThat(server).isNotNull();
assertThat(serviceA).isNotNull();
assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN))
.isEqualTo(Status.UP);
assertThat(serviceA).isNotNull();
assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN))
.isEqualTo(Status.UP);
});
}
@Test
void createWhenHasStatusAggregatorPropertyAndServiceSpecificPropertyReturnsInstanceWithPropertyUsedForExpectedServices() {
this.contextRunner
.withPropertyValues("spring.grpc.server.health.status.order=up,down",
"spring.grpc.server.health.service.a.include=*",
"spring.grpc.server.health.service.a.status.order=unknown,up,down",
"spring.grpc.server.health.service.b.include=*")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent server = components.getServer();
HealthCheckedGrpcComponent serviceA = components.getService("a");
HealthCheckedGrpcComponent serviceB = components.getService("b");
assertThat(server).isNotNull();
assertThat(serviceA).isNotNull();
assertThat(serviceB).isNotNull();
assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
.isEqualTo(Status.UP);
assertThat(serviceA).isNotNull();
assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
.isEqualTo(Status.UNKNOWN);
assertThat(serviceB).isNotNull();
assertThat(serviceB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
.isEqualTo(Status.UP);
});
}
@Test
void createWhenHasStatusAggregatorPropertyAndServiceQualifiedBeanReturnsInstanceWithBeanUsedForExpectedServices() {
this.contextRunner.withUserConfiguration(CustomStatusAggregatorServiceAConfiguration.class)
.withPropertyValues("spring.grpc.server.health.status.order=up,down",
"spring.grpc.server.health.service.a.include=*",
"spring.grpc.server.health.service.a.status.order=up,down",
"spring.grpc.server.health.service.b.include=*")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent server = components.getServer();
HealthCheckedGrpcComponent serviceA = components.getService("a");
HealthCheckedGrpcComponent serviceB = components.getService("b");
assertThat(server).isNotNull();
assertThat(serviceA).isNotNull();
assertThat(serviceB).isNotNull();
assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
.isEqualTo(Status.UP);
assertThat(serviceA).isNotNull();
assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
.isEqualTo(Status.UNKNOWN);
assertThat(serviceB).isNotNull();
assertThat(serviceB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
.isEqualTo(Status.UP);
});
}
@Test
void createWhenHasServiceSpecificStatusAggregatorPropertyAndServiceQualifiedBeanReturnsInstanceWithBeanUsedForExpectedServices() {
this.contextRunner.withUserConfiguration(CustomStatusAggregatorServiceAConfiguration.class)
.withPropertyValues("spring.grpc.server.health.service.a.include=*",
"spring.grpc.server.health.service.a.status.order=up,down",
"spring.grpc.server.health.service.b.include=*",
"spring.grpc.server.health.service.b.status.order=up,down")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent server = components.getServer();
HealthCheckedGrpcComponent serviceA = components.getService("a");
HealthCheckedGrpcComponent serviceB = components.getService("b");
assertThat(server).isNotNull();
assertThat(serviceA).isNotNull();
assertThat(serviceB).isNotNull();
assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
.isEqualTo(Status.DOWN);
assertThat(serviceA).isNotNull();
assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
.isEqualTo(Status.UNKNOWN);
assertThat(serviceB).isNotNull();
assertThat(serviceB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
.isEqualTo(Status.UP);
});
}
@Test
void createWhenHasStatusMapperBeanReturnsInstanceWithMapperUsedForAllServices() {
this.contextRunner.withUserConfiguration(CustomStatusMapperConfiguration.class)
.withPropertyValues("spring.grpc.server.health.status.mapping.down=service-unknown",
"spring.grpc.server.health.service.a.include=*")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent server = components.getServer();
HealthCheckedGrpcComponent serviceA = components.getService("a");
assertThat(server).isNotNull();
assertThat(serviceA).isNotNull();
assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING);
assertThat(serviceA).isNotNull();
assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING);
});
}
@Test
void createWhenHasStatusMapperBeanAndServiceSpecificPropertyReturnsInstanceThatUsesBeanOnlyForUnconfiguredServices() {
this.contextRunner.withUserConfiguration(CustomStatusMapperConfiguration.class)
.withPropertyValues("spring.grpc.server.health.service.a.include=*",
"spring.grpc.server.health.service.a.status.mapping.down=service-unknown",
"spring.grpc.server.health.service.b.include=*")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent server = components.getServer();
HealthCheckedGrpcComponent serviceA = components.getService("a");
HealthCheckedGrpcComponent serviceB = components.getService("b");
assertThat(server).isNotNull();
assertThat(serviceA).isNotNull();
assertThat(serviceB).isNotNull();
assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING);
assertThat(serviceA).isNotNull();
assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN))
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
assertThat(serviceB).isNotNull();
assertThat(serviceB.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING);
});
}
@Test
void createWhenHasStatusMapperPropertyReturnsInstanceWithPropertyUsedForAllServices() {
this.contextRunner
.withPropertyValues("spring.grpc.server.health.status.mapping.down=service-unknown",
"spring.grpc.server.health.service.a.include=*")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent server = components.getServer();
HealthCheckedGrpcComponent serviceA = components.getService("a");
assertThat(server).isNotNull();
assertThat(serviceA).isNotNull();
assertThat(server.getStatusMapper().getServingStatus(Status.DOWN))
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
assertThat(serviceA).isNotNull();
assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN))
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
});
}
@Test
void createWhenHasStatusMapperPropertyAndServiceSpecificPropertyReturnsInstanceWithPropertyUsedForExpectedServices() {
this.contextRunner
.withPropertyValues("spring.grpc.server.health.status.mapping.down=service-unknown",
"spring.grpc.server.health.service.a.include=*",
"spring.grpc.server.health.service.a.status.mapping.down=unrecognized",
"spring.grpc.server.health.service.b.include=*")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent server = components.getServer();
HealthCheckedGrpcComponent serviceA = components.getService("a");
HealthCheckedGrpcComponent serviceB = components.getService("b");
assertThat(server).isNotNull();
assertThat(serviceA).isNotNull();
assertThat(serviceB).isNotNull();
assertThat(server.getStatusMapper().getServingStatus(Status.DOWN))
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
assertThat(serviceA).isNotNull();
assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN))
.isEqualTo(ServingStatus.UNRECOGNIZED);
assertThat(serviceB).isNotNull();
assertThat(serviceB.getStatusMapper().getServingStatus(Status.DOWN))
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
});
}
@Test
void createWhenHasStatusMapperPropertyAndServiceQualifiedBeanReturnsInstanceWithBeanUsedForExpectedServices() {
this.contextRunner.withUserConfiguration(CustomStatusMapperServiceAConfiguration.class)
.withPropertyValues("spring.grpc.server.health.status.mapping.down=service-unknown",
"spring.grpc.server.health.service.a.include=*",
"spring.grpc.server.health.service.a.status.mapping.down=service-unknown",
"spring.grpc.server.health.service.b.include=*")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent server = components.getServer();
HealthCheckedGrpcComponent serviceA = components.getService("a");
HealthCheckedGrpcComponent serviceB = components.getService("b");
assertThat(server).isNotNull();
assertThat(serviceA).isNotNull();
assertThat(serviceB).isNotNull();
assertThat(server.getStatusMapper().getServingStatus(Status.DOWN))
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
assertThat(serviceA).isNotNull();
assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING);
assertThat(serviceB).isNotNull();
assertThat(serviceB.getStatusMapper().getServingStatus(Status.DOWN))
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
});
}
@Test
void createWhenHasServiceSpecificStatusMapperPropertyAndServiceQualifiedBeanReturnsInstanceWithBeanUsedForExpectedServices() {
this.contextRunner.withUserConfiguration(CustomStatusMapperServiceAConfiguration.class)
.withPropertyValues("spring.grpc.server.health.service.a.include=*",
"spring.grpc.server.health.service.a.status.mapping.down=service-unknown",
"spring.grpc.server.health.service.b.include=*",
"spring.grpc.server.health.service.b.status.mapping.down=service-unknown")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
HealthCheckedGrpcComponent server = components.getServer();
HealthCheckedGrpcComponent serviceA = components.getService("a");
HealthCheckedGrpcComponent serviceB = components.getService("b");
assertThat(server).isNotNull();
assertThat(serviceA).isNotNull();
assertThat(serviceB).isNotNull();
assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.NOT_SERVING);
assertThat(serviceA).isNotNull();
assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING);
assertThat(serviceB).isNotNull();
assertThat(serviceB.getStatusMapper().getServingStatus(Status.DOWN))
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
});
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(GrpcServerHealthProperties.class)
static class AutoConfiguredHealthCheckedGrpcComponentsTestConfiguration {
@Bean
AutoConfiguredHealthCheckedGrpcComponents healthCheckedGrpcComponents(
ConfigurableApplicationContext applicationContext, GrpcServerHealthProperties properties) {
return new AutoConfiguredHealthCheckedGrpcComponents(applicationContext, properties);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomStatusAggregatorConfiguration {
@Bean
@Primary
StatusAggregator statusAggregator() {
return StatusAggregator.of(Status.UNKNOWN, Status.UP, Status.DOWN);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomStatusAggregatorServiceAConfiguration {
@Bean
@Qualifier("a")
StatusAggregator statusAggregator() {
return StatusAggregator.of(Status.UNKNOWN, Status.UP, Status.DOWN);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomStatusMapperConfiguration {
@Bean
@Primary
StatusMapper httpCodeStatusMapper() {
return StatusMapper.of(Collections.singletonMap(Status.DOWN.getCode(), ServingStatus.SERVING));
}
}
@Configuration(proxyBeanMethods = false)
static class CustomStatusMapperServiceAConfiguration {
@Bean
@Qualifier("a")
StatusMapper httpCodeStatusMapper() {
return StatusMapper.of(Collections.singletonMap(Status.DOWN.getCode(), ServingStatus.SERVING));
}
}
}
@@ -0,0 +1,458 @@
/*
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import io.grpc.BindableService;
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
import io.grpc.protobuf.services.HealthStatusManager;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration;
import org.springframework.boot.diagnostics.FailureAnalyzedException;
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponents;
import org.springframework.boot.grpc.server.health.StatusAggregator;
import org.springframework.boot.grpc.server.health.StatusMapper;
import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembershipValidator;
import org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration;
import org.springframework.boot.health.contributor.CompositeHealthContributor;
import org.springframework.boot.health.contributor.CompositeReactiveHealthContributor;
import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.boot.health.contributor.ReactiveHealthIndicator;
import org.springframework.boot.health.contributor.Status;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.server.GrpcServerFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GrpcServerHealthAutoConfiguration}.
*
* @author Phillip Webb
* @author Chris Bono
* @author Andrey Litvitski
*/
class GrpcServerHealthAutoConfigurationTests {
private static final AutoConfigurations autoConfigurations = AutoConfigurations.of(
GrpcServerHealthAutoConfiguration.class, GrpcServerHealthSchedulerAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class, TaskSchedulingAutoConfiguration.class);
private final BindableService service = mock();
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withUserConfiguration(HealthIndicatorsConfiguration.class)
.withConfiguration(autoConfigurations)
.with(this::serviceBean);
@Test
void autoConfiguresBeans() {
this.contextRunner.run(this::assertConfigured);
}
@Test
void whenNoBindableServiceDefinedDoesNotAutoConfigureBeans() {
new ApplicationContextRunner().withConfiguration(autoConfigurations).run(this::assertNotConfigured);
}
@Test
void whenNoBindableServiceDefinedButHealthEnabledPropertyIsTrueAutoConfiguresBeans() {
new ApplicationContextRunner().withConfiguration(autoConfigurations)
.withPropertyValues("spring.grpc.server.health.enabled=true")
.run(this::assertConfigured);
}
@Test
void whenGrpcNotOnClasspathDoesNotAutoConfigureBeans() {
this.contextRunner.withClassLoader(new FilteredClassLoader(BindableService.class))
.run(this::assertNotConfigured);
}
@Test
void whenSpringGrpcNotOnClasspathDoesNotAutoConfigureBeans() {
this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerFactory.class))
.run(this::assertNotConfigured);
}
@Test
void whenHealthStatusManagerNotOnClasspathDoesNotAutoConfigureBeans() {
this.contextRunner.withClassLoader(new FilteredClassLoader(HealthStatusManager.class))
.run(this::assertNotConfigured);
}
@Test
void whenNoTaskSchedulerDoesNotAutoConfigureHealthScheduler() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GrpcServerHealthAutoConfiguration.class,
HealthContributorRegistryAutoConfiguration.class))
.with(this::serviceBean)
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerHealthScheduler.class));
}
@Test
void whenHealthScheduleEnabledPropertyFalseDoesNotAutoConfigureHealthScheduler() {
this.contextRunner.withPropertyValues("spring.grpc.server.health.schedule.enabled=false")
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerHealthScheduler.class));
}
@Test
void whenHealthScheduleEnabledPropertyTrueDoesAutoConfigureHealthScheduler() {
this.contextRunner.withPropertyValues("spring.grpc.server.health.schedule.enabled=true")
.run((context) -> assertThat(context).hasSingleBean(GrpcServerHealthScheduler.class));
}
@Test
void whenHealthEnabledPropertyIsTrueAutoConfiguresBeans() {
this.contextRunner.withPropertyValues("spring.grpc.server.health.enabled=true").run(this::assertConfigured);
}
@Test
void whenHealthEnabledPropertyIsFalseDoesNotAutoConfigureBeans() {
this.contextRunner.withPropertyValues("spring.grpc.server.health.enabled=false").run(this::assertNotConfigured);
}
@Test
void whenServerEnabledPropertyIsFalseDoesNotAutoConfigureBeans() {
this.contextRunner.withPropertyValues("spring.grpc.server.enabled=false").run(this::assertNotConfigured);
}
@Test
void whenServerEnabledPropertyIsTrueAutoConfiguresBeans() {
this.contextRunner.withPropertyValues("spring.grpc.server.enabled=true").run(this::assertConfigured);
}
@Test
void enterTerminalStateIsCalledOnShutdown() {
AtomicReference<HealthStatusManager> manager = new AtomicReference<>();
this.contextRunner.run((context) -> {
manager.set(context.getBean(HealthStatusManager.class));
assertTerminalState(manager.get(), false);
});
assertTerminalState(manager.get(), true);
}
@Test
void whenHasUserDefinedHealthStatusManagerDoesNotAutoConfigureBean() {
HealthStatusManager customHealthStatusManager = mock();
this.contextRunner
.withBean("customHealthStatusManager", HealthStatusManager.class, () -> customHealthStatusManager)
.run((context) -> assertThat(context).getBean(HealthStatusManager.class)
.isSameAs(customHealthStatusManager));
}
@Test
void whenHasNoHealthContributorRegistryOnlyAutoConfiguresBasicService() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GrpcServerHealthAutoConfiguration.class,
TaskSchedulingAutoConfiguration.class))
.with(this::serviceBean)
.run((context) -> {
assertThat(context).hasSingleBean(HealthStatusManager.class);
assertThat(context).hasBean("grpcServerHealthService");
assertThat(context).doesNotHaveBean(GrpcServerHealth.class);
});
}
@Test
void createsStatusAggregatorFromProperties() {
this.contextRunner.withPropertyValues("spring.grpc.server.health.status.order=up,down").run((context) -> {
StatusAggregator aggregator = context.getBean(StatusAggregator.class);
assertThat(aggregator.getAggregateStatus(Status.UP, Status.DOWN)).isEqualTo(Status.UP);
});
}
@Test
void whenHasStatusAggregatorBeanIgnoresProperties() {
this.contextRunner.withUserConfiguration(StatusAggregatorConfiguration.class)
.withPropertyValues("spring.grpc.server.health.status.order=up,down")
.run((context) -> {
StatusAggregator aggregator = context.getBean(StatusAggregator.class);
assertThat(aggregator.getAggregateStatus(Status.UP, Status.DOWN)).isEqualTo(Status.UNKNOWN);
});
}
@Test
void createsStatusMapperFromProperties() {
this.contextRunner.withPropertyValues("spring.grpc.server.health.status.mapping.up=not-serving")
.run((context) -> {
StatusMapper mapper = context.getBean(StatusMapper.class);
assertThat(mapper.getServingStatus(Status.UP)).isEqualTo(ServingStatus.NOT_SERVING);
});
}
@Test
void whenHasHttpCodeStatusMapperBeanIgnoresProperties() {
this.contextRunner.withUserConfiguration(StatusMapperConfiguration.class)
.withPropertyValues("spring.grpc.server.health.status.mapping.up=not-serving")
.run((context) -> {
StatusMapper mapper = context.getBean(StatusMapper.class);
assertThat(mapper.getServingStatus(Status.UP)).isEqualTo(ServingStatus.UNRECOGNIZED);
});
}
@Test
void createsHealthCheckedGrpcComponents() {
this.contextRunner.withPropertyValues("spring.grpc.server.health.service.test.include=*").run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
assertThat(components).isInstanceOf(AutoConfiguredHealthCheckedGrpcComponents.class);
assertThat(components.getServiceNames()).containsOnly("test");
});
}
@Test
void whenComponentsIncludesContributorThatExistsDoesNotFail() {
this.contextRunner.withUserConfiguration(CompositeHealthIndicatorConfiguration.class)
.withPropertyValues("spring.grpc.server.health.service.test.include=composite/b/c")
.run((context) -> assertThat(context).hasNotFailed());
}
@Test
void whenHealthCheckedGrpcComponentsIncludesReactiveContributorThatExists() {
this.contextRunner.withUserConfiguration(CompositeReactiveHealthIndicatorConfiguration.class)
.withPropertyValues("spring.grpc.server.health.service.test.include=composite/b/c")
.run((context) -> assertThat(context).hasNotFailed());
}
@Test
void whenHealthCheckedGrpcComponentsIncludesContributorThatDoesNotExistThrowsException() {
this.contextRunner.withUserConfiguration(CompositeHealthIndicatorConfiguration.class)
.withPropertyValues("spring.grpc.server.health.service.test.include=composite/b/c,nope")
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure()).isInstanceOf(FailureAnalyzedException.class)
.hasMessage("Health contributor 'nope' defined in "
+ "'spring.grpc.server.health.service.test.include' does not exist");
});
}
@Test
void whenHealthCheckedGrpcComponentsExcludesContributorThatDoesNotExistThrowsException() {
this.contextRunner
.withPropertyValues("spring.grpc.server.health.service.test.exclude=composite/b/d",
"spring.grpc.server.health.service.test.include=*")
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure()).isInstanceOf(FailureAnalyzedException.class)
.hasMessage("Health contributor 'composite/b/d' defined in "
+ "'spring.grpc.server.health.service.test.exclude' does not exist");
});
}
@Test
void whenHealthCheckedGrpcComponentsIncludesContributorThatDoesNotExistAndValidationDisabledCreatesComponents() {
this.contextRunner
.withPropertyValues("spring.grpc.server.health.services.validate-membership=false",
"spring.grpc.server.health.service.test.include=nope")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
assertThat(components).isInstanceOf(AutoConfiguredHealthCheckedGrpcComponents.class);
assertThat(components.getServiceNames()).containsOnly("test");
});
}
@Test
void whenHasHealthCheckedGrpcComponentsBeanDoesNotCreateAdditional() {
this.contextRunner.withUserConfiguration(HealthCheckedGrpcComponentsConfiguration.class)
.withPropertyValues("spring.grpc.server.health.service.test.include=*")
.run((context) -> {
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
assertThat(components.getServiceNames()).containsOnly("mock");
});
}
@Test
void createsGrpcServerHealth() {
this.contextRunner.run((context) -> {
GrpcServerHealth serverHealth = context.getBean(GrpcServerHealth.class);
Map<String, ServingStatus> result = new LinkedHashMap<>();
serverHealth.update(result::put);
assertThat(result).containsExactly(entry("", ServingStatus.SERVING));
});
}
@Test
void whenHasGrpcServerHealthBeanDoesNotCreateAdditional() {
this.contextRunner.withUserConfiguration(GrpcServerHealthConfiguration.class).run((context) -> {
GrpcServerHealth serverHealth = context.getBean(GrpcServerHealth.class);
Map<String, ServingStatus> result = new LinkedHashMap<>();
serverHealth.update(result::put);
assertThat(result).isEmpty();
});
}
@Test
void runWithIndicatorsInParentContextFindsIndicators() {
new ApplicationContextRunner().withUserConfiguration(DownHealthIndicatorConfiguration.class)
.run((parent) -> new ApplicationContextRunner().withConfiguration(autoConfigurations)
.withUserConfiguration(HealthIndicatorsConfiguration.class)
.with(this::serviceBean)
.withParent(parent)
.run((context) -> {
GrpcServerHealth serverHealth = context.getBean(GrpcServerHealth.class);
Map<String, ServingStatus> result = new LinkedHashMap<>();
serverHealth.update(result::put);
assertThat(result).containsExactly(entry("", ServingStatus.NOT_SERVING));
}));
}
private void assertTerminalState(HealthStatusManager healthStatusManager, boolean expected) {
assertThat(healthStatusManager).extracting("healthService.terminal").isEqualTo(expected);
}
private void assertConfigured(AssertableApplicationContext context) {
assertThat(context).hasSingleBean(HealthStatusManager.class);
assertThat(context).hasBean("grpcServerHealthService");
assertThat(context).hasSingleBean(StatusAggregator.class);
assertThat(context).hasSingleBean(StatusMapper.class);
assertThat(context).hasSingleBean(HealthCheckedGrpcComponents.class);
assertThat(context).hasSingleBean(GrpcServerHealth.class);
assertThat(context).hasSingleBean(HealthContributorMembershipValidator.class);
assertThat(context).hasSingleBean(GrpcServerHealthScheduler.class);
}
private void assertNotConfigured(AssertableApplicationContext context) {
assertThat(context).doesNotHaveBean(HealthStatusManager.class);
assertThat(context).doesNotHaveBean("grpcServerHealthService");
assertThat(context).doesNotHaveBean(GrpcServerHealthAutoConfiguration.class);
}
private <R extends AbstractApplicationContextRunner<R, C, A>, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider<C>> R serviceBean(
R contextRunner) {
return contextRunner.withBean(BindableService.class, () -> this.service);
}
@Configuration(proxyBeanMethods = false)
static class HealthIndicatorsConfiguration {
@Bean
HealthIndicator simpleHealthIndicator() {
return () -> Health.up().withDetail("counter", 42).build();
}
@Bean
HealthIndicator additionalHealthIndicator() {
return () -> Health.up().build();
}
@Bean
ReactiveHealthIndicator reactiveHealthIndicator() {
return () -> Mono.just(Health.up().build());
}
}
@Configuration(proxyBeanMethods = false)
static class DownHealthIndicatorConfiguration {
@Bean
HealthIndicator downHealthIndicator() {
return () -> Health.down().build();
}
}
@Configuration(proxyBeanMethods = false)
static class CompositeHealthIndicatorConfiguration {
@Bean
CompositeHealthContributor compositeHealthIndicator() {
return CompositeHealthContributor.fromMap(Map.of("a", createHealthIndicator(), "b",
CompositeHealthContributor.fromMap(Map.of("c", createHealthIndicator()))));
}
private HealthIndicator createHealthIndicator() {
return () -> Health.up().build();
}
}
@Configuration(proxyBeanMethods = false)
static class CompositeReactiveHealthIndicatorConfiguration {
@Bean
CompositeReactiveHealthContributor compositeHealthIndicator() {
return CompositeReactiveHealthContributor.fromMap(Map.of("a", createHealthIndicator(), "b",
CompositeReactiveHealthContributor.fromMap(Map.of("c", createHealthIndicator()))));
}
private ReactiveHealthIndicator createHealthIndicator() {
return () -> Mono.just(Health.up().build());
}
}
@Configuration(proxyBeanMethods = false)
static class StatusAggregatorConfiguration {
@Bean
StatusAggregator statusAggregator() {
return (statuses) -> Status.UNKNOWN;
}
}
@Configuration(proxyBeanMethods = false)
static class StatusMapperConfiguration {
@Bean
StatusMapper httpCodeStatusMapper() {
return (status) -> ServingStatus.UNRECOGNIZED;
}
}
@Configuration(proxyBeanMethods = false)
static class HealthCheckedGrpcComponentsConfiguration {
@Bean
HealthCheckedGrpcComponents healthCheckedGrpcComponents() {
HealthCheckedGrpcComponents components = mock();
given(components.getServiceNames()).willReturn(Collections.singleton("mock"));
return components;
}
}
@Configuration(proxyBeanMethods = false)
static class GrpcServerHealthConfiguration {
@Bean
GrpcServerHealth grpcServerHealth() {
return mock();
}
}
}
@@ -0,0 +1,79 @@
/*
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import io.grpc.protobuf.services.HealthStatusManager;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent;
import org.springframework.scheduling.TaskScheduler;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
/**
* Tests for {@link GrpcServerHealthScheduler}.
*
* @author Phillip Webb
*/
class GrpcServerHealthSchedulerTests {
@Test
void onApplicationEventWhenEventIsGrpcStartStartsHealth() {
Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
GrpcServerHealth serverHealth = mock();
HealthStatusManager statusManager = mock();
TaskScheduler taskScheduler = mock();
Duration period = Duration.ofSeconds(10);
Duration delay = Duration.ofSeconds(30);
GrpcServerHealthScheduler healthScheduler = new GrpcServerHealthScheduler(clock, serverHealth, statusManager,
taskScheduler, period, delay);
then(serverHealth).should(never()).update(statusManager);
healthScheduler.onApplicationEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 123));
Instant startTime = Instant.now(clock).plus(delay);
ArgumentCaptor<Runnable> runnable = ArgumentCaptor.captor();
then(taskScheduler).should().scheduleAtFixedRate(runnable.capture(), eq(startTime), eq(period));
then(serverHealth).should(never()).update(statusManager);
runnable.getValue().run();
then(serverHealth).should().update(statusManager);
}
@Test
void onApplicationEventWhenEventIsGrpcStartAndCalledTwiceStartsHealthOnlyOnce() {
GrpcServerHealth serverHealth = mock();
HealthStatusManager statusManager = mock();
TaskScheduler taskScheduler = mock();
GrpcServerHealthScheduler healthScheduler = new GrpcServerHealthScheduler(serverHealth, statusManager,
taskScheduler, Duration.ofSeconds(10), Duration.ofSeconds(30));
then(serverHealth).should(never()).update(statusManager);
healthScheduler.onApplicationEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 123));
healthScheduler.onApplicationEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 345));
then(taskScheduler).should(times(1)).scheduleAtFixedRate(any(), any(), any());
}
}
@@ -0,0 +1,293 @@
/*
* Copyright 2012-present 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.grpc.server.health;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.health.contributor.CompositeHealthContributor;
import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.HealthContributor;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.boot.health.contributor.ReactiveHealthIndicator;
import org.springframework.boot.health.contributor.Status;
import org.springframework.boot.health.registry.DefaultHealthContributorRegistry;
import org.springframework.boot.health.registry.DefaultReactiveHealthContributorRegistry;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GrpcServerHealth}.
*
* @author Phillip Webb
*/
class GrpcServerHealthTests {
private static final HealthIndicator UP = () -> Health.up().build();
private static final HealthIndicator DOWN = () -> Health.down().build();
private static final ReactiveHealthIndicator REACTIVE_DOWN = () -> Mono.just(Health.down().build());
@Test
@SuppressWarnings("NullAway") // Test null check
void createWhenRegistryIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new GrpcServerHealth(null, null, mock()))
.withMessage("'registry' must not be null");
}
@Test
@SuppressWarnings("NullAway") // Test null check
void createWhenComponentsIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new GrpcServerHealth(mock(), null, null))
.withMessage("'components' must not be null");
}
@Test
void updateWhenHasServerComponentAndUp() {
assertThat(updateWithServerComponent("test", UP)).containsExactly(entry("", ServingStatus.SERVING));
}
@Test
void updateWhenHasServerComponentAndDown() {
assertThat(updateWithServerComponent("test", DOWN)).containsExactly(entry("", ServingStatus.NOT_SERVING));
}
@Test
void updateWhenHasServerComponentAndMissing() {
assertThat(updateWithServerComponent("other", UP)).containsExactly(entry("", ServingStatus.UNKNOWN));
}
private Map<String, ServingStatus> updateWithServerComponent(String indicatorName, HealthIndicator indicator) {
HealthCheckedGrpcComponent server = new TestHealthCheckedGrpcComponent(Set.of("test"));
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(server, Collections.emptyMap());
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
registry.registerContributor(indicatorName, indicator);
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
Map<String, ServingStatus> result = new LinkedHashMap<>();
health.update(result::put);
return result;
}
@Test
void updateWhenHasServices() {
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
services.put("one", new TestHealthCheckedGrpcComponent(Set.of("up")));
services.put("two", new TestHealthCheckedGrpcComponent(Set.of("down")));
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
registry.registerContributor("up", UP);
registry.registerContributor("down", DOWN);
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
Map<String, ServingStatus> result = new LinkedHashMap<>();
health.update(result::put);
assertThat(result).containsExactly(entry("one", ServingStatus.SERVING),
entry("two", ServingStatus.NOT_SERVING));
}
@Test
void updateUsesCache() {
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
services.put("one", new TestHealthCheckedGrpcComponent(Set.of("test")));
services.put("two", new TestHealthCheckedGrpcComponent(Set.of("test")));
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
HealthIndicator contributor = mock();
given(contributor.health(false)).willReturn(Health.up().build(), Health.down().build());
registry.registerContributor("test", contributor);
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
Map<String, ServingStatus> result = new LinkedHashMap<>();
health.update(result::put);
assertThat(result).containsExactly(entry("one", ServingStatus.SERVING), entry("two", ServingStatus.SERVING));
}
@Test
void updateWhenHasEmptyNamedServicesDoesNotIncludeIt() {
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
services.put("", new TestHealthCheckedGrpcComponent(Set.of("up")));
services.put("one", new TestHealthCheckedGrpcComponent(Set.of("up")));
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
registry.registerContributor("up", UP);
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
Map<String, ServingStatus> result = new LinkedHashMap<>();
health.update(result::put);
assertThat(result).containsExactly(entry("one", ServingStatus.SERVING));
}
@Test
void updateWhenHasFallbackRegistry() {
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
services.put("one", new TestHealthCheckedGrpcComponent(Set.of("1")));
services.put("two", new TestHealthCheckedGrpcComponent(Set.of("2")));
services.put("three", new TestHealthCheckedGrpcComponent(Set.of("3")));
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
DefaultReactiveHealthContributorRegistry fallbackRegistry = new DefaultReactiveHealthContributorRegistry();
registry.registerContributor("1", UP);
registry.registerContributor("2", UP);
fallbackRegistry.registerContributor("2", REACTIVE_DOWN);
fallbackRegistry.registerContributor("3", REACTIVE_DOWN);
GrpcServerHealth health = new GrpcServerHealth(registry, fallbackRegistry, components);
Map<String, ServingStatus> result = new LinkedHashMap<>();
health.update(result::put);
assertThat(result).containsExactly(entry("one", ServingStatus.SERVING), entry("two", ServingStatus.SERVING),
entry("three", ServingStatus.NOT_SERVING));
}
@Test
void updateWhenHasCompositeContributor() {
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
services.put("one", new TestHealthCheckedGrpcComponent(Set.of("dbs", "dbs/db1", "dbs/db2")));
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
HealthContributor contributor = CompositeHealthContributor.fromMap(Map.of("db1", UP, "db2", DOWN));
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
registry.registerContributor("dbs", contributor);
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
Map<String, ServingStatus> result = new LinkedHashMap<>();
health.update(result::put);
assertThat(result).containsExactly(entry("one", ServingStatus.NOT_SERVING));
}
@Test
void updateFiltersIndicatorsAndCompositeParentByName() {
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
services.put("one", new TestHealthCheckedGrpcComponent(Set.of("dbs", "dbs/db1")));
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
HealthContributor contributor = CompositeHealthContributor.fromMap(Map.of("db1", UP, "db2", DOWN));
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
registry.registerContributor("dbs", contributor);
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
Map<String, ServingStatus> result = new LinkedHashMap<>();
health.update(result::put);
assertThat(result).containsExactly(entry("one", ServingStatus.SERVING));
}
@Test
@SuppressWarnings("unchecked")
void updateCallsStatusAggregator() {
StatusAggregator statusAggregator = mock();
given(statusAggregator.getAggregateStatus((Set<Status>) any(Set.class))).willReturn(Status.DOWN);
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
services.put("one",
new TestHealthCheckedGrpcComponent(Set.of("up"), statusAggregator, StatusMapper.getDefault()));
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
registry.registerContributor("up", UP);
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
Map<String, ServingStatus> result = new LinkedHashMap<>();
health.update(result::put);
assertThat(result).containsExactly(entry("one", ServingStatus.NOT_SERVING));
then(statusAggregator).should().getAggregateStatus(Set.of(Status.UP));
}
@Test
void updateCallsStatusMapper() {
StatusMapper statusMapper = mock();
given(statusMapper.getServingStatus(Status.UP)).willReturn(ServingStatus.UNRECOGNIZED);
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
services.put("one",
new TestHealthCheckedGrpcComponent(Set.of("up"), StatusAggregator.getDefault(), statusMapper));
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
registry.registerContributor("up", UP);
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
Map<String, ServingStatus> result = new LinkedHashMap<>();
health.update(result::put);
assertThat(result).containsExactly(entry("one", ServingStatus.UNRECOGNIZED));
then(statusMapper).should().getServingStatus(Status.UP);
}
static class TestHealthCheckedGrpcComponents implements HealthCheckedGrpcComponents {
private final @Nullable HealthCheckedGrpcComponent server;
private final Map<String, HealthCheckedGrpcComponent> services;
TestHealthCheckedGrpcComponents(@Nullable HealthCheckedGrpcComponent server,
Map<String, HealthCheckedGrpcComponent> services) {
this.server = server;
this.services = services;
}
@Override
public @Nullable HealthCheckedGrpcComponent getServer() {
return this.server;
}
@Override
public Set<String> getServiceNames() {
return this.services.keySet();
}
@Override
public @Nullable HealthCheckedGrpcComponent getService(String serviceName) {
return this.services.get(serviceName);
}
}
private static class TestHealthCheckedGrpcComponent implements HealthCheckedGrpcComponent {
private final Set<String> members;
private final StatusAggregator statusAggregator;
private final StatusMapper statusMapper;
TestHealthCheckedGrpcComponent(Set<String> memebers) {
this(memebers, StatusAggregator.getDefault(), StatusMapper.getDefault());
}
TestHealthCheckedGrpcComponent(Set<String> memebers, StatusAggregator statusAggregator,
StatusMapper statusMapper) {
this.members = memebers;
this.statusAggregator = statusAggregator;
this.statusMapper = statusMapper;
}
@Override
public boolean isMember(String healthContributorName) {
return this.members.contains(healthContributorName);
}
@Override
public StatusAggregator getStatusAggregator() {
return this.statusAggregator;
}
@Override
public StatusMapper getStatusMapper() {
return this.statusMapper;
}
}
}
@@ -0,0 +1,71 @@
/*
* Copyright 2012-present 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.grpc.server.health;
import org.junit.jupiter.api.Test;
import org.springframework.boot.health.actuate.endpoint.StatusAggregator;
import org.springframework.boot.health.contributor.Status;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link StatusAggregator}.
*
* @author Phillip Webb
*/
class StatusAggregatorTests {
@Test
void ofAndGetAggregateStatusWhenUsingDefaultInstance() {
StatusAggregator aggregator = StatusAggregator.getDefault();
Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE);
assertThat(status).isEqualTo(Status.DOWN);
}
@Test
void ofAndGetAggregateStatusWhenUsingCustomOrder() {
StatusAggregator aggregator = StatusAggregator.of(Status.UNKNOWN, Status.UP, Status.OUT_OF_SERVICE,
Status.DOWN);
Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE);
assertThat(status).isEqualTo(Status.UNKNOWN);
}
@Test
void ofAndGetAggregateStatusWhenHasCustomStatusAndUsingDefaultOrder() {
StatusAggregator aggregator = StatusAggregator.getDefault();
Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE,
new Status("CUSTOM"));
assertThat(status).isEqualTo(Status.DOWN);
}
@Test
void ofAndGetAggregateStatusWhenHasCustomStatusAndUsingCustomOrder() {
StatusAggregator aggregator = StatusAggregator.of("DOWN", "OUT_OF_SERVICE", "UP", "UNKNOWN", "CUSTOM");
Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE,
new Status("CUSTOM"));
assertThat(status).isEqualTo(Status.DOWN);
}
@Test
void ofWithNonUniformCodes() {
StatusAggregator aggregator = StatusAggregator.of("out-of-service", "up");
Status status = aggregator.getAggregateStatus(Status.UP, Status.OUT_OF_SERVICE);
assertThat(status).isEqualTo(Status.OUT_OF_SERVICE);
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2012-present 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.grpc.server.health;
import java.util.LinkedHashMap;
import java.util.Map;
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
import org.junit.jupiter.api.Test;
import org.springframework.boot.health.contributor.Status;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link StatusMapper}.
*
* @author Phillip Webb
*/
class StatusMapperTests {
@Test
void createWhenMappingsAreNullUsesDefaultMappings() {
StatusMapper mapper = StatusMapper.of(null);
assertThat(mapper.getServingStatus(Status.UNKNOWN)).isEqualTo(ServingStatus.UNKNOWN);
assertThat(mapper.getServingStatus(Status.UP)).isEqualTo(ServingStatus.SERVING);
assertThat(mapper.getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.NOT_SERVING);
assertThat(mapper.getServingStatus(Status.OUT_OF_SERVICE)).isEqualTo(ServingStatus.NOT_SERVING);
}
@Test
void getStatusCodeReturnsMappedStatus() {
Map<String, ServingStatus> map = new LinkedHashMap<>();
map.put("up", ServingStatus.UNRECOGNIZED);
map.put("down", ServingStatus.UNKNOWN);
StatusMapper mapper = StatusMapper.of(map);
assertThat(mapper.getServingStatus(Status.UP)).isEqualTo(ServingStatus.UNRECOGNIZED);
assertThat(mapper.getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.UNKNOWN);
assertThat(mapper.getServingStatus(Status.OUT_OF_SERVICE)).isEqualTo(ServingStatus.SERVING);
}
@Test
void getStatusCodeWhenMappingsAreNotUniformReturnsMappedStatus() {
Map<String, ServingStatus> map = new LinkedHashMap<>();
map.put("out-of-service", ServingStatus.SERVING);
StatusMapper mapper = StatusMapper.of(map);
assertThat(mapper.getServingStatus(Status.OUT_OF_SERVICE)).isEqualTo(ServingStatus.SERVING);
}
}