mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
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:
+65
-15
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-5
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+58
-9
@@ -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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user