Skip to content

Commit 28b073d

Browse files
Бацура Сергей АлександровичБацура Сергей Александрович
Бацура Сергей Александрович
authored and
Бацура Сергей Александрович
committed
Feat(deadline) add deadline property for client
1 parent a18aeeb commit 28b073d

File tree

8 files changed

+266
-1
lines changed

8 files changed

+266
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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.CallOptions;
28+
import io.grpc.stub.AbstractStub;
29+
import lombok.extern.slf4j.Slf4j;
30+
import net.devh.boot.grpc.client.config.GrpcChannelProperties;
31+
import net.devh.boot.grpc.client.config.GrpcChannelsProperties;
32+
import net.devh.boot.grpc.client.inject.StubTransformer;
33+
import net.devh.boot.grpc.client.interceptor.DeadlineSetupClientInterceptor;
34+
import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor;
35+
36+
/**
37+
* The deadline autoconfiguration for the client.
38+
*
39+
* <p>
40+
* You can disable this config by using:
41+
* </p>
42+
*
43+
* <pre>
44+
* <code>@ImportAutoConfiguration(exclude = GrpcClientDeadlineAutoConfiguration.class)</code>
45+
* </pre>
46+
*
47+
* @author Sergei Batsura ([email protected])
48+
*/
49+
@Slf4j
50+
@Configuration(proxyBeanMethods = false)
51+
@AutoConfigureBefore(GrpcClientAutoConfiguration.class)
52+
public class GrpcClientDeadlineAutoConfiguration {
53+
54+
private final CallOptions.Key<Duration> deadlineDuration =
55+
CallOptions.Key.createWithDefault("deadlineDuration", null);
56+
57+
/**
58+
* Creates a {@link StubTransformer} bean that will add the call credentials to the created stubs.
59+
*
60+
* @param props The properties for deadline configuration.
61+
* @return The StubTransformer bean that will add the deadline from properties.
62+
* @see AbstractStub#withDeadline(io.grpc.Deadline)
63+
*/
64+
@Bean
65+
StubTransformer deadlineStubTransformer(final GrpcChannelsProperties props) {
66+
requireNonNull(props, "properties");
67+
68+
return (name, stub) -> {
69+
GrpcChannelProperties channelProps = props.getChannel(name);
70+
if (channelProps != null && channelProps.getDeadline() != null) {
71+
return stub.withOption(deadlineDuration, channelProps.getDeadline());
72+
} else {
73+
return stub;
74+
}
75+
};
76+
}
77+
78+
@GrpcGlobalClientInterceptor
79+
DeadlineSetupClientInterceptor deadlineClientInterceptor() {
80+
return new DeadlineSetupClientInterceptor(deadlineDuration);
81+
}
82+
83+
}

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

+31
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,34 @@ public void setAddress(final String address) {
118118
this.address = address == null ? null : URI.create(address);
119119
}
120120

121+
// --------------------------------------------------
122+
// Target Deadline
123+
// --------------------------------------------------
124+
125+
private Duration deadline = null;
126+
127+
/**
128+
* Gets the connection deadline.
129+
*
130+
* @return The connection deadline or null
131+
* @see #setDeadline(Duration)
132+
*/
133+
public Duration getDeadline() {
134+
return this.deadline;
135+
}
136+
137+
/**
138+
* Set the deadline for the stub. If nothing is configured then the deadline will not be used by default. If zero
139+
* value is configured then the deadline will immediately.
140+
*
141+
* @param deadline The connection deadline or null.
142+
*
143+
* @see #setDeadline(Duration)
144+
*/
145+
public void setDeadline(Duration deadline) {
146+
this.deadline = deadline;
147+
}
148+
121149
// --------------------------------------------------
122150
// defaultLoadBalancingPolicy
123151
// --------------------------------------------------
@@ -480,6 +508,9 @@ public void copyDefaultsFrom(final GrpcChannelProperties config) {
480508
if (this.address == null) {
481509
this.address = config.address;
482510
}
511+
if (this.deadline == null) {
512+
this.deadline = config.deadline;
513+
}
483514
if (this.defaultLoadBalancingPolicy == null) {
484515
this.defaultLoadBalancingPolicy = config.defaultLoadBalancingPolicy;
485516
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 java.time.Duration;
20+
import java.util.concurrent.TimeUnit;
21+
22+
import io.grpc.CallOptions;
23+
import io.grpc.Channel;
24+
import io.grpc.ClientCall;
25+
import io.grpc.ClientInterceptor;
26+
import io.grpc.MethodDescriptor;
27+
import lombok.RequiredArgsConstructor;
28+
import lombok.extern.slf4j.Slf4j;
29+
30+
/**
31+
* Deadline setup client interceptor that create new deadline object.
32+
*
33+
* @author Sergei Batsura ([email protected])
34+
*/
35+
@Slf4j
36+
@GrpcGlobalClientInterceptor
37+
@RequiredArgsConstructor
38+
public class DeadlineSetupClientInterceptor implements ClientInterceptor {
39+
40+
private final CallOptions.Key<Duration> deadlineDuration;
41+
42+
@Override
43+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
44+
final MethodDescriptor<ReqT, RespT> method,
45+
final CallOptions callOptions,
46+
final Channel next) {
47+
48+
Duration duration = callOptions.getOption(deadlineDuration);
49+
if (duration != null) {
50+
return next.newCall(method, callOptions.withDeadlineAfter(duration.toMillis(), TimeUnit.MILLISECONDS));
51+
} else {
52+
return next.newCall(method, callOptions);
53+
}
54+
}
55+
}

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.deadline",
112+
"type": "java.time.Duration",
113+
"sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties",
114+
"description": "A deadline is used to specify a point in time past which a client is unwilling to wait for a response from a server"
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.GrpcClientDeadlineAutoConfiguration

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.GrpcClientDeadlineAutoConfiguration;
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, GrpcClientDeadlineAutoConfiguration.class})
3233
public class BaseAutoConfiguration {
3334

3435
}

tests/src/test/java/net/devh/boot/grpc/test/interceptor/DefaultClientInterceptorTest.java

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import io.micrometer.core.instrument.binder.grpc.MetricCollectingClientInterceptor;
3434
import io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor;
3535
import net.devh.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration;
36+
import net.devh.boot.grpc.client.interceptor.DeadlineSetupClientInterceptor;
3637
import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry;
3738
import net.devh.boot.grpc.client.metrics.MetricsClientInterceptor;
3839

@@ -54,6 +55,7 @@ void testDefaultInterceptors() {
5455
expected.add(this.applicationContext.getBean(MetricCollectingClientInterceptor.class));
5556
expected.add(this.applicationContext.getBean(MetricsClientInterceptor.class));
5657
expected.add(this.applicationContext.getBean(ObservationGrpcClientInterceptor.class));
58+
expected.add(this.applicationContext.getBean(DeadlineSetupClientInterceptor.class));
5759

5860
final List<ClientInterceptor> actual = new ArrayList<>(this.registry.getClientInterceptors());
5961
assertThat(actual).containsExactlyInAnyOrderElementsOf(expected);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 io.grpc.Status.DEADLINE_EXCEEDED;
20+
import static net.devh.boot.grpc.test.util.GrpcAssertions.assertStatus;
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
import static org.junit.jupiter.api.Assertions.assertNotNull;
23+
import static org.junit.jupiter.api.Assertions.assertNull;
24+
import static org.junit.jupiter.api.Assertions.assertThrows;
25+
26+
import java.util.concurrent.ExecutionException;
27+
28+
import org.junit.jupiter.api.Test;
29+
import org.springframework.boot.test.context.SpringBootTest;
30+
import org.springframework.test.annotation.DirtiesContext;
31+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
32+
33+
import io.grpc.StatusRuntimeException;
34+
import io.grpc.internal.testing.StreamRecorder;
35+
import io.grpc.stub.StreamObserver;
36+
import lombok.SneakyThrows;
37+
import lombok.extern.slf4j.Slf4j;
38+
import net.devh.boot.grpc.client.config.GrpcChannelProperties;
39+
import net.devh.boot.grpc.test.config.BaseAutoConfiguration;
40+
import net.devh.boot.grpc.test.config.ServiceConfiguration;
41+
import net.devh.boot.grpc.test.proto.SomeType;
42+
43+
/**
44+
* These tests check the property {@link GrpcChannelProperties#getDeadline()}.
45+
*/
46+
@Slf4j
47+
@SpringBootTest(properties = {
48+
"grpc.client.GLOBAL.address=localhost:9090",
49+
"grpc.client.GLOBAL.deadline=1s",
50+
"grpc.client.GLOBAL.negotiationType=PLAINTEXT",
51+
})
52+
@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class})
53+
public class DeadlineTests extends AbstractSimpleServerClientTest {
54+
55+
@Test
56+
@SneakyThrows
57+
@DirtiesContext
58+
void testServiceStubDeadlineEnabledAndSuccessful() {
59+
log.info("--- Starting test with unsuccessful and than successful call ---");
60+
final StreamRecorder<SomeType> streamRecorder1 = StreamRecorder.create();
61+
StreamObserver<SomeType> echo1 = this.testServiceStub.echo(streamRecorder1);
62+
assertThrows(ExecutionException.class, () -> streamRecorder1.firstValue().get());
63+
64+
final StreamRecorder<SomeType> streamRecorder2 = StreamRecorder.create();
65+
StreamObserver<SomeType> echo2 = testServiceStub.echo(streamRecorder2);
66+
echo2.onNext(SomeType.getDefaultInstance());
67+
assertNull(streamRecorder2.getError());
68+
assertNotNull(streamRecorder2.firstValue().get().getVersion());
69+
log.info("--- Test completed --- ");
70+
}
71+
72+
@Test
73+
@SneakyThrows
74+
@DirtiesContext
75+
void testServiceStubDeadlineEnabledAndUnsuccessful() {
76+
log.info("--- Starting test with unsuccessful call ---");
77+
final StreamRecorder<SomeType> streamRecorder = StreamRecorder.create();
78+
this.testServiceStub.echo(streamRecorder);
79+
assertThrows(ExecutionException.class, () -> streamRecorder.firstValue().get());
80+
assertNotNull(streamRecorder.getError());
81+
assertEquals(StatusRuntimeException.class, streamRecorder.getError().getClass());
82+
assertStatus(DEADLINE_EXCEEDED.getCode(), (StatusRuntimeException) streamRecorder.getError());
83+
log.info("--- Test completed --- ");
84+
}
85+
86+
}

0 commit comments

Comments
 (0)