Skip to content

Commit ee810d1

Browse files
authored
Merge pull request #1132 from batsura-sa/feature/deadline_property
Feat(defaultRequestTimeout) add defaultRequestTimeout property for the client
2 parents abf7fb8 + c1b1ced commit ee810d1

File tree

7 files changed

+269
-1
lines changed

7 files changed

+269
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (c) 2016-2024 The gRPC-Spring Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package net.devh.boot.grpc.client.autoconfigure;
18+
19+
import static java.util.Objects.requireNonNull;
20+
21+
import java.time.Duration;
22+
23+
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
24+
import org.springframework.context.annotation.Bean;
25+
import org.springframework.context.annotation.Configuration;
26+
27+
import io.grpc.ClientInterceptor;
28+
import lombok.extern.slf4j.Slf4j;
29+
import net.devh.boot.grpc.client.channelfactory.GrpcChannelConfigurer;
30+
import net.devh.boot.grpc.client.config.GrpcChannelsProperties;
31+
import net.devh.boot.grpc.client.interceptor.DefaultRequestTimeoutSetupClientInterceptor;
32+
33+
/**
34+
* The default request timeout autoconfiguration for the client.
35+
*
36+
* <p>
37+
* You can disable this config by using:
38+
* </p>
39+
*
40+
* <pre>
41+
* <code>@ImportAutoConfiguration(exclude = GrpcClientDefaultRequestTimeoutAutoConfiguration.class)</code>
42+
* </pre>
43+
*
44+
* @author Sergei Batsura ([email protected])
45+
*/
46+
@Slf4j
47+
@Configuration(proxyBeanMethods = false)
48+
@AutoConfigureBefore(GrpcClientAutoConfiguration.class)
49+
public class GrpcClientDefaultRequestTimeoutAutoConfiguration {
50+
51+
/**
52+
* Creates a {@link GrpcChannelConfigurer} bean applying the default request timeout from config to each new call
53+
* using a {@link ClientInterceptor}.
54+
*
55+
* @param props The properties for timeout configuration.
56+
* @return The GrpcChannelConfigurer bean with interceptor if timeout is configured.
57+
* @see DefaultRequestTimeoutSetupClientInterceptor
58+
*/
59+
@Bean
60+
GrpcChannelConfigurer timeoutGrpcChannelConfigurer(final GrpcChannelsProperties props) {
61+
requireNonNull(props, "properties");
62+
63+
return (channel, name) -> {
64+
Duration timeout = props.getChannel(name).getDefaultRequestTimeout();
65+
if (timeout != null && timeout.toMillis() > 0L) {
66+
channel.intercept(new DefaultRequestTimeoutSetupClientInterceptor(timeout));
67+
}
68+
};
69+
}
70+
71+
}

grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java

+33
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.util.unit.DataSize;
3333
import org.springframework.util.unit.DataUnit;
3434

35+
import io.grpc.CallOptions;
3536
import io.grpc.LoadBalancerRegistry;
3637
import io.grpc.ManagedChannelBuilder;
3738
import io.grpc.NameResolverProvider;
@@ -118,6 +119,35 @@ public void setAddress(final String address) {
118119
this.address = address == null ? null : URI.create(address);
119120
}
120121

