All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.grpc.internal.ServiceConfigUtil Maven / Gradle / Ivy

/*
 * 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 static com.google.common.math.LongMath.checkedAdd;

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.Status;
import io.grpc.internal.RetriableStream.Throttle;
import java.text.ParseException;
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.concurrent.TimeUnit;
import javax.annotation.Nullable;

/**
 * Helper utility to work with service configs.
 *
 * 

This class contains helper methods to parse service config JSON values into Java types. */ public final class ServiceConfigUtil { private static final String SERVICE_CONFIG_METHOD_CONFIG_KEY = "methodConfig"; private static final String SERVICE_CONFIG_LOAD_BALANCING_POLICY_KEY = "loadBalancingPolicy"; private static final String SERVICE_CONFIG_LOAD_BALANCING_CONFIG_KEY = "loadBalancingConfig"; // TODO(chengyuanzhang): delete this key after shifting to use bootstrap. private static final String XDS_CONFIG_BALANCER_NAME_KEY = "balancerName"; private static final String XDS_CONFIG_CHILD_POLICY_KEY = "childPolicy"; private static final String XDS_CONFIG_FALLBACK_POLICY_KEY = "fallbackPolicy"; private static final String XDS_CONFIG_EDS_SERVICE_NAME = "edsServiceName"; private static final String XDS_CONFIG_LRS_SERVER_NAME = "lrsLoadReportingServerName"; private static final String SERVICE_CONFIG_STICKINESS_METADATA_KEY = "stickinessMetadataKey"; private static final String METHOD_CONFIG_NAME_KEY = "name"; private static final String METHOD_CONFIG_TIMEOUT_KEY = "timeout"; private static final String METHOD_CONFIG_WAIT_FOR_READY_KEY = "waitForReady"; private static final String METHOD_CONFIG_MAX_REQUEST_MESSAGE_BYTES_KEY = "maxRequestMessageBytes"; private static final String METHOD_CONFIG_MAX_RESPONSE_MESSAGE_BYTES_KEY = "maxResponseMessageBytes"; private static final String METHOD_CONFIG_RETRY_POLICY_KEY = "retryPolicy"; private static final String METHOD_CONFIG_HEDGING_POLICY_KEY = "hedgingPolicy"; private static final String NAME_SERVICE_KEY = "service"; private static final String NAME_METHOD_KEY = "method"; private static final String RETRY_POLICY_MAX_ATTEMPTS_KEY = "maxAttempts"; private static final String RETRY_POLICY_INITIAL_BACKOFF_KEY = "initialBackoff"; private static final String RETRY_POLICY_MAX_BACKOFF_KEY = "maxBackoff"; private static final String RETRY_POLICY_BACKOFF_MULTIPLIER_KEY = "backoffMultiplier"; private static final String RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY = "retryableStatusCodes"; private static final String HEDGING_POLICY_MAX_ATTEMPTS_KEY = "maxAttempts"; private static final String HEDGING_POLICY_HEDGING_DELAY_KEY = "hedgingDelay"; private static final String HEDGING_POLICY_NON_FATAL_STATUS_CODES_KEY = "nonFatalStatusCodes"; private static final long DURATION_SECONDS_MIN = -315576000000L; private static final long DURATION_SECONDS_MAX = 315576000000L; private ServiceConfigUtil() {} /** * Fetch the health-checked service name from service config. {@code null} if can't find one. */ @Nullable public static String getHealthCheckedServiceName(@Nullable Map serviceConfig) { String healthCheckKey = "healthCheckConfig"; String serviceNameKey = "serviceName"; if (serviceConfig == null || !serviceConfig.containsKey(healthCheckKey)) { return null; } /* schema as follows { "healthCheckConfig": { // Service name to use in the health-checking request. "serviceName": string } } */ Map healthCheck = JsonUtil.getObject(serviceConfig, healthCheckKey); if (!healthCheck.containsKey(serviceNameKey)) { return null; } return JsonUtil.getString(healthCheck, "serviceName"); } @Nullable static Throttle getThrottlePolicy(@Nullable Map serviceConfig) { String retryThrottlingKey = "retryThrottling"; if (serviceConfig == null || !serviceConfig.containsKey(retryThrottlingKey)) { 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 throttling = JsonUtil.getObject(serviceConfig, retryThrottlingKey); // TODO(dapengzhang0): check if this is null. float maxTokens = JsonUtil.getDouble(throttling, "maxTokens").floatValue(); float tokenRatio = JsonUtil.getDouble(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 retryPolicy) { if (!retryPolicy.containsKey(RETRY_POLICY_MAX_ATTEMPTS_KEY)) { return null; } return JsonUtil.getDouble(retryPolicy, RETRY_POLICY_MAX_ATTEMPTS_KEY).intValue(); } @Nullable static Long getInitialBackoffNanosFromRetryPolicy(Map retryPolicy) { if (!retryPolicy.containsKey(RETRY_POLICY_INITIAL_BACKOFF_KEY)) { return null; } String rawInitialBackoff = JsonUtil.getString(retryPolicy, RETRY_POLICY_INITIAL_BACKOFF_KEY); try { return parseDuration(rawInitialBackoff); } catch (ParseException e) { throw new RuntimeException(e); } } @Nullable static Long getMaxBackoffNanosFromRetryPolicy(Map retryPolicy) { if (!retryPolicy.containsKey(RETRY_POLICY_MAX_BACKOFF_KEY)) { return null; } String rawMaxBackoff = JsonUtil.getString(retryPolicy, RETRY_POLICY_MAX_BACKOFF_KEY); try { return parseDuration(rawMaxBackoff); } catch (ParseException e) { throw new RuntimeException(e); } } @Nullable static Double getBackoffMultiplierFromRetryPolicy(Map retryPolicy) { if (!retryPolicy.containsKey(RETRY_POLICY_BACKOFF_MULTIPLIER_KEY)) { return null; } return JsonUtil.getDouble(retryPolicy, RETRY_POLICY_BACKOFF_MULTIPLIER_KEY); } private static Set getStatusCodesFromList(List statuses) { EnumSet 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 getRetryableStatusCodesFromRetryPolicy(Map retryPolicy) { verify( retryPolicy.containsKey(RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY), "%s is required in retry policy", RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY); Set codes = getStatusCodesFromList( JsonUtil.getList(retryPolicy, RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY)); verify(!codes.isEmpty(), "%s must not be empty", RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY); verify( !codes.contains(Status.Code.OK), "%s must not contain OK", RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY); return codes; } @Nullable static Integer getMaxAttemptsFromHedgingPolicy(Map hedgingPolicy) { if (!hedgingPolicy.containsKey(HEDGING_POLICY_MAX_ATTEMPTS_KEY)) { return null; } return JsonUtil.getDouble(hedgingPolicy, HEDGING_POLICY_MAX_ATTEMPTS_KEY).intValue(); } @Nullable static Long getHedgingDelayNanosFromHedgingPolicy(Map hedgingPolicy) { if (!hedgingPolicy.containsKey(HEDGING_POLICY_HEDGING_DELAY_KEY)) { return null; } String rawHedgingDelay = JsonUtil.getString(hedgingPolicy, HEDGING_POLICY_HEDGING_DELAY_KEY); try { return parseDuration(rawHedgingDelay); } catch (ParseException e) { throw new RuntimeException(e); } } static Set getNonFatalStatusCodesFromHedgingPolicy(Map hedgingPolicy) { if (!hedgingPolicy.containsKey(HEDGING_POLICY_NON_FATAL_STATUS_CODES_KEY)) { return Collections.unmodifiableSet(EnumSet.noneOf(Status.Code.class)); } Set codes = getStatusCodesFromList( JsonUtil.getList(hedgingPolicy, HEDGING_POLICY_NON_FATAL_STATUS_CODES_KEY)); verify( !codes.contains(Status.Code.OK), "%s must not contain OK", HEDGING_POLICY_NON_FATAL_STATUS_CODES_KEY); return codes; } @Nullable static String getServiceFromName(Map name) { if (!name.containsKey(NAME_SERVICE_KEY)) { return null; } return JsonUtil.getString(name, NAME_SERVICE_KEY); } @Nullable static String getMethodFromName(Map name) { if (!name.containsKey(NAME_METHOD_KEY)) { return null; } return JsonUtil.getString(name, NAME_METHOD_KEY); } @Nullable static Map getRetryPolicyFromMethodConfig(Map methodConfig) { if (!methodConfig.containsKey(METHOD_CONFIG_RETRY_POLICY_KEY)) { return null; } return JsonUtil.getObject(methodConfig, METHOD_CONFIG_RETRY_POLICY_KEY); } @Nullable static Map getHedgingPolicyFromMethodConfig(Map methodConfig) { if (!methodConfig.containsKey(METHOD_CONFIG_HEDGING_POLICY_KEY)) { return null; } return JsonUtil.getObject(methodConfig, METHOD_CONFIG_HEDGING_POLICY_KEY); } @Nullable static List> getNameListFromMethodConfig( Map methodConfig) { if (!methodConfig.containsKey(METHOD_CONFIG_NAME_KEY)) { return null; } return JsonUtil.checkObjectList(JsonUtil.getList(methodConfig, METHOD_CONFIG_NAME_KEY)); } /** * 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 methodConfig) { if (!methodConfig.containsKey(METHOD_CONFIG_TIMEOUT_KEY)) { return null; } String rawTimeout = JsonUtil.getString(methodConfig, METHOD_CONFIG_TIMEOUT_KEY); try { return parseDuration(rawTimeout); } catch (ParseException e) { throw new RuntimeException(e); } } @Nullable static Boolean getWaitForReadyFromMethodConfig(Map methodConfig) { if (!methodConfig.containsKey(METHOD_CONFIG_WAIT_FOR_READY_KEY)) { return null; } return JsonUtil.getBoolean(methodConfig, METHOD_CONFIG_WAIT_FOR_READY_KEY); } @Nullable static Integer getMaxRequestMessageBytesFromMethodConfig(Map methodConfig) { if (!methodConfig.containsKey(METHOD_CONFIG_MAX_REQUEST_MESSAGE_BYTES_KEY)) { return null; } return JsonUtil.getDouble(methodConfig, METHOD_CONFIG_MAX_REQUEST_MESSAGE_BYTES_KEY).intValue(); } @Nullable static Integer getMaxResponseMessageBytesFromMethodConfig(Map methodConfig) { if (!methodConfig.containsKey(METHOD_CONFIG_MAX_RESPONSE_MESSAGE_BYTES_KEY)) { return null; } return JsonUtil.getDouble(methodConfig, METHOD_CONFIG_MAX_RESPONSE_MESSAGE_BYTES_KEY) .intValue(); } @Nullable static List> getMethodConfigFromServiceConfig( Map serviceConfig) { if (!serviceConfig.containsKey(SERVICE_CONFIG_METHOD_CONFIG_KEY)) { return null; } return JsonUtil .checkObjectList(JsonUtil.getList(serviceConfig, SERVICE_CONFIG_METHOD_CONFIG_KEY)); } /** * Extracts load balancing configs from a service config. */ @VisibleForTesting public static List> getLoadBalancingConfigsFromServiceConfig( Map serviceConfig) { /* schema as follows { "loadBalancingConfig": [ {"xds" : { "balancerName": "balancer1", "childPolicy": [...], "fallbackPolicy": [...], } }, {"round_robin": {}} ], "loadBalancingPolicy": "ROUND_ROBIN" // The deprecated policy key } */ List> lbConfigs = new ArrayList<>(); if (serviceConfig.containsKey(SERVICE_CONFIG_LOAD_BALANCING_CONFIG_KEY)) { List configs = JsonUtil.getList(serviceConfig, SERVICE_CONFIG_LOAD_BALANCING_CONFIG_KEY); for (Map config : JsonUtil.checkObjectList(configs)) { lbConfigs.add(config); } } if (lbConfigs.isEmpty()) { // No LoadBalancingConfig found. Fall back to the deprecated LoadBalancingPolicy if (serviceConfig.containsKey(SERVICE_CONFIG_LOAD_BALANCING_POLICY_KEY)) { // TODO(zhangkun83): check if this is null. String policy = JsonUtil.getString(serviceConfig, SERVICE_CONFIG_LOAD_BALANCING_POLICY_KEY); // Convert the policy to a config, so that the caller can handle them in the same way. policy = policy.toLowerCase(Locale.ROOT); Map 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 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 unwrapLoadBalancingConfigList(List> list) { ArrayList result = new ArrayList<>(); for (Map rawChildPolicy : list) { result.add(unwrapLoadBalancingConfig(rawChildPolicy)); } return Collections.unmodifiableList(result); } /** * Extracts the loadbalancer name from xds loadbalancer config. */ // TODO(chengyuanzhang): delete after shifting to use bootstrap. public static String getBalancerNameFromXdsConfig(Map rawXdsConfig) { return JsonUtil.getString(rawXdsConfig, XDS_CONFIG_BALANCER_NAME_KEY); } /** * Extract the server name to use in EDS query. */ @Nullable public static String getEdsServiceNameFromXdsConfig(Map rawXdsConfig) { return JsonUtil.getString(rawXdsConfig, XDS_CONFIG_EDS_SERVICE_NAME); } /** * Extract the LRS server name to send load reports to. */ @Nullable public static String getLrsServerNameFromXdsConfig(Map rawXdsConfig) { return JsonUtil.getString(rawXdsConfig, XDS_CONFIG_LRS_SERVER_NAME); } /** * Extracts list of child policies from xds loadbalancer config. */ @Nullable public static List getChildPolicyFromXdsConfig(Map rawXdsConfig) { List rawChildPolicies = JsonUtil.getList(rawXdsConfig, XDS_CONFIG_CHILD_POLICY_KEY); if (rawChildPolicies != null) { return unwrapLoadBalancingConfigList(JsonUtil.checkObjectList(rawChildPolicies)); } return null; } /** * Extracts list of fallback policies from xds loadbalancer config. */ @Nullable public static List getFallbackPolicyFromXdsConfig(Map rawXdsConfig) { List rawFallbackPolicies = JsonUtil.getList(rawXdsConfig, XDS_CONFIG_FALLBACK_POLICY_KEY); if (rawFallbackPolicies != null) { return unwrapLoadBalancingConfigList(JsonUtil.checkObjectList(rawFallbackPolicies)); } return null; } /** * Extracts the stickiness metadata key from a service config, or {@code null}. */ @Nullable public static String getStickinessMetadataKeyFromServiceConfig( Map serviceConfig) { if (!serviceConfig.containsKey(SERVICE_CONFIG_STICKINESS_METADATA_KEY)) { return null; } return JsonUtil.getString(serviceConfig, SERVICE_CONFIG_STICKINESS_METADATA_KEY); } /** * Parse from a string to produce a duration. Copy of * {@link com.google.protobuf.util.Durations#parse}. * * @return A Duration parsed from the string. * @throws ParseException if parsing fails. */ private static long parseDuration(String value) throws ParseException { // Must ended with "s". if (value.isEmpty() || value.charAt(value.length() - 1) != 's') { throw new ParseException("Invalid duration string: " + value, 0); } boolean negative = false; if (value.charAt(0) == '-') { negative = true; value = value.substring(1); } String secondValue = value.substring(0, value.length() - 1); String nanoValue = ""; int pointPosition = secondValue.indexOf('.'); if (pointPosition != -1) { nanoValue = secondValue.substring(pointPosition + 1); secondValue = secondValue.substring(0, pointPosition); } long seconds = Long.parseLong(secondValue); int nanos = nanoValue.isEmpty() ? 0 : parseNanos(nanoValue); if (seconds < 0) { throw new ParseException("Invalid duration string: " + value, 0); } if (negative) { seconds = -seconds; nanos = -nanos; } try { return normalizedDuration(seconds, nanos); } catch (IllegalArgumentException e) { throw new ParseException("Duration value is out of range.", 0); } } /** * Copy of {@link com.google.protobuf.util.Timestamps#parseNanos}. */ private static int parseNanos(String value) throws ParseException { int result = 0; for (int i = 0; i < 9; ++i) { result = result * 10; if (i < value.length()) { if (value.charAt(i) < '0' || value.charAt(i) > '9') { throw new ParseException("Invalid nanoseconds.", 0); } result += value.charAt(i) - '0'; } } return result; } private static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1); /** * Copy of {@link com.google.protobuf.util.Durations#normalizedDuration}. */ @SuppressWarnings("NarrowingCompoundAssignment") private static long normalizedDuration(long seconds, int nanos) { if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) { seconds = checkedAdd(seconds, nanos / NANOS_PER_SECOND); nanos %= NANOS_PER_SECOND; } if (seconds > 0 && nanos < 0) { nanos += NANOS_PER_SECOND; // no overflow since nanos is negative (and we're adding) seconds--; // no overflow since seconds is positive (and we're decrementing) } if (seconds < 0 && nanos > 0) { nanos -= NANOS_PER_SECOND; // no overflow since nanos is positive (and we're subtracting) seconds++; // no overflow since seconds is negative (and we're incrementing) } if (!durationIsValid(seconds, nanos)) { throw new IllegalArgumentException(String.format( "Duration is not valid. See proto definition for valid values. " + "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. " + "Nanos (%s) must be in range [-999,999,999, +999,999,999]. " + "Nanos must have the same sign as seconds", seconds, nanos)); } return saturatedAdd(TimeUnit.SECONDS.toNanos(seconds), nanos); } /** * Returns true if the given number of seconds and nanos is a valid {@code Duration}. The {@code * seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos} * value must be in the range [-999,999,999, +999,999,999]. * *

Note: Durations less than one second are represented with a 0 {@code seconds} field * and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero * value for the {@code nanos} field must be of the same sign as the {@code seconds} field. * *

Copy of {@link com.google.protobuf.util.Duration#isValid}.

*/ private static boolean durationIsValid(long seconds, int nanos) { if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) { return false; } if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) { return false; } if (seconds < 0 || nanos < 0) { if (seconds > 0 || nanos > 0) { return false; } } return true; } /** * Returns the sum of {@code a} and {@code b} unless it would overflow or underflow in which case * {@code Long.MAX_VALUE} or {@code Long.MIN_VALUE} is returned, respectively. * *

Copy of {@link com.google.common.math.LongMath#saturatedAdd}.

* */ @SuppressWarnings("ShortCircuitBoolean") private static long saturatedAdd(long a, long b) { long naiveSum = a + b; if ((a ^ b) < 0 | (a ^ naiveSum) >= 0) { // If a and b have different signs or a has the same sign as the result then there was no // overflow, return. return naiveSum; } // we did over/under flow, if the sign is negative we should return MAX otherwise MIN return Long.MAX_VALUE + ((naiveSum >>> (Long.SIZE - 1)) ^ 1); } /** * 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 rawConfigValue; public LbConfig(String policyName, Map rawConfigValue) { this.policyName = checkNotNull(policyName, "policyName"); this.rawConfigValue = checkNotNull(rawConfigValue, "rawConfigValue"); } public String getPolicyName() { return policyName; } public Map 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(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy