blob: e5512dc8df4c5997c7aa2a70c00032602e9b5050 [file] [log] [blame]
/*
* Copyright 2019 The gRPC 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
*
* http://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 io.grpc.internal;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import io.grpc.CallOptions;
import io.grpc.InternalConfigSelector;
import io.grpc.LoadBalancer.PickSubchannelArgs;
import io.grpc.MethodDescriptor;
import io.grpc.Status.Code;
import io.grpc.internal.RetriableStream.Throttle;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* {@link ManagedChannelServiceConfig} is a fully parsed and validated representation of service
* configuration data.
*/
final class ManagedChannelServiceConfig {
@Nullable
private final MethodInfo defaultMethodConfig;
private final Map<String, MethodInfo> serviceMethodMap;
private final Map<String, MethodInfo> serviceMap;
@Nullable
private final Throttle retryThrottling;
@Nullable
private final Object loadBalancingConfig;
@Nullable
private final Map<String, ?> healthCheckingConfig;
ManagedChannelServiceConfig(
@Nullable MethodInfo defaultMethodConfig,
Map<String, MethodInfo> serviceMethodMap,
Map<String, MethodInfo> serviceMap,
@Nullable Throttle retryThrottling,
@Nullable Object loadBalancingConfig,
@Nullable Map<String, ?> healthCheckingConfig) {
this.defaultMethodConfig = defaultMethodConfig;
this.serviceMethodMap = Collections.unmodifiableMap(new HashMap<>(serviceMethodMap));
this.serviceMap = Collections.unmodifiableMap(new HashMap<>(serviceMap));
this.retryThrottling = retryThrottling;
this.loadBalancingConfig = loadBalancingConfig;
this.healthCheckingConfig =
healthCheckingConfig != null
? Collections.unmodifiableMap(new HashMap<>(healthCheckingConfig))
: null;
}
/** Returns an empty {@link ManagedChannelServiceConfig}. */
static ManagedChannelServiceConfig empty() {
return
new ManagedChannelServiceConfig(
null,
new HashMap<String, MethodInfo>(),
new HashMap<String, MethodInfo>(),
/* retryThrottling= */ null,
/* loadBalancingConfig= */ null,
/* healthCheckingConfig= */ null);
}
/**
* Parses the Channel level config values (e.g. excludes load balancing)
*/
static ManagedChannelServiceConfig fromServiceConfig(
Map<String, ?> serviceConfig,
boolean retryEnabled,
int maxRetryAttemptsLimit,
int maxHedgedAttemptsLimit,
@Nullable Object loadBalancingConfig) {
Throttle retryThrottling = null;
if (retryEnabled) {
retryThrottling = ServiceConfigUtil.getThrottlePolicy(serviceConfig);
}
Map<String, MethodInfo> serviceMethodMap = new HashMap<>();
Map<String, MethodInfo> serviceMap = new HashMap<>();
Map<String, ?> healthCheckingConfig =
ServiceConfigUtil.getHealthCheckedService(serviceConfig);
// Try and do as much validation here before we swap out the existing configuration. In case
// the input is invalid, we don't want to lose the existing configuration.
List<Map<String, ?>> methodConfigs =
ServiceConfigUtil.getMethodConfigFromServiceConfig(serviceConfig);
if (methodConfigs == null) {
// this is surprising, but possible.
return
new ManagedChannelServiceConfig(
null,
serviceMethodMap,
serviceMap,
retryThrottling,
loadBalancingConfig,
healthCheckingConfig);
}
MethodInfo defaultMethodConfig = null;
for (Map<String, ?> methodConfig : methodConfigs) {
MethodInfo info = new MethodInfo(
methodConfig, retryEnabled, maxRetryAttemptsLimit, maxHedgedAttemptsLimit);
List<Map<String, ?>> nameList =
ServiceConfigUtil.getNameListFromMethodConfig(methodConfig);
if (nameList == null || nameList.isEmpty()) {
continue;
}
for (Map<String, ?> name : nameList) {
String serviceName = ServiceConfigUtil.getServiceFromName(name);
String methodName = ServiceConfigUtil.getMethodFromName(name);
if (Strings.isNullOrEmpty(serviceName)) {
checkArgument(
Strings.isNullOrEmpty(methodName), "missing service name for method %s", methodName);
checkArgument(
defaultMethodConfig == null,
"Duplicate default method config in service config %s",
serviceConfig);
defaultMethodConfig = info;
} else if (Strings.isNullOrEmpty(methodName)) {
// Service scoped config
checkArgument(
!serviceMap.containsKey(serviceName), "Duplicate service %s", serviceName);
serviceMap.put(serviceName, info);
} else {
// Method scoped config
String fullMethodName = MethodDescriptor.generateFullMethodName(serviceName, methodName);
checkArgument(
!serviceMethodMap.containsKey(fullMethodName),
"Duplicate method name %s",
fullMethodName);
serviceMethodMap.put(fullMethodName, info);
}
}
}
return
new ManagedChannelServiceConfig(
defaultMethodConfig,
serviceMethodMap,
serviceMap,
retryThrottling,
loadBalancingConfig,
healthCheckingConfig);
}
@Nullable
Map<String, ?> getHealthCheckingConfig() {
return healthCheckingConfig;
}
/**
* Used as a fallback per-RPC config supplier when the attributes value of {@link
* InternalConfigSelector#KEY} is not available. Returns {@code null} if there is no method
* config in this service config.
*/
@Nullable
InternalConfigSelector getDefaultConfigSelector() {
if (serviceMap.isEmpty() && serviceMethodMap.isEmpty() && defaultMethodConfig == null) {
return null;
}
return new ServiceConfigConvertedSelector(this);
}
@VisibleForTesting
@Nullable
Object getLoadBalancingConfig() {
return loadBalancingConfig;
}
@Nullable
Throttle getRetryThrottling() {
return retryThrottling;
}
@Nullable
MethodInfo getMethodConfig(MethodDescriptor<?, ?> method) {
MethodInfo methodInfo = serviceMethodMap.get(method.getFullMethodName());
if (methodInfo == null) {
String serviceName = method.getServiceName();
methodInfo = serviceMap.get(serviceName);
}
if (methodInfo == null) {
methodInfo = defaultMethodConfig;
}
return methodInfo;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ManagedChannelServiceConfig that = (ManagedChannelServiceConfig) o;
return Objects.equal(defaultMethodConfig, that.defaultMethodConfig)
&& Objects.equal(serviceMethodMap, that.serviceMethodMap)
&& Objects.equal(serviceMap, that.serviceMap)
&& Objects.equal(retryThrottling, that.retryThrottling)
&& Objects.equal(loadBalancingConfig, that.loadBalancingConfig);
}
@Override
public int hashCode() {
return Objects.hashCode(
defaultMethodConfig, serviceMethodMap, serviceMap, retryThrottling, loadBalancingConfig);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("defaultMethodConfig", defaultMethodConfig)
.add("serviceMethodMap", serviceMethodMap)
.add("serviceMap", serviceMap)
.add("retryThrottling", retryThrottling)
.add("loadBalancingConfig", loadBalancingConfig)
.toString();
}
/**
* Equivalent of MethodConfig from a ServiceConfig with restrictions from Channel setting.
*/
static final class MethodInfo {
static final CallOptions.Key<MethodInfo> KEY =
CallOptions.Key.create("io.grpc.internal.ManagedChannelServiceConfig.MethodInfo");
// TODO(carl-mastrangelo): add getters for these fields and make them private.
final Long timeoutNanos;
final Boolean waitForReady;
final Integer maxInboundMessageSize;
final Integer maxOutboundMessageSize;
final RetryPolicy retryPolicy;
final HedgingPolicy hedgingPolicy;
/**
* Constructor.
*
* @param retryEnabled when false, the argument maxRetryAttemptsLimit will have no effect.
*/
MethodInfo(
Map<String, ?> methodConfig, boolean retryEnabled, int maxRetryAttemptsLimit,
int maxHedgedAttemptsLimit) {
timeoutNanos = ServiceConfigUtil.getTimeoutFromMethodConfig(methodConfig);
waitForReady = ServiceConfigUtil.getWaitForReadyFromMethodConfig(methodConfig);
maxInboundMessageSize =
ServiceConfigUtil.getMaxResponseMessageBytesFromMethodConfig(methodConfig);
if (maxInboundMessageSize != null) {
checkArgument(
maxInboundMessageSize >= 0,
"maxInboundMessageSize %s exceeds bounds", maxInboundMessageSize);
}
maxOutboundMessageSize =
ServiceConfigUtil.getMaxRequestMessageBytesFromMethodConfig(methodConfig);
if (maxOutboundMessageSize != null) {
checkArgument(
maxOutboundMessageSize >= 0,
"maxOutboundMessageSize %s exceeds bounds", maxOutboundMessageSize);
}
Map<String, ?> retryPolicyMap =
retryEnabled ? ServiceConfigUtil.getRetryPolicyFromMethodConfig(methodConfig) : null;
retryPolicy = retryPolicyMap == null
? null : retryPolicy(retryPolicyMap, maxRetryAttemptsLimit);
Map<String, ?> hedgingPolicyMap =
retryEnabled ? ServiceConfigUtil.getHedgingPolicyFromMethodConfig(methodConfig) : null;
hedgingPolicy = hedgingPolicyMap == null
? null : hedgingPolicy(hedgingPolicyMap, maxHedgedAttemptsLimit);
}
@Override
public int hashCode() {
return Objects.hashCode(
timeoutNanos,
waitForReady,
maxInboundMessageSize,
maxOutboundMessageSize,
retryPolicy,
hedgingPolicy);
}
@Override
public boolean equals(Object other) {
if (!(other instanceof MethodInfo)) {
return false;
}
MethodInfo that = (MethodInfo) other;
return Objects.equal(this.timeoutNanos, that.timeoutNanos)
&& Objects.equal(this.waitForReady, that.waitForReady)
&& Objects.equal(this.maxInboundMessageSize, that.maxInboundMessageSize)
&& Objects.equal(this.maxOutboundMessageSize, that.maxOutboundMessageSize)
&& Objects.equal(this.retryPolicy, that.retryPolicy)
&& Objects.equal(this.hedgingPolicy, that.hedgingPolicy);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("timeoutNanos", timeoutNanos)
.add("waitForReady", waitForReady)
.add("maxInboundMessageSize", maxInboundMessageSize)
.add("maxOutboundMessageSize", maxOutboundMessageSize)
.add("retryPolicy", retryPolicy)
.add("hedgingPolicy", hedgingPolicy)
.toString();
}
private static RetryPolicy retryPolicy(Map<String, ?> retryPolicy, int maxAttemptsLimit) {
int maxAttempts = checkNotNull(
ServiceConfigUtil.getMaxAttemptsFromRetryPolicy(retryPolicy),
"maxAttempts cannot be empty");
checkArgument(maxAttempts >= 2, "maxAttempts must be greater than 1: %s", maxAttempts);
maxAttempts = Math.min(maxAttempts, maxAttemptsLimit);
long initialBackoffNanos = checkNotNull(
ServiceConfigUtil.getInitialBackoffNanosFromRetryPolicy(retryPolicy),
"initialBackoff cannot be empty");
checkArgument(
initialBackoffNanos > 0,
"initialBackoffNanos must be greater than 0: %s",
initialBackoffNanos);
long maxBackoffNanos = checkNotNull(
ServiceConfigUtil.getMaxBackoffNanosFromRetryPolicy(retryPolicy),
"maxBackoff cannot be empty");
checkArgument(
maxBackoffNanos > 0, "maxBackoff must be greater than 0: %s", maxBackoffNanos);
double backoffMultiplier = checkNotNull(
ServiceConfigUtil.getBackoffMultiplierFromRetryPolicy(retryPolicy),
"backoffMultiplier cannot be empty");
checkArgument(
backoffMultiplier > 0,
"backoffMultiplier must be greater than 0: %s",
backoffMultiplier);
Long perAttemptRecvTimeout =
ServiceConfigUtil.getPerAttemptRecvTimeoutNanosFromRetryPolicy(retryPolicy);
checkArgument(
perAttemptRecvTimeout == null || perAttemptRecvTimeout >= 0,
"perAttemptRecvTimeout cannot be negative: %s",
perAttemptRecvTimeout);
Set<Code> retryableCodes =
ServiceConfigUtil.getRetryableStatusCodesFromRetryPolicy(retryPolicy);
checkArgument(
perAttemptRecvTimeout != null || !retryableCodes.isEmpty(),
"retryableStatusCodes cannot be empty without perAttemptRecvTimeout");
return new RetryPolicy(
maxAttempts, initialBackoffNanos, maxBackoffNanos, backoffMultiplier,
perAttemptRecvTimeout, retryableCodes);
}
private static HedgingPolicy hedgingPolicy(
Map<String, ?> hedgingPolicy, int maxAttemptsLimit) {
int maxAttempts = checkNotNull(
ServiceConfigUtil.getMaxAttemptsFromHedgingPolicy(hedgingPolicy),
"maxAttempts cannot be empty");
checkArgument(maxAttempts >= 2, "maxAttempts must be greater than 1: %s", maxAttempts);
maxAttempts = Math.min(maxAttempts, maxAttemptsLimit);
long hedgingDelayNanos = checkNotNull(
ServiceConfigUtil.getHedgingDelayNanosFromHedgingPolicy(hedgingPolicy),
"hedgingDelay cannot be empty");
checkArgument(
hedgingDelayNanos >= 0, "hedgingDelay must not be negative: %s", hedgingDelayNanos);
return new HedgingPolicy(
maxAttempts, hedgingDelayNanos,
ServiceConfigUtil.getNonFatalStatusCodesFromHedgingPolicy(hedgingPolicy));
}
}
static final class ServiceConfigConvertedSelector extends InternalConfigSelector {
final ManagedChannelServiceConfig config;
/** Converts the service config to config selector. */
private ServiceConfigConvertedSelector(ManagedChannelServiceConfig config) {
this.config = config;
}
@Override
public Result selectConfig(PickSubchannelArgs args) {
return Result.newBuilder()
.setConfig(config)
.build();
}
}
}