122+
// --------------------------------------------------
123+
// defaultRequestTimeout
124+
// --------------------------------------------------
125+
126+
private Duration defaultRequestTimeout = null;
127+
128+
/**
129+
* Gets the default request timeout for each new call.
130+
*
131+
* @return The default request timeout or null
132+
* @see #setDefaultRequestTimeout(Duration)
133+
*/
134+
public Duration getDefaultRequestTimeout() {
135+
return this.defaultRequestTimeout;
136+
}
137+
138+
/**
139+
* Set the default request timeout duration for new calls (on a per call basis). By default and if zero value is
140+
* configured, the timeout will not be used. The default request timeout will be ignored, if a deadline has been
141+
* applied manually.
142+
*
143+
* @param defaultRequestTimeout the default request timeout or null.
144+
*
145+
* @see CallOptions#withDeadlineAfter(long, TimeUnit)
146+
*/
147+
public void setDefaultRequestTimeout(Duration defaultRequestTimeout) {
148+
this.defaultRequestTimeout = defaultRequestTimeout;
149+
}
150+
121151
// --------------------------------------------------
122152
// defaultLoadBalancingPolicy
123153
// --------------------------------------------------
@@ -480,6 +510,9 @@ public void copyDefaultsFrom(final GrpcChannelProperties config) {
480510
if (this.address == null) {
481511
this.address = config.address;
482512
}
513+
if (this.defaultRequestTimeout == null) {
514+
this.defaultRequestTimeout = config.defaultRequestTimeout;
515+
}
483516
if (this.defaultLoadBalancingPolicy == null) {
484517
this.defaultLoadBalancingPolicy = config.defaultLoadBalancingPolicy;
485518
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright (c) 2016-2024 The gRPC-Spring Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package net.devh.boot.grpc.client.interceptor;
18+
19+
import static java.util.Objects.requireNonNull;
20+
21+
import java.time.Duration;
22+
import java.util.concurrent.TimeUnit;
23+
24+
import io.grpc.CallOptions;
25+
import io.grpc.Channel;
26+
import io.grpc.ClientCall;
27+
import io.grpc.ClientInterceptor;
28+
import io.grpc.MethodDescriptor;
29+
import lombok.extern.slf4j.Slf4j;
30+
31+
/**
32+
* A client interceptor configuring the default request timeout / deadline for each call.
33+
*
34+
* @author Sergei Batsura ([email protected])
35+
*/
36+
@Slf4j
37+
public class DefaultRequestTimeoutSetupClientInterceptor implements ClientInterceptor {
38+
39+
private final Duration defaultRequestTimeout;
40+
41+
public DefaultRequestTimeoutSetupClientInterceptor(Duration defaultRequestTimeout) {
42+
this.defaultRequestTimeout = requireNonNull(defaultRequestTimeout, "defaultRequestTimeout");
43+
}
44+
45+
@Override
46+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
47+
final MethodDescriptor<ReqT, RespT> method,
48+
final CallOptions callOptions,
49+
final Channel next) {
50+
51+
if (callOptions.getDeadline() == null) {
52+
return next.newCall(method,
53+
callOptions.withDeadlineAfter(defaultRequestTimeout.toMillis(), TimeUnit.MILLISECONDS));
54+
} else {
55+
return next.newCall(method, callOptions);
56+
}
57+
}
58+
}

grpc-client-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json

+6
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@
107107
"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.",
108108
"defaultValue": 0
109109
},
110+
{
111+
"name": "grpc.client.GLOBAL.defaultRequestTimeout",
112+
"type": "java.time.Duration",
113+
"sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties",
114+
"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."
115+
},
110116
{
111117
"name": "grpc.client.GLOBAL.security.authority-override",
112118
"type": "java.lang.String",

grpc-client-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ net.devh.boot.grpc.client.autoconfigure.GrpcClientHealthAutoConfiguration
44
net.devh.boot.grpc.client.autoconfigure.GrpcClientMicrometerTraceAutoConfiguration
55
net.devh.boot.grpc.client.autoconfigure.GrpcClientSecurityAutoConfiguration
66
net.devh.boot.grpc.client.autoconfigure.GrpcDiscoveryClientAutoConfiguration
7+
net.devh.boot.grpc.client.autoconfigure.GrpcClientDefaultRequestTimeoutAutoConfiguration

tests/src/test/java/net/devh/boot/grpc/test/config/BaseAutoConfiguration.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.springframework.context.annotation.Configuration;
2121

2222
import net.devh.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration;
23+
import net.devh.boot.grpc.client.autoconfigure.GrpcClientDefaultRequestTimeoutAutoConfiguration;
2324
import net.devh.boot.grpc.common.autoconfigure.GrpcCommonCodecAutoConfiguration;
2425
import net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration;
2526
import net.devh.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration;
@@ -28,7 +29,7 @@
2829
@Configuration
2930
@ImportAutoConfiguration({GrpcCommonCodecAutoConfiguration.class, GrpcServerAutoConfiguration.class,
3031
GrpcServerFactoryAutoConfiguration.class, GrpcServerSecurityAutoConfiguration.class,
31-
GrpcClientAutoConfiguration.class})
32+
GrpcClientAutoConfiguration.class, GrpcClientDefaultRequestTimeoutAutoConfiguration.class})
3233
public class BaseAutoConfiguration {
3334

3435
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright (c) 2016-2024 The gRPC-Spring Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package net.devh.boot.grpc.test.setup;
18+
19+
import static org.junit.jupiter.api.Assertions.assertNotNull;
20+
import static org.junit.jupiter.api.Assertions.assertNull;
21+
import static org.junit.jupiter.api.Assertions.assertThrows;
22+
23+
import java.util.concurrent.ExecutionException;
24+
import java.util.concurrent.TimeUnit;
25+
26+
import org.junit.jupiter.api.Test;
27+
import org.springframework.boot.test.context.SpringBootTest;
28+
import org.springframework.test.annotation.DirtiesContext;
29+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
30+
31+
import io.grpc.internal.testing.StreamRecorder;
32+
import io.grpc.stub.StreamObserver;
33+
import lombok.SneakyThrows;
34+
import lombok.extern.slf4j.Slf4j;
35+
import net.devh.boot.grpc.client.config.GrpcChannelProperties;
36+
import net.devh.boot.grpc.test.config.BaseAutoConfiguration;
37+
import net.devh.boot.grpc.test.config.ServiceConfiguration;
38+
import net.devh.boot.grpc.test.proto.SomeType;
39+
40+
/**
41+
* These tests check the property {@link GrpcChannelProperties#getDefaultRequestTimeout()}}.
42+
*/
43+
public class DefaultRequestTimeoutSetupTests {
44+
45+
@Slf4j
46+
@SpringBootTest(properties = {
47+
"grpc.client.GLOBAL.address=localhost:9090",
48+
"grpc.client.GLOBAL.defaultRequestTimeout=1s",
49+
"grpc.client.GLOBAL.negotiationType=PLAINTEXT",
50+
})
51+
@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class})
52+
static class DefaultRequestTimeoutSetupTest extends AbstractSimpleServerClientTest {
53+
54+
@Test
55+
@SneakyThrows
56+
@DirtiesContext
57+
void testServiceStubTimeoutEnabledAndSuccessful() {
58+
log.info("--- Starting test with unsuccessful and than successful call ---");
59+
final StreamRecorder<SomeType> streamRecorder1 = StreamRecorder.create();
60+
this.testServiceStub.echo(streamRecorder1);
61+
assertThrows(ExecutionException.class, () -> streamRecorder1.firstValue().get());
62+
63+
final StreamRecorder<SomeType> streamRecorder2 = StreamRecorder.create();
64+
StreamObserver<SomeType> echo2 = testServiceStub.echo(streamRecorder2);
65+
echo2.onNext(SomeType.getDefaultInstance());
66+
assertNull(streamRecorder2.getError());
67+
assertNotNull(streamRecorder2.firstValue().get().getVersion());
68+
log.info("--- Test completed --- ");
69+
}
70+
71+
@Test
72+
@SneakyThrows
73+
@DirtiesContext
74+
void testServiceStubManuallyConfiguredDeadlineTakesPrecedenceOfTheConfigOne() {
75+
log.info(
76+
"--- Starting test that manually configured deadline takes precedence of the config default request timeout ---");
77+
final StreamRecorder<SomeType> streamRecorder = StreamRecorder.create();
78+
StreamObserver<SomeType> echo =
79+
this.testServiceStub.withDeadlineAfter(5L, TimeUnit.SECONDS).echo(streamRecorder);
80+
TimeUnit.SECONDS.sleep(2);
81+
echo.onNext(SomeType.getDefaultInstance());
82+
assertNull(streamRecorder.getError());
83+
assertNotNull(streamRecorder.firstValue().get().getVersion());
84+
log.info("--- Test completed --- ");
85+
}
86+
}
87+
88+
@Slf4j
89+
@SpringBootTest(properties = {
90+
"grpc.client.GLOBAL.address=localhost:9090",
91+
"grpc.client.GLOBAL.defaultRequestTimeout=0s",
92+
"grpc.client.GLOBAL.negotiationType=PLAINTEXT",
93+
})
94+
@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class})
95+
static class ZeroDefaultRequestTimeoutSetupTest extends AbstractSimpleServerClientTest {
96+
}
97+
98+
}

0 commit comments

Comments
 (0)