Added defaultDeadline client property

Signed-off-by: Sergei Batsura <batsura.sa@gmail.com>
[resolves #136]
This commit is contained in:
Sergei Batsura
2025-03-13 13:42:33 +03:00
committed by Dave Syer
parent ff1b8e3dfa
commit b004ba6559
10 changed files with 205 additions and 8 deletions

View File

@@ -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);
};
}
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(final MethodDescriptor<ReqT, RespT> 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);
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<T extends ManagedChannelBuilder<T
Map<String, ?> 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<Duration> durationProperty(BiConsumer<Long, TimeUnit> setter) {

View File

@@ -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;

View File

@@ -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());