diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java index e2b36188288..8b0c34dea40 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthScheduler.java @@ -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 { +class GrpcServerHealthScheduler { private static final Log logger = LogFactory.getLog(GrpcServerHealthScheduler.class); - private final SingletonSupplier> 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(); + } + }); + } + } } diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerAutoConfiguration.java index 972d4a79d3f..8666226b0fd 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerAutoConfiguration.java @@ -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()); } } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerTests.java index c054fc87663..a9d7b607e39 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthSchedulerTests.java @@ -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 = 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 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()); + + } + }