Browse Source
Update `GrpcClientProperties` to include type-safe service config properties commonly configured items. Closes gh-49540pull/46608/head
6 changed files with 908 additions and 4 deletions
@ -0,0 +1,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); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,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(); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue