blob: 294e9dbcf732dc9b9530fc1215dda6ec6e6b4979 [file] [log] [blame]
/*
* Copyright 2018 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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Verify.verify;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.VerifyException;
import io.grpc.LoadBalancerProvider;
import io.grpc.LoadBalancerRegistry;
import io.grpc.NameResolver.ConfigOrError;
import io.grpc.Status;
import io.grpc.internal.RetriableStream.Throttle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/**
* Helper utility to work with service configs.
*
* <p>This class contains helper methods to parse service config JSON values into Java types.
*/
public final class ServiceConfigUtil {
private ServiceConfigUtil() {}
/**
* Fetches the health-checked service config from service config. {@code null} if can't find one.
*/
@Nullable
public static Map<String, ?> getHealthCheckedService(@Nullable Map<String, ?> serviceConfig) {
if (serviceConfig == null) {
return null;
}
/* schema as follows
{
"healthCheckConfig": {
// Service name to use in the health-checking request.
"serviceName": string
}
}
*/
return JsonUtil.getObject(serviceConfig, "healthCheckConfig");
}
/**
* Fetches the health-checked service name from health-checked service config. {@code null} if
* can't find one.
*/
@Nullable
public static String getHealthCheckedServiceName(
@Nullable Map<String, ?> healthCheckedServiceConfig) {
if (healthCheckedServiceConfig == null) {
return null;
}
return JsonUtil.getString(healthCheckedServiceConfig, "serviceName");
}
@Nullable
static Throttle getThrottlePolicy(@Nullable Map<String, ?> serviceConfig) {
if (serviceConfig == null) {
return null;
}
/* schema as follows
{
"retryThrottling": {
// The number of tokens starts at maxTokens. The token_count will always be
// between 0 and maxTokens.
//
// This field is required and must be greater than zero.
"maxTokens": number,
// The amount of tokens to add on each successful RPC. Typically this will
// be some number between 0 and 1, e.g., 0.1.
//
// This field is required and must be greater than zero. Up to 3 decimal
// places are supported.
"tokenRatio": number
}
}
*/
Map<String, ?> throttling = JsonUtil.getObject(serviceConfig, "retryThrottling");
if (throttling == null) {
return null;
}
// TODO(dapengzhang0): check if this is null.
float maxTokens = JsonUtil.getNumber(throttling, "maxTokens").floatValue();
float tokenRatio = JsonUtil.getNumber(throttling, "tokenRatio").floatValue();
checkState(maxTokens > 0f, "maxToken should be greater than zero");
checkState(tokenRatio > 0f, "tokenRatio should be greater than zero");
return new Throttle(maxTokens, tokenRatio);
}
@Nullable
static Integer getMaxAttemptsFromRetryPolicy(Map<String, ?> retryPolicy) {
return JsonUtil.getNumberAsInteger(retryPolicy, "maxAttempts");
}
@Nullable
static Long getInitialBackoffNanosFromRetryPolicy(Map<String, ?> retryPolicy) {
return JsonUtil.getStringAsDuration(retryPolicy, "initialBackoff");
}
@Nullable
static Long getMaxBackoffNanosFromRetryPolicy(Map<String, ?> retryPolicy) {
return JsonUtil.getStringAsDuration(retryPolicy, "maxBackoff");
}
@Nullable
static Double getBackoffMultiplierFromRetryPolicy(Map<String, ?> retryPolicy) {
return JsonUtil.getNumber(retryPolicy, "backoffMultiplier");
}
@Nullable
static Long getPerAttemptRecvTimeoutNanosFromRetryPolicy(Map<String, ?> retryPolicy) {
return JsonUtil.getStringAsDuration(retryPolicy, "perAttemptRecvTimeout");
}
private static Set<Status.Code> getListOfStatusCodesAsSet(Map<String, ?> obj, String key) {
List<?> statuses = JsonUtil.getList(obj, key);
if (statuses == null) {
return null;
}
return getStatusCodesFromList(statuses);
}
private static Set<Status.Code> getStatusCodesFromList(List<?> statuses) {
EnumSet<Status.Code> codes = EnumSet.noneOf(Status.Code.class);
for (Object status : statuses) {
Status.Code code;
if (status instanceof Double) {
Double statusD = (Double) status;
int codeValue = statusD.intValue();
verify((double) codeValue == statusD, "Status code %s is not integral", status);
code = Status.fromCodeValue(codeValue).getCode();
verify(code.value() == statusD.intValue(), "Status code %s is not valid", status);
} else if (status instanceof String) {
try {
code = Status.Code.valueOf((String) status);
} catch (IllegalArgumentException iae) {
throw new VerifyException("Status code " + status + " is not valid", iae);
}
} else {
throw new VerifyException(
"Can not convert status code " + status + " to Status.Code, because its type is "
+ status.getClass());
}
codes.add(code);
}
return Collections.unmodifiableSet(codes);
}
static Set<Status.Code> getRetryableStatusCodesFromRetryPolicy(Map<String, ?> retryPolicy) {
String retryableStatusCodesKey = "retryableStatusCodes";
Set<Status.Code> codes = getListOfStatusCodesAsSet(retryPolicy, retryableStatusCodesKey);
verify(codes != null, "%s is required in retry policy", retryableStatusCodesKey);
verify(!codes.contains(Status.Code.OK), "%s must not contain OK", retryableStatusCodesKey);
return codes;
}
@Nullable
static Integer getMaxAttemptsFromHedgingPolicy(Map<String, ?> hedgingPolicy) {
return JsonUtil.getNumberAsInteger(hedgingPolicy, "maxAttempts");
}
@Nullable
static Long getHedgingDelayNanosFromHedgingPolicy(Map<String, ?> hedgingPolicy) {
return JsonUtil.getStringAsDuration(hedgingPolicy, "hedgingDelay");
}
static Set<Status.Code> getNonFatalStatusCodesFromHedgingPolicy(Map<String, ?> hedgingPolicy) {
String nonFatalStatusCodesKey = "nonFatalStatusCodes";
Set<Status.Code> codes = getListOfStatusCodesAsSet(hedgingPolicy, nonFatalStatusCodesKey);
if (codes == null) {
return Collections.unmodifiableSet(EnumSet.noneOf(Status.Code.class));
}
verify(!codes.contains(Status.Code.OK), "%s must not contain OK", nonFatalStatusCodesKey);
return codes;
}
@Nullable
static String getServiceFromName(Map<String, ?> name) {
return JsonUtil.getString(name, "service");
}
@Nullable
static String getMethodFromName(Map<String, ?> name) {
return JsonUtil.getString(name, "method");
}
@Nullable
static Map<String, ?> getRetryPolicyFromMethodConfig(Map<String, ?> methodConfig) {
return JsonUtil.getObject(methodConfig, "retryPolicy");
}
@Nullable
static Map<String, ?> getHedgingPolicyFromMethodConfig(Map<String, ?> methodConfig) {
return JsonUtil.getObject(methodConfig, "hedgingPolicy");
}
@Nullable
static List<Map<String, ?>> getNameListFromMethodConfig(
Map<String, ?> methodConfig) {
return JsonUtil.getListOfObjects(methodConfig, "name");
}
/**
* Returns the number of nanoseconds of timeout for the given method config.
*
* @return duration nanoseconds, or {@code null} if it isn't present.
*/
@Nullable
static Long getTimeoutFromMethodConfig(Map<String, ?> methodConfig) {
return JsonUtil.getStringAsDuration(methodConfig, "timeout");
}
@Nullable
static Boolean getWaitForReadyFromMethodConfig(Map<String, ?> methodConfig) {
return JsonUtil.getBoolean(methodConfig, "waitForReady");
}
@Nullable
static Integer getMaxRequestMessageBytesFromMethodConfig(Map<String, ?> methodConfig) {
return JsonUtil.getNumberAsInteger(methodConfig, "maxRequestMessageBytes");
}
@Nullable
static Integer getMaxResponseMessageBytesFromMethodConfig(Map<String, ?> methodConfig) {
return JsonUtil.getNumberAsInteger(methodConfig, "maxResponseMessageBytes");
}
@Nullable
static List<Map<String, ?>> getMethodConfigFromServiceConfig(
Map<String, ?> serviceConfig) {
return JsonUtil.getListOfObjects(serviceConfig, "methodConfig");
}
/**
* Extracts load balancing configs from a service config.
*/
@VisibleForTesting
public static List<Map<String, ?>> getLoadBalancingConfigsFromServiceConfig(
Map<String, ?> serviceConfig) {
/* schema as follows
{
"loadBalancingConfig": [
{"xds" :
{
"childPolicy": [...],
"fallbackPolicy": [...],
}
},
{"round_robin": {}}
],
"loadBalancingPolicy": "ROUND_ROBIN" // The deprecated policy key
}
*/
List<Map<String, ?>> lbConfigs = new ArrayList<>();
String loadBalancingConfigKey = "loadBalancingConfig";
if (serviceConfig.containsKey(loadBalancingConfigKey)) {
lbConfigs.addAll(JsonUtil.getListOfObjects(
serviceConfig, loadBalancingConfigKey));
}
if (lbConfigs.isEmpty()) {
// No LoadBalancingConfig found. Fall back to the deprecated LoadBalancingPolicy
String policy = JsonUtil.getString(serviceConfig, "loadBalancingPolicy");
if (policy != null) {
// Convert the policy to a config, so that the caller can handle them in the same way.
policy = policy.toLowerCase(Locale.ROOT);
Map<String, ?> fakeConfig = Collections.singletonMap(policy, Collections.emptyMap());
lbConfigs.add(fakeConfig);
}
}
return Collections.unmodifiableList(lbConfigs);
}
/**
* Unwrap a LoadBalancingConfig JSON object into a {@link LbConfig}. The input is a JSON object
* (map) with exactly one entry, where the key is the policy name and the value is a config object
* for that policy.
*/
public static LbConfig unwrapLoadBalancingConfig(Map<String, ?> lbConfig) {
if (lbConfig.size() != 1) {
throw new RuntimeException(
"There are " + lbConfig.size() + " fields in a LoadBalancingConfig object. Exactly one"
+ " is expected. Config=" + lbConfig);
}
String key = lbConfig.entrySet().iterator().next().getKey();
return new LbConfig(key, JsonUtil.getObject(lbConfig, key));
}
/**
* Given a JSON list of LoadBalancingConfigs, and convert it into a list of LbConfig.
*/
public static List<LbConfig> unwrapLoadBalancingConfigList(List<Map<String, ?>> list) {
if (list == null) {
return null;
}
ArrayList<LbConfig> result = new ArrayList<>();
for (Map<String, ?> rawChildPolicy : list) {
result.add(unwrapLoadBalancingConfig(rawChildPolicy));
}
return Collections.unmodifiableList(result);
}
/**
* Parses and selects a load balancing policy from a non-empty list of raw configs. If selection
* is successful, the returned ConfigOrError object will include a {@link
* ServiceConfigUtil.PolicySelection} as its config value.
*/
public static ConfigOrError selectLbPolicyFromList(
List<LbConfig> lbConfigs, LoadBalancerRegistry lbRegistry) {
List<String> policiesTried = new ArrayList<>();
for (LbConfig lbConfig : lbConfigs) {
String policy = lbConfig.getPolicyName();
LoadBalancerProvider provider = lbRegistry.getProvider(policy);
if (provider == null) {
policiesTried.add(policy);
} else {
if (!policiesTried.isEmpty()) {
Logger.getLogger(ServiceConfigUtil.class.getName()).log(
Level.FINEST,
"{0} specified by Service Config are not available", policiesTried);
}
ConfigOrError parsedLbPolicyConfig =
provider.parseLoadBalancingPolicyConfig(lbConfig.getRawConfigValue());
if (parsedLbPolicyConfig.getError() != null) {
return parsedLbPolicyConfig;
}
return ConfigOrError.fromConfig(
new PolicySelection(provider, parsedLbPolicyConfig.getConfig()));
}
}
return ConfigOrError.fromError(
Status.UNKNOWN.withDescription(
"None of " + policiesTried + " specified by Service Config are available."));
}
/**
* A LoadBalancingConfig that includes the policy name (the key) and its raw config value (parsed
* JSON).
*/
public static final class LbConfig {
private final String policyName;
private final Map<String, ?> rawConfigValue;
public LbConfig(String policyName, Map<String, ?> rawConfigValue) {
this.policyName = checkNotNull(policyName, "policyName");
this.rawConfigValue = checkNotNull(rawConfigValue, "rawConfigValue");
}
public String getPolicyName() {
return policyName;
}
public Map<String, ?> getRawConfigValue() {
return rawConfigValue;
}
@Override
public boolean equals(Object o) {
if (o instanceof LbConfig) {
LbConfig other = (LbConfig) o;
return policyName.equals(other.policyName)
&& rawConfigValue.equals(other.rawConfigValue);
}
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(policyName, rawConfigValue);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("policyName", policyName)
.add("rawConfigValue", rawConfigValue)
.toString();
}
}
public static final class PolicySelection {
final LoadBalancerProvider provider;
@Nullable
final Object config;
/** Constructs a PolicySelection with selected LB provider and the deeply parsed LB config. */
public PolicySelection(
LoadBalancerProvider provider,
@Nullable Object config) {
this.provider = checkNotNull(provider, "provider");
this.config = config;
}
public LoadBalancerProvider getProvider() {
return provider;
}
@Nullable
public Object getConfig() {
return config;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PolicySelection that = (PolicySelection) o;
return Objects.equal(provider, that.provider)
&& Objects.equal(config, that.config);
}
@Override
public int hashCode() {
return Objects.hashCode(provider, config);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("provider", provider)
.add("config", config)
.toString();
}
}
}