From b004ba6559590b88e207b303643c130c75d54b53 Mon Sep 17 00:00:00 2001 From: Sergei Batsura Date: Thu, 13 Mar 2025 13:42:33 +0300 Subject: [PATCH] Added defaultDeadline client property Signed-off-by: Sergei Batsura [resolves #136] --- .../sample/DefaultDeadlineSetupTests.java | 124 ++++++++++++++++++ .../sample/GrpcServerApplicationTests.java | 2 +- .../sample/GrpcServerApplicationTests.java | 2 +- .../sample/GrpcServerApplicationTests.java | 2 +- ...DefaultDeadlineSetupClientInterceptor.java | 56 ++++++++ .../BasicAuthenticationInterceptor.java | 2 +- .../BearerTokenAuthenticationInterceptor.java | 2 +- ...entPropertiesChannelBuilderCustomizer.java | 8 +- .../client/GrpcClientProperties.java | 14 ++ .../client/GrpcClientPropertiesTests.java | 1 + 10 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 samples/grpc-client/src/test/java/org/springframework/grpc/sample/DefaultDeadlineSetupTests.java create mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/client/interceptor/DefaultDeadlineSetupClientInterceptor.java rename spring-grpc-core/src/main/java/org/springframework/grpc/client/{ => interceptor}/security/BasicAuthenticationInterceptor.java (97%) rename spring-grpc-core/src/main/java/org/springframework/grpc/client/{ => interceptor}/security/BearerTokenAuthenticationInterceptor.java (96%) diff --git a/samples/grpc-client/src/test/java/org/springframework/grpc/sample/DefaultDeadlineSetupTests.java b/samples/grpc-client/src/test/java/org/springframework/grpc/sample/DefaultDeadlineSetupTests.java new file mode 100644 index 0000000..f89b607 --- /dev/null +++ b/samples/grpc-client/src/test/java/org/springframework/grpc/sample/DefaultDeadlineSetupTests.java @@ -0,0 +1,124 @@ +package org.springframework.grpc.sample; + +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import java.io.File; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.experimental.boot.server.exec.CommonsExecWebServerFactoryBean; +import org.springframework.experimental.boot.server.exec.MavenClasspathEntry; +import org.springframework.experimental.boot.test.context.DynamicProperty; +import org.springframework.experimental.boot.test.context.EnableDynamicProperty; +import org.springframework.grpc.client.EnableGrpcClients; +import org.springframework.grpc.client.GrpcClient; +import org.springframework.grpc.sample.proto.HelloRequest; +import org.springframework.grpc.sample.proto.SimpleGrpc; +import org.springframework.test.annotation.DirtiesContext; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class DefaultDeadlineSetupTests { + + @Nested + @SpringBootTest(properties = { "spring.grpc.client.default-channel.address=static://0.0.0.0:${local.grpc.port}", + "spring.grpc.client.default-channel.default-deadline=1s", + "spring.main.allow-bean-definition-overriding=true" }) + @DirtiesContext + @EnabledIf("serverJarAvailable") + class Deadline { + + static boolean serverJarAvailable() { + return new File("../grpc-server/target/grpc-server-sample-0.6.0-SNAPSHOT.jar").exists(); + } + + @Test + void contextLoads() { + // Real test case in ExtraConfiguration#runner(SimpleGrpc.SimpleBlockingStub)} + } + + @EnableGrpcClients(@GrpcClient(types = SimpleGrpc.SimpleBlockingStub.class)) + @TestConfiguration + @EnableDynamicProperty + static class ExtraConfiguration { + + @Bean + @DynamicProperty(name = "local.grpc.port", value = "port") + static CommonsExecWebServerFactoryBean grpcServer() { + return CommonsExecWebServerFactoryBean.builder() + .classpath(classpath -> classpath + .entries(new MavenClasspathEntry("org.springframework.grpc:grpc-server-sample:0.6.0-SNAPSHOT")) + .files("target/test-classes")); + } + + @Bean + @Primary + public CommandLineRunner runner(SimpleGrpc.SimpleBlockingStub stub) { + return args -> { + var rs = stub.streamHello(HelloRequest.newBuilder().setName("Deadline").build()); + Assertions.assertNotNull(rs); + StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> { + while (rs.hasNext()) { + System.out.println(rs.next()); + } + }); + assertEquals(Status.Code.DEADLINE_EXCEEDED, exception.getStatus().getCode()); + }; + } + + } + + } + + @Nested + @SpringBootTest(properties = { "spring.grpc.client.default-channel.address=static://0.0.0.0:${local.grpc.port}", + "spring.grpc.client.default-channel.default-deadline=1s", + "spring.main.allow-bean-definition-overriding=true" }) + @DirtiesContext + @EnabledIf("serverJarAvailable") + class WithoutDeadline { + + static boolean serverJarAvailable() { + return new File("../grpc-server/target/grpc-server-sample-0.6.0-SNAPSHOT.jar").exists(); + } + + @Test + void contextLoads() { + // Real test case in ExtraConfiguration#runner(SimpleGrpc.SimpleBlockingStub)} + } + + @EnableGrpcClients(@GrpcClient(types = SimpleGrpc.SimpleBlockingStub.class)) + @TestConfiguration + @EnableDynamicProperty + static class ExtraConfiguration { + + @Bean + @DynamicProperty(name = "local.grpc.port", value = "port") + static CommonsExecWebServerFactoryBean grpcServer() { + return CommonsExecWebServerFactoryBean.builder() + .classpath(classpath -> classpath + .entries(new MavenClasspathEntry("org.springframework.grpc:grpc-server-sample:0.6.0-SNAPSHOT")) + .files("target/test-classes")); + } + + @Bean + @Primary + public CommandLineRunner runner(SimpleGrpc.SimpleBlockingStub stub) { + return args -> { + var rs = stub.sayHello(HelloRequest.newBuilder().setName("WithoutDeadline").build()); + Assertions.assertNotNull(rs); + }; + } + + } + + } + +} \ No newline at end of file diff --git a/samples/grpc-oauth2/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-oauth2/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java index 648d94e..57c5676 100644 --- a/samples/grpc-oauth2/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java +++ b/samples/grpc-oauth2/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -24,7 +24,7 @@ import org.springframework.grpc.client.ChannelBuilderOptions; import org.springframework.grpc.client.EnableGrpcClients; import org.springframework.grpc.client.GrpcClient; import org.springframework.grpc.client.GrpcClientRegistryCustomizer; -import org.springframework.grpc.client.security.BearerTokenAuthenticationInterceptor; +import org.springframework.grpc.client.interceptor.security.BearerTokenAuthenticationInterceptor; import org.springframework.grpc.sample.proto.HelloReply; import org.springframework.grpc.sample.proto.HelloRequest; import org.springframework.grpc.sample.proto.SimpleGrpc; diff --git a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java index fc06508..381cb45 100644 --- a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java +++ b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -20,7 +20,7 @@ import org.springframework.grpc.client.ChannelBuilderOptions; import org.springframework.grpc.client.EnableGrpcClients; import org.springframework.grpc.client.GrpcClient; import org.springframework.grpc.client.GrpcClientRegistryCustomizer; -import org.springframework.grpc.client.security.BasicAuthenticationInterceptor; +import org.springframework.grpc.client.interceptor.security.BasicAuthenticationInterceptor; import org.springframework.grpc.sample.proto.HelloReply; import org.springframework.grpc.sample.proto.HelloRequest; import org.springframework.grpc.sample.proto.SimpleGrpc; diff --git a/samples/grpc-tomcat-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-tomcat-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java index 8b5b3e9..dd68800 100644 --- a/samples/grpc-tomcat-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java +++ b/samples/grpc-tomcat-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -18,7 +18,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Lazy; import org.springframework.grpc.client.ChannelBuilderOptions; import org.springframework.grpc.client.GrpcChannelFactory; -import org.springframework.grpc.client.security.BasicAuthenticationInterceptor; +import org.springframework.grpc.client.interceptor.security.BasicAuthenticationInterceptor; import org.springframework.grpc.sample.proto.HelloReply; import org.springframework.grpc.sample.proto.HelloRequest; import org.springframework.grpc.sample.proto.SimpleGrpc; diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/interceptor/DefaultDeadlineSetupClientInterceptor.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/interceptor/DefaultDeadlineSetupClientInterceptor.java new file mode 100644 index 0000000..ab814a6 --- /dev/null +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/interceptor/DefaultDeadlineSetupClientInterceptor.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024-2025 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.grpc.client.interceptor; + +import static java.util.Objects.requireNonNull; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; + +/** + * A client interceptor configuring the default deadline for each call. + * + * @author Sergei Batsura (batsura.sa@gmail.com) + */ +public class DefaultDeadlineSetupClientInterceptor implements ClientInterceptor { + + private final Duration defaultDeadline; + + public DefaultDeadlineSetupClientInterceptor(Duration defaultDeadline) { + this.defaultDeadline = requireNonNull(defaultDeadline, "defaultDeadline"); + } + + @Override + public ClientCall interceptCall(final MethodDescriptor method, + final CallOptions callOptions, final Channel next) { + + if (callOptions.getDeadline() == null) { + return next.newCall(method, + callOptions.withDeadlineAfter(this.defaultDeadline.toMillis(), TimeUnit.MILLISECONDS)); + } + else { + return next.newCall(method, callOptions); + } + } + +} diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/security/BasicAuthenticationInterceptor.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/interceptor/security/BasicAuthenticationInterceptor.java similarity index 97% rename from spring-grpc-core/src/main/java/org/springframework/grpc/client/security/BasicAuthenticationInterceptor.java rename to spring-grpc-core/src/main/java/org/springframework/grpc/client/interceptor/security/BasicAuthenticationInterceptor.java index cc38486..e69929f 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/security/BasicAuthenticationInterceptor.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/interceptor/security/BasicAuthenticationInterceptor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.grpc.client.security; +package org.springframework.grpc.client.interceptor.security; import java.util.Base64; diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/security/BearerTokenAuthenticationInterceptor.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/interceptor/security/BearerTokenAuthenticationInterceptor.java similarity index 96% rename from spring-grpc-core/src/main/java/org/springframework/grpc/client/security/BearerTokenAuthenticationInterceptor.java rename to spring-grpc-core/src/main/java/org/springframework/grpc/client/interceptor/security/BearerTokenAuthenticationInterceptor.java index 144f7d2..c4e7010 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/security/BearerTokenAuthenticationInterceptor.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/interceptor/security/BearerTokenAuthenticationInterceptor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.grpc.client.security; +package org.springframework.grpc.client.interceptor.security; import java.util.function.Supplier; diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/ClientPropertiesChannelBuilderCustomizer.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/ClientPropertiesChannelBuilderCustomizer.java index 506bdd4..b0bba6b 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/ClientPropertiesChannelBuilderCustomizer.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/ClientPropertiesChannelBuilderCustomizer.java @@ -15,19 +15,18 @@ */ package org.springframework.grpc.autoconfigure.client; +import io.grpc.ManagedChannelBuilder; import java.time.Duration; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; - import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.grpc.autoconfigure.client.GrpcClientProperties.ChannelConfig; import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; +import org.springframework.grpc.client.interceptor.DefaultDeadlineSetupClientInterceptor; import org.springframework.util.unit.DataSize; -import io.grpc.ManagedChannelBuilder; - /** * A {@link GrpcChannelBuilderCustomizer} that maps {@link GrpcClientProperties client * properties} to a channel builder. @@ -65,6 +64,9 @@ class ClientPropertiesChannelBuilderCustomizer healthCheckConfig = Map.of("healthCheckConfig", Map.of("serviceName", serviceNameToCheck)); builder.defaultServiceConfig(healthCheckConfig); } + if (channel.getDefaultDeadline() != null && channel.getDefaultDeadline().toMillis() > 0L) { + builder.intercept(new DefaultDeadlineSetupClientInterceptor(channel.getDefaultDeadline())); + } } Consumer durationProperty(BiConsumer setter) { diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientProperties.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientProperties.java index 8770e81..88b10f5 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientProperties.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientProperties.java @@ -307,6 +307,19 @@ public class GrpcClientProperties implements EnvironmentAware, VirtualTargets { this.userAgent = userAgent; } + /** + * The default deadline for RPCs performed on this channel. + */ + private Duration defaultDeadline = null; + + public Duration getDefaultDeadline() { + return defaultDeadline; + } + + public void setDefaultDeadline(final Duration defaultDeadline) { + this.defaultDeadline = defaultDeadline; + } + /** * Provide a copy of the channel instance. * @return a copy of the channel instance. @@ -324,6 +337,7 @@ public class GrpcClientProperties implements EnvironmentAware, VirtualTargets { copy.maxInboundMessageSize = this.maxInboundMessageSize; copy.maxInboundMetadataSize = this.maxInboundMetadataSize; copy.userAgent = this.userAgent; + copy.defaultDeadline = this.defaultDeadline; copy.health.copyValuesFrom(this.getHealth()); copy.ssl.copyValuesFrom(this.getSsl()); return copy; diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientPropertiesTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientPropertiesTests.java index 1e01319..f81118e 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientPropertiesTests.java +++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientPropertiesTests.java @@ -208,6 +208,7 @@ class GrpcClientPropertiesTests { defaultChannel.setMaxInboundMessageSize(DataSize.ofMegabytes(100)); defaultChannel.setMaxInboundMetadataSize(DataSize.ofMegabytes(200)); defaultChannel.setUserAgent("me"); + defaultChannel.setDefaultDeadline(Duration.ofMinutes(1)); defaultChannel.getSsl().setEnabled(true); defaultChannel.getSsl().setBundle("custom-bundle"); var properties = newProperties(defaultChannel, Map.of());