Support GrpcServerHealthScheduler in servlet environments

Refine `GrpcServerHealthScheduler` so that it will start when gRPC
is running behind a servlet.

Closes gh-50209
This commit is contained in:
Phillip Webb
2026-04-30 21:25:57 -07:00
parent ebce054028
commit c5cd580073
3 changed files with 129 additions and 29 deletions
@@ -19,18 +19,22 @@ 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 jakarta.servlet.ServletContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.grpc.server.GrpcServletRegistration;
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.log.LogMessage;
import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.function.SingletonSupplier;
import org.springframework.web.context.WebApplicationContext;
/**
* Schedules gRPC health updates once the gRPC server has been started.
@@ -38,32 +42,78 @@ import org.springframework.util.function.SingletonSupplier;
* @author Phillip Webb
* @author Chris Bono
*/
class GrpcServerHealthScheduler implements ApplicationListener<GrpcServerStartedEvent> {
class GrpcServerHealthScheduler {
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,
GrpcServerHealthScheduler(ConfigurableApplicationContext applicationContext, GrpcServerHealth grpcServerHealth,
HealthStatusManager grpcServerHealthStatusManager, TaskScheduler taskScheduler, Duration period,
Duration delay) {
this.scheduleHealth = SingletonSupplier.of(() -> {
this(applicationContext, grpcServerHealth, grpcServerHealthStatusManager, taskScheduler, period, delay,
Clock.systemDefaultZone());
}
GrpcServerHealthScheduler(ConfigurableApplicationContext applicationContext, GrpcServerHealth grpcServerHealth,
HealthStatusManager grpcServerHealthStatusManager, TaskScheduler taskScheduler, Duration period,
Duration delay, Clock clock) {
SingletonSupplier<?> 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);
});
if (ClassUtils.isPresent("jakarta.servlet.Servlet", null)
&& ClassUtils.isPresent("org.springframework.web.context.WebApplicationContext", null)) {
ServletTrigger.apply(applicationContext, scheduleHealth);
}
GrpcServerTrigger.apply(applicationContext, scheduleHealth);
}
@Override
public void onApplicationEvent(GrpcServerStartedEvent event) {
this.scheduleHealth.get();
/**
* Trigger for servlet environments.
*/
static class ServletTrigger {
static void apply(ConfigurableApplicationContext applicationContext, SingletonSupplier<?> scheduleHealth) {
if (hasGrpcServletRegistration(applicationContext)) {
scheduleHealth.get();
return;
}
}
private static boolean hasGrpcServletRegistration(ConfigurableApplicationContext applicationContext) {
if (!ObjectUtils.isEmpty(applicationContext.getBeanNamesForType(GrpcServletRegistration.class))) {
return true;
}
if (applicationContext instanceof WebApplicationContext webApplicationContext) {
ServletContext servletContext = webApplicationContext.getServletContext();
if (servletContext != null && servletContext.getServletRegistrations()
.values()
.stream()
.anyMatch((servletRegistration) -> "io.grpc.servlet.jakarta.GrpcServlet"
.equals(servletRegistration.getClassName()))) {
return true;
}
}
return false;
}
}
/**
* Trigger for regular gRPC servers.
*/
static class GrpcServerTrigger {
static void apply(ConfigurableApplicationContext applicationContext, SingletonSupplier<?> scheduleHealth) {
applicationContext.addApplicationListener((event) -> {
if (event instanceof GrpcServerStartedEvent) {
scheduleHealth.get();
}
});
}
}
}
@@ -27,6 +27,7 @@ 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.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.grpc.server.GrpcServerFactory;
@@ -47,12 +48,12 @@ public final class GrpcServerHealthSchedulerAutoConfiguration {
@Bean
@ConditionalOnBean({ TaskScheduler.class, GrpcServerHealth.class })
GrpcServerHealthScheduler grpcServerHealthScheduler(GrpcServerHealth grpcServerHealth,
HealthStatusManager grpcServerHealthStatusManager, TaskScheduler taskScheduler,
GrpcServerHealthProperties properties) {
GrpcServerHealthScheduler grpcServerHealthScheduler(ConfigurableApplicationContext applicationContext,
GrpcServerHealth grpcServerHealth, HealthStatusManager grpcServerHealthStatusManager,
TaskScheduler taskScheduler, GrpcServerHealthProperties properties) {
Schedule schedule = properties.getSchedule();
return new GrpcServerHealthScheduler(grpcServerHealth, grpcServerHealthStatusManager, taskScheduler,
schedule.getPeriod(), schedule.getDelay());
return new GrpcServerHealthScheduler(applicationContext, grpcServerHealth, grpcServerHealthStatusManager,
taskScheduler, schedule.getPeriod(), schedule.getDelay());
}
}
@@ -20,17 +20,26 @@ import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Map;
import io.grpc.protobuf.services.HealthStatusManager;
import io.grpc.servlet.jakarta.GrpcServlet;
import jakarta.servlet.ServletRegistration;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.grpc.server.GrpcServletRegistration;
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent;
import org.springframework.mock.web.MockServletContext;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.web.context.support.GenericWebApplicationContext;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -44,17 +53,18 @@ import static org.mockito.Mockito.times;
class GrpcServerHealthSchedulerTests {
@Test
void onApplicationEventWhenEventIsGrpcStartStartsHealth() {
void onGrpcServerStartedEventWhenEventIsGrpcStartStartsHealth() {
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);
ConfigurableApplicationContext context = new GenericApplicationContext();
context.refresh();
new GrpcServerHealthScheduler(context, serverHealth, statusManager, taskScheduler, period, delay, clock);
then(serverHealth).should(never()).update(statusManager);
healthScheduler.onApplicationEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 123));
context.publishEvent(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));
@@ -64,16 +74,55 @@ class GrpcServerHealthSchedulerTests {
}
@Test
void onApplicationEventWhenEventIsGrpcStartAndCalledTwiceStartsHealthOnlyOnce() {
void onGrpcServerStartedEventWhenEventIsGrpcStartAndCalledTwiceStartsHealthOnlyOnce() {
GrpcServerHealth serverHealth = mock();
HealthStatusManager statusManager = mock();
TaskScheduler taskScheduler = mock();
GrpcServerHealthScheduler healthScheduler = new GrpcServerHealthScheduler(serverHealth, statusManager,
taskScheduler, Duration.ofSeconds(10), Duration.ofSeconds(30));
ConfigurableApplicationContext context = new GenericApplicationContext();
context.refresh();
new GrpcServerHealthScheduler(context, 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));
context.publishEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 123));
context.publishEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 345));
then(taskScheduler).should(times(1)).scheduleAtFixedRate(any(), any(), any());
}
@Test
void webApplicationWithGrpcServletRegistrationBeanStartsHealth() {
GrpcServerHealth serverHealth = mock();
HealthStatusManager statusManager = mock();
TaskScheduler taskScheduler = mock();
GenericWebApplicationContext context = new GenericWebApplicationContext();
context.registerBean("registartion", GrpcServletRegistration.class, () -> mock());
context.refresh();
new GrpcServerHealthScheduler(context, serverHealth, statusManager, taskScheduler, Duration.ofSeconds(10),
Duration.ofSeconds(30));
then(taskScheduler).should(times(1)).scheduleAtFixedRate(any(), any(), any());
}
@Test
void webApplicationWithGrpcServletRegistrationStartsHealth() {
GrpcServerHealth serverHealth = mock();
HealthStatusManager statusManager = mock();
TaskScheduler taskScheduler = mock();
MockServletContext servletContext = new MockServletContext() {
@Override
public java.util.Map<String, ServletRegistration> getServletRegistrations() {
ServletRegistration registration = mock();
given(registration.getClassName()).willReturn(GrpcServlet.class.getName());
return Map.of("grpc", registration);
}
};
GenericWebApplicationContext context = new GenericWebApplicationContext();
context.setServletContext(servletContext);
context.refresh();
new GrpcServerHealthScheduler(context, serverHealth, statusManager, taskScheduler, Duration.ofSeconds(10),
Duration.ofSeconds(30));
then(taskScheduler).should(times(1)).scheduleAtFixedRate(any(), any(), any());
}
}