diff --git a/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientDefaultRequestTimeoutAutoConfiguration.java b/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientDefaultRequestTimeoutAutoConfiguration.java new file mode 100644 index 000000000..68fd79d36 --- /dev/null +++ b/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientDefaultRequestTimeoutAutoConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016-2024 The gRPC-Spring 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 + * + * http://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 net.devh.boot.grpc.client.autoconfigure; + +import static java.util.Objects.requireNonNull; + +import java.time.Duration; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.grpc.ClientInterceptor; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.channelfactory.GrpcChannelConfigurer; +import net.devh.boot.grpc.client.config.GrpcChannelsProperties; +import net.devh.boot.grpc.client.interceptor.DefaultRequestTimeoutSetupClientInterceptor; + +/** + * The default request timeout autoconfiguration for the client. + * + *

+ * You can disable this config by using: + *

+ * + *
+ * @ImportAutoConfiguration(exclude = GrpcClientDefaultRequestTimeoutAutoConfiguration.class)
+ * 
+ * + * @author Sergei Batsura (batsura.sa@gmail.com) + */ +@Slf4j +@Configuration(proxyBeanMethods = false) +@AutoConfigureBefore(GrpcClientAutoConfiguration.class) +public class GrpcClientDefaultRequestTimeoutAutoConfiguration { + + /** + * Creates a {@link GrpcChannelConfigurer} bean applying the default request timeout from config to each new call + * using a {@link ClientInterceptor}. + * + * @param props The properties for timeout configuration. + * @return The GrpcChannelConfigurer bean with interceptor if timeout is configured. + * @see DefaultRequestTimeoutSetupClientInterceptor + */ + @Bean + GrpcChannelConfigurer timeoutGrpcChannelConfigurer(final GrpcChannelsProperties props) { + requireNonNull(props, "properties"); + + return (channel, name) -> { + Duration timeout = props.getChannel(name).getDefaultRequestTimeout(); + if (timeout != null && timeout.toMillis() > 0L) { + channel.intercept(new DefaultRequestTimeoutSetupClientInterceptor(timeout)); + } + }; + } + +} diff --git a/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java b/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java index 2d06a4e2c..301a89e73 100644 --- a/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java +++ b/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java @@ -32,6 +32,7 @@ import org.springframework.util.unit.DataSize; import org.springframework.util.unit.DataUnit; +import io.grpc.CallOptions; import io.grpc.LoadBalancerRegistry; import io.grpc.ManagedChannelBuilder; import io.grpc.NameResolverProvider; @@ -118,6 +119,35 @@ public void setAddress(final String address) { this.address = address == null ? null : URI.create(address); } + // -------------------------------------------------- + // defaultRequestTimeout + // -------------------------------------------------- + + private Duration defaultRequestTimeout = null; + + /** + * Gets the default request timeout for each new call. + * + * @return The default request timeout or null + * @see #setDefaultRequestTimeout(Duration) + */ + public Duration getDefaultRequestTimeout() { + return this.defaultRequestTimeout; + } + + /** + * Set the default request timeout duration for new calls (on a per call basis). By default and if zero value is + * configured, the timeout will not be used. The default request timeout will be ignored, if a deadline has been + * applied manually. + * + * @param defaultRequestTimeout the default request timeout or null. + * + * @see CallOptions#withDeadlineAfter(long, TimeUnit) + */ + public void setDefaultRequestTimeout(Duration defaultRequestTimeout) { + this.defaultRequestTimeout = defaultRequestTimeout; + } + // -------------------------------------------------- // defaultLoadBalancingPolicy // -------------------------------------------------- @@ -480,6 +510,9 @@ public void copyDefaultsFrom(final GrpcChannelProperties config) { if (this.address == null) { this.address = config.address; } + if (this.defaultRequestTimeout == null) { + this.defaultRequestTimeout = config.defaultRequestTimeout; + } if (this.defaultLoadBalancingPolicy == null) { this.defaultLoadBalancingPolicy = config.defaultLoadBalancingPolicy; } diff --git a/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/interceptor/DefaultRequestTimeoutSetupClientInterceptor.java b/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/interceptor/DefaultRequestTimeoutSetupClientInterceptor.java new file mode 100644 index 000000000..3a7e90cc6 --- /dev/null +++ b/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/interceptor/DefaultRequestTimeoutSetupClientInterceptor.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2016-2024 The gRPC-Spring 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 + * + * http://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 net.devh.boot.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; +import lombok.extern.slf4j.Slf4j; + +/** + * A client interceptor configuring the default request timeout / deadline for each call. + * + * @author Sergei Batsura (batsura.sa@gmail.com) + */ +@Slf4j +public class DefaultRequestTimeoutSetupClientInterceptor implements ClientInterceptor { + + private final Duration defaultRequestTimeout; + + public DefaultRequestTimeoutSetupClientInterceptor(Duration defaultRequestTimeout) { + this.defaultRequestTimeout = requireNonNull(defaultRequestTimeout, "defaultRequestTimeout"); + } + + @Override + public ClientCall interceptCall( + final MethodDescriptor method, + final CallOptions callOptions, + final Channel next) { + + if (callOptions.getDeadline() == null) { + return next.newCall(method, + callOptions.withDeadlineAfter(defaultRequestTimeout.toMillis(), TimeUnit.MILLISECONDS)); + } else { + return next.newCall(method, callOptions); + } + } +} diff --git a/grpc-client-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/grpc-client-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 44903c96e..d259292f1 100644 --- a/grpc-client-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/grpc-client-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -107,6 +107,12 @@ "description": "Connection timeout at application startup. If set to a positive duration instructs a client to connect to GRPC-endpoint when GRPC stub is created.", "defaultValue": 0 }, + { + "name": "grpc.client.GLOBAL.defaultRequestTimeout", + "type": "java.time.Duration", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "description": "The default timeout is applied to each new call. By default, and if a zero value is configured, the timeout will not be set. The default timeout will be ignored if a deadline has been set manually." + }, { "name": "grpc.client.GLOBAL.security.authority-override", "type": "java.lang.String", diff --git a/grpc-client-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/grpc-client-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index c43842c1c..1608d654b 100644 --- a/grpc-client-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/grpc-client-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -4,3 +4,4 @@ net.devh.boot.grpc.client.autoconfigure.GrpcClientHealthAutoConfiguration net.devh.boot.grpc.client.autoconfigure.GrpcClientMicrometerTraceAutoConfiguration net.devh.boot.grpc.client.autoconfigure.GrpcClientSecurityAutoConfiguration net.devh.boot.grpc.client.autoconfigure.GrpcDiscoveryClientAutoConfiguration +net.devh.boot.grpc.client.autoconfigure.GrpcClientDefaultRequestTimeoutAutoConfiguration diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/BaseAutoConfiguration.java b/tests/src/test/java/net/devh/boot/grpc/test/config/BaseAutoConfiguration.java index bea9d69ff..780cb77ee 100644 --- a/tests/src/test/java/net/devh/boot/grpc/test/config/BaseAutoConfiguration.java +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/BaseAutoConfiguration.java @@ -20,6 +20,7 @@ import org.springframework.context.annotation.Configuration; import net.devh.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration; +import net.devh.boot.grpc.client.autoconfigure.GrpcClientDefaultRequestTimeoutAutoConfiguration; import net.devh.boot.grpc.common.autoconfigure.GrpcCommonCodecAutoConfiguration; import net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration; import net.devh.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration; @@ -28,7 +29,7 @@ @Configuration @ImportAutoConfiguration({GrpcCommonCodecAutoConfiguration.class, GrpcServerAutoConfiguration.class, GrpcServerFactoryAutoConfiguration.class, GrpcServerSecurityAutoConfiguration.class, - GrpcClientAutoConfiguration.class}) + GrpcClientAutoConfiguration.class, GrpcClientDefaultRequestTimeoutAutoConfiguration.class}) public class BaseAutoConfiguration { } diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/DefaultRequestTimeoutSetupTests.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/DefaultRequestTimeoutSetupTests.java new file mode 100644 index 000000000..25d5c6df7 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/DefaultRequestTimeoutSetupTests.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2016-2024 The gRPC-Spring 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 + * + * http://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 net.devh.boot.grpc.test.setup; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import io.grpc.internal.testing.StreamRecorder; +import io.grpc.stub.StreamObserver; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.config.GrpcChannelProperties; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.proto.SomeType; + +/** + * These tests check the property {@link GrpcChannelProperties#getDefaultRequestTimeout()}}. + */ +public class DefaultRequestTimeoutSetupTests { + + @Slf4j + @SpringBootTest(properties = { + "grpc.client.GLOBAL.address=localhost:9090", + "grpc.client.GLOBAL.defaultRequestTimeout=1s", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT", + }) + @SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) + static class DefaultRequestTimeoutSetupTest extends AbstractSimpleServerClientTest { + + @Test + @SneakyThrows + @DirtiesContext + void testServiceStubTimeoutEnabledAndSuccessful() { + log.info("--- Starting test with unsuccessful and than successful call ---"); + final StreamRecorder streamRecorder1 = StreamRecorder.create(); + this.testServiceStub.echo(streamRecorder1); + assertThrows(ExecutionException.class, () -> streamRecorder1.firstValue().get()); + + final StreamRecorder streamRecorder2 = StreamRecorder.create(); + StreamObserver echo2 = testServiceStub.echo(streamRecorder2); + echo2.onNext(SomeType.getDefaultInstance()); + assertNull(streamRecorder2.getError()); + assertNotNull(streamRecorder2.firstValue().get().getVersion()); + log.info("--- Test completed --- "); + } + + @Test + @SneakyThrows + @DirtiesContext + void testServiceStubManuallyConfiguredDeadlineTakesPrecedenceOfTheConfigOne() { + log.info( + "--- Starting test that manually configured deadline takes precedence of the config default request timeout ---"); + final StreamRecorder streamRecorder = StreamRecorder.create(); + StreamObserver echo = + this.testServiceStub.withDeadlineAfter(5L, TimeUnit.SECONDS).echo(streamRecorder); + TimeUnit.SECONDS.sleep(2); + echo.onNext(SomeType.getDefaultInstance()); + assertNull(streamRecorder.getError()); + assertNotNull(streamRecorder.firstValue().get().getVersion()); + log.info("--- Test completed --- "); + } + } + + @Slf4j + @SpringBootTest(properties = { + "grpc.client.GLOBAL.address=localhost:9090", + "grpc.client.GLOBAL.defaultRequestTimeout=0s", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT", + }) + @SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) + static class ZeroDefaultRequestTimeoutSetupTest extends AbstractSimpleServerClientTest { + } + +}