mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-29 13:39:16 +00:00
Add type-safe gRPC service config properties
Update `GrpcClientProperties` to include type-safe service config properties commonly configured items. Closes gh-49540
This commit is contained in:
@@ -30,6 +30,7 @@ dependencies {
|
||||
|
||||
optional(project(":core:spring-boot-autoconfigure"))
|
||||
optional(project(":module:spring-boot-micrometer-observation"))
|
||||
optional("io.grpc:grpc-grpclb")
|
||||
optional("io.grpc:grpc-stub")
|
||||
optional("io.grpc:grpc-netty")
|
||||
optional("io.grpc:grpc-netty-shaded")
|
||||
|
||||
+13
@@ -70,6 +70,11 @@ public class GrpcClientProperties {
|
||||
*/
|
||||
private boolean bypassCertificateValidation;
|
||||
|
||||
/**
|
||||
* Service config for the channel.
|
||||
*/
|
||||
private @Nullable ServiceConfig serviceConfig;
|
||||
|
||||
private final Inbound inbound = new Inbound();
|
||||
|
||||
@Name("default")
|
||||
@@ -107,6 +112,14 @@ public class GrpcClientProperties {
|
||||
this.bypassCertificateValidation = bypassCertificateValidation;
|
||||
}
|
||||
|
||||
public @Nullable ServiceConfig getServiceConfig() {
|
||||
return this.serviceConfig;
|
||||
}
|
||||
|
||||
public void setServiceConfig(@Nullable ServiceConfig serviceConfig) {
|
||||
this.serviceConfig = serviceConfig;
|
||||
}
|
||||
|
||||
public Inbound getInbound() {
|
||||
return this.inbound;
|
||||
}
|
||||
|
||||
+37
-4
@@ -16,9 +16,15 @@
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel.Health;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link GrpcClientDefaultServiceConfigCustomizer} to apply {@link GrpcClientProperties}.
|
||||
@@ -34,11 +40,38 @@ record PropertiesGrpcClientDefaultServiceConfigCustomizer(
|
||||
public void customize(String target, Map<String, Object> defaultServiceConfig) {
|
||||
Channel channel = this.properties.getChannel().get(target);
|
||||
channel = (channel != null) ? channel : this.properties.getChannel().get("default");
|
||||
if (channel != null && channel.getHealth().isEnabled()) {
|
||||
String serviceName = channel.getHealth().getServiceName();
|
||||
Map<String, String> healthCheckConfig = Map.of("serviceName", (serviceName != null) ? serviceName : "");
|
||||
defaultServiceConfig.put("healthCheckConfig", healthCheckConfig);
|
||||
if (channel == null) {
|
||||
return;
|
||||
}
|
||||
applyServiceConfig(channel.getServiceConfig(), defaultServiceConfig);
|
||||
applyHealth(channel.getHealth(), defaultServiceConfig);
|
||||
}
|
||||
|
||||
private void applyServiceConfig(@Nullable ServiceConfig serviceConfig, Map<String, Object> defaultServiceConfig) {
|
||||
if (serviceConfig != null) {
|
||||
serviceConfig.applyTo(defaultServiceConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyHealth(Health health, Map<String, Object> defaultServiceConfig) {
|
||||
if (!health.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
String serviceName = (health.getServiceName() != null) ? health.getServiceName() : "";
|
||||
Map<String, Object> healthCheckConfig = cloneOrCreateHealthCheckConfig(defaultServiceConfig);
|
||||
String existingServiceName = (String) healthCheckConfig.get(ServiceConfig.HEALTH_CHECK_SERVICE_NAME_KEY);
|
||||
Assert.state(existingServiceName == null || serviceName.equals(existingServiceName),
|
||||
() -> "Unable to change health check config service name from '%s' to '%s'"
|
||||
.formatted(existingServiceName, serviceName));
|
||||
healthCheckConfig.put(ServiceConfig.HEALTH_CHECK_SERVICE_NAME_KEY, serviceName);
|
||||
defaultServiceConfig.put(ServiceConfig.HEALTH_CHECK_CONFIG_KEY, healthCheckConfig);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> cloneOrCreateHealthCheckConfig(Map<String, Object> defaultServiceConfig) {
|
||||
Map<String, Object> healthCheckConfig = (Map<String, Object>) defaultServiceConfig
|
||||
.get(ServiceConfig.HEALTH_CHECK_CONFIG_KEY);
|
||||
return new LinkedHashMap<>((healthCheckConfig != null) ? healthCheckConfig : Collections.emptyMap());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import io.grpc.Status;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.context.properties.PropertyMapper.Source.Adapter;
|
||||
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
|
||||
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
/**
|
||||
* Bindable service configuration for gRPC channel. Allows type safe binding of common
|
||||
* service configuration options which can ultimately be applied to the {@link Map}
|
||||
* provided by a {@link GrpcClientDefaultServiceConfigCustomizer}.
|
||||
* <p>
|
||||
* The configuration provided here is a subset of the canonical <a href=
|
||||
* "https://github.com/grpc/grpc-proto/blob/master/grpc/service_config/service_config.proto">service_config.proto</a>
|
||||
* protocol definition. For advanced or experimental service configurations, use the
|
||||
* {@link GrpcClientDefaultServiceConfigCustomizer} to directly add any entries supported
|
||||
* by {@code grpc-java}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @param loadbalancing load balancing configurations in the order that they should be
|
||||
* applied
|
||||
* @param method method configuration
|
||||
* @param retrythrottling retry throttling policy
|
||||
* @param healthcheck health check configuration
|
||||
* @since 4.1.0
|
||||
* @see GrpcClientDefaultServiceConfigCustomizer
|
||||
* @see io.grpc.internal.ServiceConfigUtil
|
||||
*/
|
||||
public record ServiceConfig(@Nullable List<LoadBalancingConfig> loadbalancing, @Nullable List<MethodConfig> method,
|
||||
@Nullable RetryThrottlingPolicy retrythrottling, @Nullable HealthCheckConfig healthcheck) {
|
||||
|
||||
static final String HEALTH_CHECK_CONFIG_KEY = "healthCheckConfig";
|
||||
|
||||
static final String HEALTH_CHECK_SERVICE_NAME_KEY = "serviceName";
|
||||
|
||||
/**
|
||||
* Apply this service config to the given gRPC Java config Map.
|
||||
* @param grpcJavaConfig the gRPC Java config map
|
||||
*/
|
||||
public void applyTo(Map<String, Object> grpcJavaConfig) {
|
||||
applyTo(new GrpcJavaConfig(grpcJavaConfig));
|
||||
}
|
||||
|
||||
private void applyTo(GrpcJavaConfig config) {
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::loadbalancing)
|
||||
.as(listOf(LoadBalancingConfig::grpcJavaConfig))
|
||||
.to(config.in("loadBalancingConfig"));
|
||||
map.from(this::method).as(listOf(MethodConfig::grpcJavaConfig)).to(config.in("methodConfig"));
|
||||
map.from(this::retrythrottling).as(RetryThrottlingPolicy::grpcJavaConfig).to(config.in("retryThrottling"));
|
||||
map.from(this::healthcheck).as(HealthCheckConfig::grpcJavaConfig).to(config.in(HEALTH_CHECK_CONFIG_KEY));
|
||||
}
|
||||
|
||||
static <T> Adapter<List<T>, @Nullable List<Map<String, Object>>> listOf(Function<T, Map<String, Object>> adapter) {
|
||||
return (list) -> (!CollectionUtils.isEmpty(list)) ? list.stream().map(adapter).toList() : null;
|
||||
}
|
||||
|
||||
static String durationString(Duration duration) {
|
||||
return duration.getSeconds() + "." + duration.getNano() + "s";
|
||||
}
|
||||
|
||||
static String bytesString(DataSize dataSize) {
|
||||
return Long.toString(dataSize.toBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load balancing config.
|
||||
*
|
||||
* @param pickfirst 'pick first' load balancing
|
||||
* @param roundrobin 'round robin' load balancing
|
||||
* @param weightedroundrobin 'weighted round robin' load balancing
|
||||
* @param grpc 'grpc' load balancing
|
||||
*/
|
||||
public record LoadBalancingConfig(@Nullable PickFirstLoadBalancingConfig pickfirst,
|
||||
@Nullable RoundRobinLoadBalancingConfig roundrobin,
|
||||
@Nullable WeightedRoundRobinLoadBalancingConfig weightedroundrobin,
|
||||
@Nullable GrpcLoadBalancingConfig grpc) {
|
||||
|
||||
public LoadBalancingConfig {
|
||||
if (pickfirst == null && roundrobin == null && weightedroundrobin == null && grpc == null) {
|
||||
throw new InvalidConfigurationPropertyValueException("loadbalancing", null,
|
||||
"Missing load balancing strategy");
|
||||
}
|
||||
MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> {
|
||||
entries.put("loadbalancing.pickfirst", pickfirst);
|
||||
entries.put("loadbalancing.roundrobin", roundrobin);
|
||||
entries.put("loadbalancing.weightedroundrobin", weightedroundrobin);
|
||||
entries.put("loadbalancing.grpc", grpc);
|
||||
});
|
||||
}
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
LinkedHashMap<String, Object> grpcJavaConfig = new LinkedHashMap<>();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::pickfirst)
|
||||
.as(PickFirstLoadBalancingConfig::grpcJavaConfig)
|
||||
.to((loadBalancingConfig) -> grpcJavaConfig.put("pick_first", loadBalancingConfig));
|
||||
map.from(this::roundrobin)
|
||||
.as(RoundRobinLoadBalancingConfig::grpcJavaConfig)
|
||||
.to((loadBalancingConfig) -> grpcJavaConfig.put("round_robin", loadBalancingConfig));
|
||||
map.from(this::weightedroundrobin)
|
||||
.as(WeightedRoundRobinLoadBalancingConfig::grpcJavaConfig)
|
||||
.to((loadBalancingConfig) -> grpcJavaConfig.put("weighted_round_robin", loadBalancingConfig));
|
||||
map.from(this::grpc)
|
||||
.as(GrpcLoadBalancingConfig::grpcJavaConfig)
|
||||
.to((loadBalancingConfig) -> grpcJavaConfig.put("grpclb", loadBalancingConfig));
|
||||
return grpcJavaConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 'pick first' load balancing.
|
||||
*
|
||||
* @param shuffleAddressList randomly shuffle the list of addresses received from
|
||||
* the name resolver before attempting to connect to them.
|
||||
*/
|
||||
public record PickFirstLoadBalancingConfig(Boolean shuffleAddressList) {
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
// Aligned with PickFirstLoadBalancerProvider
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::shuffleAddressList).to(grpcJavaConfig.in("shuffleAddressList"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 'round robin' load balancing.
|
||||
*/
|
||||
public record RoundRobinLoadBalancingConfig() {
|
||||
|
||||
/**
|
||||
* Return the gRPC java config as supported by the
|
||||
* {@code SecretRoundRobinLoadBalancerProvider}.
|
||||
* @return the config
|
||||
*/
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 'weighted round robin' load balancing.
|
||||
*
|
||||
* @param blackoutPeriod must report load metrics continuously for at least this
|
||||
* long before the endpoint weight will be used
|
||||
* @param weightExpirationPeriod if has not reported load metrics in this long,
|
||||
* then we stop using the reported weight
|
||||
* @param outOfBandReportingPeriod load reporting interval to request from the
|
||||
* server
|
||||
* @param enableOutOfBandLoadReport whether to enable out-of-band utilization
|
||||
* reporting collection from the endpoints
|
||||
* @param weightUpdatePeriod how often endpoint weights are recalculated
|
||||
* @param errorUtilizationPenalty multiplier used to adjust endpoint weights with
|
||||
* the error rate calculated as eps/qps
|
||||
*/
|
||||
public record WeightedRoundRobinLoadBalancingConfig(Duration blackoutPeriod, Duration weightExpirationPeriod,
|
||||
Duration outOfBandReportingPeriod, Boolean enableOutOfBandLoadReport, Duration weightUpdatePeriod,
|
||||
Float errorUtilizationPenalty) {
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
// Aligned with WeightedRoundRobinLoadBalancerProvider
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::blackoutPeriod)
|
||||
.as(ServiceConfig::durationString)
|
||||
.to(grpcJavaConfig.in("blackoutPeriod"));
|
||||
map.from(this::weightExpirationPeriod)
|
||||
.as(ServiceConfig::durationString)
|
||||
.to(grpcJavaConfig.in("weightExpirationPeriod"));
|
||||
map.from(this::outOfBandReportingPeriod)
|
||||
.as(ServiceConfig::durationString)
|
||||
.to(grpcJavaConfig.in("oobReportingPeriod"));
|
||||
map.from(this::enableOutOfBandLoadReport).to(grpcJavaConfig.in("enableOobLoadReport"));
|
||||
map.from(this::weightUpdatePeriod)
|
||||
.as(ServiceConfig::durationString)
|
||||
.to(grpcJavaConfig.in("weightUpdatePeriod"));
|
||||
map.from(this::errorUtilizationPenalty).to(grpcJavaConfig.in("errorUtilizationPenalty"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 'grpc' load balancing.
|
||||
*
|
||||
* @param child what load balancer policies to use for routing between the backend
|
||||
* addresses
|
||||
* @param serviceName override of the service name to be sent to the balancer
|
||||
* @param initialFallbackTimeout timeout in seconds for receiving the server list
|
||||
*/
|
||||
public record GrpcLoadBalancingConfig(List<LoadBalancingConfig> child, String serviceName,
|
||||
Duration initialFallbackTimeout) {
|
||||
|
||||
public GrpcLoadBalancingConfig {
|
||||
child.forEach(this::assertChild);
|
||||
}
|
||||
|
||||
private void assertChild(LoadBalancingConfig child) {
|
||||
if (child.pickfirst() == null && child.roundrobin() == null) {
|
||||
throw new InvalidConfigurationPropertyValueException("loadbalancing.grpc.child", null,
|
||||
"Only 'pickfirst' or 'roundrobin' child load balancer strategies can be used");
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
// Aligned with GrpclbLoadBalancerProvider
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::child)
|
||||
.as(listOf(LoadBalancingConfig::grpcJavaConfig))
|
||||
.to(grpcJavaConfig.in("childPolicy"));
|
||||
map.from(this::serviceName).to(grpcJavaConfig.in("serviceName"));
|
||||
map.from(this::initialFallbackTimeout)
|
||||
.as(ServiceConfig::durationString)
|
||||
.to(grpcJavaConfig.in("initialFallbackTimeout"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method configuration.
|
||||
*
|
||||
* @param name Names of the methods to which this configuration applies
|
||||
* @param waitForReady Whether RPCs sent to this method should wait until the
|
||||
* connection is ready by default
|
||||
* @param maxRequestMessage maximum allowed payload size for an individual request or
|
||||
* object in a stream
|
||||
* @param maxResponseMessage maximum allowed payload size for an individual response
|
||||
* or object in a stream
|
||||
* @param timeout default timeout for RPCs sent to this method
|
||||
* @param retry retry policy for outgoing RPCs
|
||||
* @param hedging hedging policy for outgoing RPCs
|
||||
*/
|
||||
public record MethodConfig(List<Name> name, Boolean waitForReady, DataSize maxRequestMessage,
|
||||
DataSize maxResponseMessage, Duration timeout, RetryPolicy retry, HedgingPolicy hedging) {
|
||||
|
||||
public MethodConfig {
|
||||
MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> {
|
||||
entries.put("method.retry", retry);
|
||||
entries.put("method.hedging", hedging);
|
||||
});
|
||||
}
|
||||
|
||||
static @Nullable List<Map<String, Object>> grpcJavaConfigs(List<MethodConfig> methodConfigs) {
|
||||
return (!CollectionUtils.isEmpty(methodConfigs))
|
||||
? methodConfigs.stream().map(MethodConfig::grpcJavaConfig).toList() : null;
|
||||
}
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::name).as(listOf(Name::grpcJavaConfig)).to(grpcJavaConfig.in("name"));
|
||||
map.from(this::waitForReady).to(grpcJavaConfig.in("waitForReady"));
|
||||
map.from(this::maxRequestMessage)
|
||||
.as(ServiceConfig::bytesString)
|
||||
.to(grpcJavaConfig.in("maxRequestMessageBytes"));
|
||||
map.from(this::maxResponseMessage)
|
||||
.as(ServiceConfig::bytesString)
|
||||
.to(grpcJavaConfig.in("maxResponseMessageBytes"));
|
||||
map.from(this::timeout).as(ServiceConfig::durationString).to(grpcJavaConfig.in("timeout"));
|
||||
map.from(this::retry).as(RetryPolicy::grpcJavaConfig).to(grpcJavaConfig.in("retryPolicy"));
|
||||
map.from(this::hedging).as(HedgingPolicy::grpcJavaConfig).to(grpcJavaConfig.in("hedgingPolicy"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of a gRPC method.
|
||||
*
|
||||
* @param service service name
|
||||
* @param method method name
|
||||
*/
|
||||
public record Name(String service, String method) {
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::service).to(grpcJavaConfig.in("service"));
|
||||
map.from(this::method).to(grpcJavaConfig.in("method"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry policy for outgoing RPCs.
|
||||
*
|
||||
* @param maxAttempts maximum number of RPC attempts, including the original
|
||||
* attempt
|
||||
* @param initialBackoff initial exponential backoff
|
||||
* @param maxBackoff maximum exponential backoff
|
||||
* @param backoffMultiplier exponential backoff multiplier
|
||||
* @param perAttemptReceiveTimeout per-attempt receive timeout
|
||||
* @param retryableStatusCodes status codes which may be retried
|
||||
*/
|
||||
public record RetryPolicy(Integer maxAttempts, Duration initialBackoff, Duration maxBackoff,
|
||||
Double backoffMultiplier, Duration perAttemptReceiveTimeout, Set<Status.Code> retryableStatusCodes) {
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::maxAttempts).as(Objects::toString).to(grpcJavaConfig.in("maxAttempts"));
|
||||
map.from(this::initialBackoff)
|
||||
.as(ServiceConfig::durationString)
|
||||
.to(grpcJavaConfig.in("initialBackoff"));
|
||||
map.from(this::maxBackoff).as(ServiceConfig::durationString).to(grpcJavaConfig.in("maxBackoff"));
|
||||
map.from(this::backoffMultiplier).to(grpcJavaConfig.in("backoffMultiplier"));
|
||||
map.from(this::perAttemptReceiveTimeout)
|
||||
.as(ServiceConfig::durationString)
|
||||
.to(grpcJavaConfig.in("perAttemptRecvTimeout"));
|
||||
map.from(this::retryableStatusCodes)
|
||||
.as((codes) -> codes.stream().map(Objects::toString).toList())
|
||||
.to(grpcJavaConfig.in("retryableStatusCodes"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Hedging policy for outgoing RPCs.
|
||||
*
|
||||
* @param maxAttempts maximum number of send attempts
|
||||
* @param delay delay for subsequent RPCs
|
||||
* @param nonFatalStatusCodes status codes which indicate other hedged RPCs may
|
||||
* still succeed
|
||||
*/
|
||||
public record HedgingPolicy(Integer maxAttempts, Duration delay, Set<Status.Code> nonFatalStatusCodes) {
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::maxAttempts).as(Objects::toString).to(grpcJavaConfig.in("maxAttempts"));
|
||||
map.from(this::delay).as(ServiceConfig::durationString).to(grpcJavaConfig.in("hedgingDelay"));
|
||||
map.from(this::nonFatalStatusCodes)
|
||||
.as((codes) -> codes.stream().map(Objects::toString).toList())
|
||||
.to(grpcJavaConfig.in("nonFatalStatusCodes"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry throttling policy.
|
||||
*
|
||||
* @param maxTokens maximum number of tokens
|
||||
* @param tokenRatio the token ratio
|
||||
*/
|
||||
public record RetryThrottlingPolicy(Float maxTokens, Float tokenRatio) {
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::maxTokens).as(Objects::toString).to(grpcJavaConfig.in("maxTokens"));
|
||||
map.from(this::tokenRatio).as(Objects::toString).to(grpcJavaConfig.in("tokenRatio"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check configuration.
|
||||
*
|
||||
* @param serviceName service name to use in the health-checking request.
|
||||
*/
|
||||
public record HealthCheckConfig(String serviceName) {
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::serviceName).to(grpcJavaConfig.in(HEALTH_CHECK_SERVICE_NAME_KEY));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper to collection gRPC java config.
|
||||
*
|
||||
* @param asMap the underling data as a map
|
||||
*/
|
||||
record GrpcJavaConfig(Map<String, Object> asMap) {
|
||||
|
||||
GrpcJavaConfig() {
|
||||
this(new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
<T> Consumer<T> in(String key) {
|
||||
return (value) -> this.asMap.put(key, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+34
@@ -29,9 +29,11 @@ import io.grpc.netty.NettyChannelBuilder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
|
||||
import org.springframework.boot.grpc.client.autoconfigure.ServiceConfig.HealthCheckConfig;
|
||||
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -169,6 +171,38 @@ class GrpcChannelBuilderCustomizersTests {
|
||||
then(builder).should().defaultServiceConfig(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenHasServiceConfig() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channel = new Channel();
|
||||
ServiceConfig serviceConfig = new ServiceConfig(null, null, null, new HealthCheckConfig("test"));
|
||||
channel.setServiceConfig(serviceConfig);
|
||||
properties.getChannel().put("default", channel);
|
||||
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null,
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
|
||||
customizers.apply("target", builder);
|
||||
Map<String, Object> expected = new LinkedHashMap<>();
|
||||
expected.put("healthCheckConfig", Map.of("serviceName", "test"));
|
||||
then(builder).should().defaultServiceConfig(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenHasClashingServiceConfigAndHealth() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channel = new Channel();
|
||||
channel.getHealth().setEnabled(true);
|
||||
channel.getHealth().setServiceName("fromhealth");
|
||||
ServiceConfig serviceConfig = new ServiceConfig(null, null, null, new HealthCheckConfig("fromservice"));
|
||||
channel.setServiceConfig(serviceConfig);
|
||||
properties.getChannel().put("default", channel);
|
||||
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null,
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
|
||||
assertThatIllegalStateException().isThrownBy(() -> customizers.apply("target", builder))
|
||||
.withMessage("Unable to change health check config service name from 'fromservice' to 'fromhealth'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenHealthEnabledAndNoServiceNameAddsHealthConfig() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.grpc.LoadBalancerRegistry;
|
||||
import io.grpc.NameResolver.ConfigOrError;
|
||||
import io.grpc.Status.Code;
|
||||
import io.grpc.internal.AutoConfiguredLoadBalancerFactory;
|
||||
import io.grpc.internal.ScParser;
|
||||
import io.grpc.internal.ServiceConfigUtil;
|
||||
import io.grpc.internal.ServiceConfigUtil.LbConfig;
|
||||
import io.grpc.internal.ServiceConfigUtil.PolicySelection;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.context.properties.bind.BindException;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
|
||||
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;
|
||||
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link ServiceConfig}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ServiceConfigTests {
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- pickfirst: {}
|
||||
""")
|
||||
void pickFirstLoadBalancing() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("loadBalancingConfig");
|
||||
List<Map<String, ?>> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map);
|
||||
assertThat(loadBalancingConfigs).hasSize(1);
|
||||
assertThat(loadBalancingConfigs.get(0)).containsKey("pick_first");
|
||||
PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs);
|
||||
assertThat(loadBalancingPolicySelection.toString()).contains("PickFirstLoadBalancer");
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("shuffleAddressList").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- pickfirst:
|
||||
shuffle-address-list: true
|
||||
""")
|
||||
void pickFirstLoadBalancingWithProperties() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("loadBalancingConfig");
|
||||
List<Map<String, ?>> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map);
|
||||
assertThat(loadBalancingConfigs).hasSize(1);
|
||||
assertThat(loadBalancingConfigs.get(0)).containsKey("pick_first");
|
||||
PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs);
|
||||
assertThat(loadBalancingPolicySelection.toString()).contains("PickFirstLoadBalancer");
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("shuffleAddressList").isEqualTo(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- roundrobin: {}
|
||||
""")
|
||||
void roundRobinLoadBalancing() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("loadBalancingConfig");
|
||||
List<Map<String, ?>> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map);
|
||||
assertThat(loadBalancingConfigs).hasSize(1);
|
||||
assertThat(loadBalancingConfigs.get(0)).containsKey("round_robin");
|
||||
PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs);
|
||||
assertThat(loadBalancingPolicySelection.toString()).contains("policy=round_robin")
|
||||
.contains("no service config");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- weightedroundrobin: {}
|
||||
""")
|
||||
void weightedRoundRobinLoadBalancing() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("loadBalancingConfig");
|
||||
List<Map<String, ?>> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map);
|
||||
assertThat(loadBalancingConfigs).hasSize(1);
|
||||
assertThat(loadBalancingConfigs.get(0)).containsKey("weighted_round_robin");
|
||||
PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs);
|
||||
assertThat(loadBalancingPolicySelection.toString()).contains("WeightedRoundRobinLoadBalancerProvider");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- weightedroundrobin:
|
||||
blackout-period: 1m
|
||||
weight-expiration-period: 500ms
|
||||
out-of-band-reporting-period: 1s
|
||||
enable-out-of-band-load-report: true
|
||||
weight-update-period: 2s
|
||||
error-utilization-penalty: 0.5
|
||||
""")
|
||||
void weightedRoundRobinLoadBalancingWithProperties() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("loadBalancingConfig");
|
||||
List<Map<String, ?>> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map);
|
||||
assertThat(loadBalancingConfigs).hasSize(1);
|
||||
assertThat(loadBalancingConfigs.get(0)).containsKey("weighted_round_robin");
|
||||
PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs);
|
||||
assertThat(loadBalancingPolicySelection.toString()).contains("WeightedRoundRobinLoadBalancerProvider");
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("blackoutPeriodNanos")
|
||||
.isEqualTo(Duration.ofMinutes(1).toNanos());
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("weightExpirationPeriodNanos")
|
||||
.isEqualTo(Duration.ofMillis(500).toNanos());
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("enableOobLoadReport").isEqualTo(true);
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("oobReportingPeriodNanos")
|
||||
.isEqualTo(Duration.ofSeconds(1).toNanos());
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("weightUpdatePeriodNanos")
|
||||
.isEqualTo(Duration.ofSeconds(2).toNanos());
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("errorUtilizationPenalty").isEqualTo(0.5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- grpc:
|
||||
child:
|
||||
- roundrobin: {}
|
||||
- pickfirst: {}
|
||||
service-name: test
|
||||
initial-fallback-timeout: 10s
|
||||
""")
|
||||
void grpcLoadBalancingWithProperties() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("loadBalancingConfig");
|
||||
List<Map<String, ?>> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map);
|
||||
assertThat(loadBalancingConfigs).hasSize(1);
|
||||
assertThat(loadBalancingConfigs.get(0)).containsKey("grpclb");
|
||||
PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs);
|
||||
assertThat(loadBalancingPolicySelection.toString()).contains("GrpclbLoadBalancerProvider");
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("mode").hasToString("ROUND_ROBIN");
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("serviceName").isEqualTo("test");
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("fallbackTimeoutMs")
|
||||
.isEqualTo(Duration.ofSeconds(10).toMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- pickfirst: {}
|
||||
- weightedroundrobin: {}
|
||||
""")
|
||||
void multipleLoadBalancerPolicies() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("loadBalancingConfig");
|
||||
List<Map<String, ?>> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map);
|
||||
assertThat(loadBalancingConfigs).hasSize(2);
|
||||
assertThat(loadBalancingConfigs.get(0)).containsKey("pick_first");
|
||||
assertThat(loadBalancingConfigs.get(1)).containsKey("weighted_round_robin");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- pickfirst: {}
|
||||
weightedroundrobin: {}
|
||||
""")
|
||||
void whenMultileLoadBalancingPoliciesInListItemThrowsException() {
|
||||
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindAndGetAsMap())
|
||||
.havingRootCause()
|
||||
.isInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- {}
|
||||
""")
|
||||
void whenNoLoadBalancingPoliciesInListItemThrowsException() {
|
||||
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindAndGetAsMap())
|
||||
.havingRootCause()
|
||||
.isInstanceOf(InvalidConfigurationPropertyValueException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
method:
|
||||
- name:
|
||||
- service: s-one
|
||||
method: m-one
|
||||
- service: s-two
|
||||
method: m-two
|
||||
wait-for-ready: true
|
||||
max-request-message: 10KB
|
||||
max-response-message: 20KB
|
||||
timeout: 30s
|
||||
""")
|
||||
@SuppressWarnings("unchecked")
|
||||
void methodConfig() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("methodConfig");
|
||||
Map<String, ?> serviceMethodMap = getServiceMethodMap(map, false);
|
||||
assertThat(serviceMethodMap).containsOnlyKeys("s-one/m-one", "s-two/m-two");
|
||||
Object methodInfo = serviceMethodMap.get("s-one/m-one");
|
||||
assertThat(methodInfo).extracting("timeoutNanos").isEqualTo(Duration.ofSeconds(30).toNanos());
|
||||
assertThat(methodInfo).extracting("waitForReady").isEqualTo(Boolean.TRUE);
|
||||
assertThat(methodInfo).extracting("maxOutboundMessageSize").isEqualTo(10240);
|
||||
assertThat(methodInfo).extracting("maxInboundMessageSize").isEqualTo(20480);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
method:
|
||||
- name:
|
||||
- service: s-one
|
||||
method: m-one
|
||||
retry:
|
||||
max-attempts: 2
|
||||
initial-backoff: 1m
|
||||
max-backoff: 1h
|
||||
backoff-multiplier: 2.5
|
||||
per-attempt-receive-timeout: 2s
|
||||
retryable-status-codes:
|
||||
- cancelled
|
||||
- already-exists
|
||||
""")
|
||||
void methodConfigRetryPolicy() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
Map<String, ?> serviceMethodMap = getServiceMethodMap(map, true);
|
||||
Object methodInfo = serviceMethodMap.get("s-one/m-one");
|
||||
assertThat(methodInfo).extracting("retryPolicy.maxAttempts").isEqualTo(2);
|
||||
assertThat(methodInfo).extracting("retryPolicy.initialBackoffNanos").isEqualTo(Duration.ofMinutes(1).toNanos());
|
||||
assertThat(methodInfo).extracting("retryPolicy.maxBackoffNanos").isEqualTo(Duration.ofHours(1).toNanos());
|
||||
assertThat(methodInfo).extracting("retryPolicy.backoffMultiplier").isEqualTo(2.5);
|
||||
assertThat(methodInfo).extracting("retryPolicy.perAttemptRecvTimeoutNanos")
|
||||
.isEqualTo(Duration.ofSeconds(2).toNanos());
|
||||
assertThat(methodInfo).extracting("retryPolicy.retryableStatusCodes")
|
||||
.asInstanceOf(InstanceOfAssertFactories.SET)
|
||||
.containsExactlyInAnyOrder(Code.CANCELLED, Code.ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
method:
|
||||
- name:
|
||||
- service: s-one
|
||||
method: m-one
|
||||
hedging:
|
||||
max-attempts: 4
|
||||
delay: 6s
|
||||
non-fatal-status-codes:
|
||||
- invalid-argument
|
||||
- deadline-exceeded
|
||||
""")
|
||||
void methodConfigHedgingPolicy() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
Map<String, ?> serviceMethodMap = getServiceMethodMap(map, true);
|
||||
Object methodInfo = serviceMethodMap.get("s-one/m-one");
|
||||
assertThat(methodInfo).extracting("hedgingPolicy.maxAttempts").isEqualTo(4);
|
||||
assertThat(methodInfo).extracting("hedgingPolicy.hedgingDelayNanos").isEqualTo(Duration.ofSeconds(6).toNanos());
|
||||
assertThat(methodInfo).extracting("hedgingPolicy.nonFatalStatusCodes")
|
||||
.asInstanceOf(InstanceOfAssertFactories.SET)
|
||||
.containsExactlyInAnyOrder(Code.INVALID_ARGUMENT, Code.DEADLINE_EXCEEDED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
method:
|
||||
- name:
|
||||
- service: s-one
|
||||
method: m-one
|
||||
retry: {}
|
||||
hedging: {}
|
||||
""")
|
||||
void whenMultiplePoliciesInMethodConfigThrowsException() {
|
||||
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindAndGetAsMap())
|
||||
.havingRootCause()
|
||||
.isInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
retrythrottling:
|
||||
max-tokens: 2.5
|
||||
token-ratio: 1.5
|
||||
""")
|
||||
void retryThrottling() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("retryThrottling");
|
||||
Object throttle = ReflectionTestUtils.invokeMethod(ServiceConfigUtil.class, "getThrottlePolicy", map);
|
||||
assertThat(throttle).extracting("maxTokens").isEqualTo(2500);
|
||||
assertThat(throttle).extracting("tokenRatio").isEqualTo(1500);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
healthcheck:
|
||||
service-name: test
|
||||
""")
|
||||
@SuppressWarnings("unchecked")
|
||||
void healthCheck() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("healthCheckConfig");
|
||||
Map<String, Object> healthCheckedService = (Map<String, Object>) ServiceConfigUtil.getHealthCheckedService(map);
|
||||
assertThat(healthCheckedService).hasSize(1).containsEntry("serviceName", "test");
|
||||
}
|
||||
|
||||
private PolicySelection getLoadBalancingPolicySelection(List<Map<String, ?>> rawConfigs) {
|
||||
List<LbConfig> unwrappedConfigs = ServiceConfigUtil.unwrapLoadBalancingConfigList(rawConfigs);
|
||||
LoadBalancerRegistry registry = LoadBalancerRegistry.getDefaultRegistry();
|
||||
ConfigOrError selected = ServiceConfigUtil.selectLbPolicyFromList(unwrappedConfigs, registry);
|
||||
assertThat(selected).isNotNull();
|
||||
PolicySelection policySelection = (PolicySelection) selected.getConfig();
|
||||
if (policySelection == null) {
|
||||
System.err.println(selected);
|
||||
System.err.println(selected.getError());
|
||||
if (selected.getError() != null && selected.getError().asException() != null) {
|
||||
selected.getError().asException().printStackTrace();
|
||||
}
|
||||
}
|
||||
assertThat(policySelection).isNotNull();
|
||||
return policySelection;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, ?> getServiceMethodMap(Map<String, Object> map, boolean retryEnabled) {
|
||||
ScParser scParser = new ScParser(retryEnabled, 100, 100, new AutoConfiguredLoadBalancerFactory("pick_first"));
|
||||
Object config = scParser.parseServiceConfig(map).getConfig();
|
||||
assertThat(config).isNotNull();
|
||||
Object serviceMethodMap = ReflectionTestUtils.getField(config, "serviceMethodMap");
|
||||
assertThat(serviceMethodMap).isNotNull();
|
||||
return (Map<String, ?>) serviceMethodMap;
|
||||
}
|
||||
|
||||
private Map<String, Object> bindAndGetAsMap() throws Exception {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
bind().applyTo(map);
|
||||
return map;
|
||||
}
|
||||
|
||||
private ServiceConfig bind() throws Exception {
|
||||
YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
|
||||
PropertySource<?> propertySource = loader.load("config.yaml", new ClassPathResource("config.yaml")).get(0);
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
environment.getPropertySources().addLast(propertySource);
|
||||
Binder binder = Binder.get(environment);
|
||||
return binder.bind("config", ServiceConfig.class).get();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user