1 /* 2 * Copyright 2018 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.internal; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 import static com.google.common.base.Preconditions.checkState; 21 import static com.google.common.math.LongMath.checkedAdd; 22 23 import com.google.common.annotations.VisibleForTesting; 24 import io.grpc.internal.RetriableStream.Throttle; 25 import java.text.ParseException; 26 import java.util.List; 27 import java.util.Map; 28 import java.util.concurrent.TimeUnit; 29 import javax.annotation.Nullable; 30 31 /** 32 * Helper utility to work with service configs. 33 */ 34 @VisibleForTesting 35 public final class ServiceConfigUtil { 36 37 private static final String SERVICE_CONFIG_METHOD_CONFIG_KEY = "methodConfig"; 38 private static final String SERVICE_CONFIG_LOAD_BALANCING_POLICY_KEY = "loadBalancingPolicy"; 39 private static final String SERVICE_CONFIG_STICKINESS_METADATA_KEY = "stickinessMetadataKey"; 40 private static final String METHOD_CONFIG_NAME_KEY = "name"; 41 private static final String METHOD_CONFIG_TIMEOUT_KEY = "timeout"; 42 private static final String METHOD_CONFIG_WAIT_FOR_READY_KEY = "waitForReady"; 43 private static final String METHOD_CONFIG_MAX_REQUEST_MESSAGE_BYTES_KEY = 44 "maxRequestMessageBytes"; 45 private static final String METHOD_CONFIG_MAX_RESPONSE_MESSAGE_BYTES_KEY = 46 "maxResponseMessageBytes"; 47 private static final String METHOD_CONFIG_RETRY_POLICY_KEY = "retryPolicy"; 48 private static final String METHOD_CONFIG_HEDGING_POLICY_KEY = "hedgingPolicy"; 49 private static final String NAME_SERVICE_KEY = "service"; 50 private static final String NAME_METHOD_KEY = "method"; 51 private static final String RETRY_POLICY_MAX_ATTEMPTS_KEY = "maxAttempts"; 52 private static final String RETRY_POLICY_INITIAL_BACKOFF_KEY = "initialBackoff"; 53 private static final String RETRY_POLICY_MAX_BACKOFF_KEY = "maxBackoff"; 54 private static final String RETRY_POLICY_BACKOFF_MULTIPLIER_KEY = "backoffMultiplier"; 55 private static final String RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY = "retryableStatusCodes"; 56 private static final String HEDGING_POLICY_MAX_ATTEMPTS_KEY = "maxAttempts"; 57 private static final String HEDGING_POLICY_HEDGING_DELAY_KEY = "hedgingDelay"; 58 private static final String HEDGING_POLICY_NON_FATAL_STATUS_CODES_KEY = "nonFatalStatusCodes"; 59 60 private static final long DURATION_SECONDS_MIN = -315576000000L; 61 private static final long DURATION_SECONDS_MAX = 315576000000L; 62 ServiceConfigUtil()63 private ServiceConfigUtil() {} 64 65 @Nullable getThrottlePolicy(@ullable Map<String, Object> serviceConfig)66 static Throttle getThrottlePolicy(@Nullable Map<String, Object> serviceConfig) { 67 String retryThrottlingKey = "retryThrottling"; 68 if (serviceConfig == null || !serviceConfig.containsKey(retryThrottlingKey)) { 69 return null; 70 } 71 72 /* schema as follows 73 { 74 "retryThrottling": { 75 // The number of tokens starts at maxTokens. The token_count will always be 76 // between 0 and maxTokens. 77 // 78 // This field is required and must be greater than zero. 79 "maxTokens": number, 80 81 // The amount of tokens to add on each successful RPC. Typically this will 82 // be some number between 0 and 1, e.g., 0.1. 83 // 84 // This field is required and must be greater than zero. Up to 3 decimal 85 // places are supported. 86 "tokenRatio": number 87 } 88 } 89 */ 90 91 Map<String, Object> throttling = getObject(serviceConfig, retryThrottlingKey); 92 93 float maxTokens = getDouble(throttling, "maxTokens").floatValue(); 94 float tokenRatio = getDouble(throttling, "tokenRatio").floatValue(); 95 checkState(maxTokens > 0f, "maxToken should be greater than zero"); 96 checkState(tokenRatio > 0f, "tokenRatio should be greater than zero"); 97 return new Throttle(maxTokens, tokenRatio); 98 } 99 100 @Nullable getMaxAttemptsFromRetryPolicy(Map<String, Object> retryPolicy)101 static Integer getMaxAttemptsFromRetryPolicy(Map<String, Object> retryPolicy) { 102 if (!retryPolicy.containsKey(RETRY_POLICY_MAX_ATTEMPTS_KEY)) { 103 return null; 104 } 105 return getDouble(retryPolicy, RETRY_POLICY_MAX_ATTEMPTS_KEY).intValue(); 106 } 107 108 @Nullable getInitialBackoffNanosFromRetryPolicy(Map<String, Object> retryPolicy)109 static Long getInitialBackoffNanosFromRetryPolicy(Map<String, Object> retryPolicy) { 110 if (!retryPolicy.containsKey(RETRY_POLICY_INITIAL_BACKOFF_KEY)) { 111 return null; 112 } 113 String rawInitialBackoff = getString(retryPolicy, RETRY_POLICY_INITIAL_BACKOFF_KEY); 114 try { 115 return parseDuration(rawInitialBackoff); 116 } catch (ParseException e) { 117 throw new RuntimeException(e); 118 } 119 } 120 121 @Nullable getMaxBackoffNanosFromRetryPolicy(Map<String, Object> retryPolicy)122 static Long getMaxBackoffNanosFromRetryPolicy(Map<String, Object> retryPolicy) { 123 if (!retryPolicy.containsKey(RETRY_POLICY_MAX_BACKOFF_KEY)) { 124 return null; 125 } 126 String rawMaxBackoff = getString(retryPolicy, RETRY_POLICY_MAX_BACKOFF_KEY); 127 try { 128 return parseDuration(rawMaxBackoff); 129 } catch (ParseException e) { 130 throw new RuntimeException(e); 131 } 132 } 133 134 @Nullable getBackoffMultiplierFromRetryPolicy(Map<String, Object> retryPolicy)135 static Double getBackoffMultiplierFromRetryPolicy(Map<String, Object> retryPolicy) { 136 if (!retryPolicy.containsKey(RETRY_POLICY_BACKOFF_MULTIPLIER_KEY)) { 137 return null; 138 } 139 return getDouble(retryPolicy, RETRY_POLICY_BACKOFF_MULTIPLIER_KEY); 140 } 141 142 @Nullable getRetryableStatusCodesFromRetryPolicy(Map<String, Object> retryPolicy)143 static List<String> getRetryableStatusCodesFromRetryPolicy(Map<String, Object> retryPolicy) { 144 if (!retryPolicy.containsKey(RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY)) { 145 return null; 146 } 147 return checkStringList(getList(retryPolicy, RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY)); 148 } 149 150 @Nullable getMaxAttemptsFromHedgingPolicy(Map<String, Object> hedgingPolicy)151 static Integer getMaxAttemptsFromHedgingPolicy(Map<String, Object> hedgingPolicy) { 152 if (!hedgingPolicy.containsKey(HEDGING_POLICY_MAX_ATTEMPTS_KEY)) { 153 return null; 154 } 155 return getDouble(hedgingPolicy, RETRY_POLICY_MAX_ATTEMPTS_KEY).intValue(); 156 } 157 158 @Nullable getHedgingDelayNanosFromHedgingPolicy(Map<String, Object> hedgingPolicy)159 static Long getHedgingDelayNanosFromHedgingPolicy(Map<String, Object> hedgingPolicy) { 160 if (!hedgingPolicy.containsKey(HEDGING_POLICY_HEDGING_DELAY_KEY)) { 161 return null; 162 } 163 String rawHedgingDelay = getString(hedgingPolicy, HEDGING_POLICY_HEDGING_DELAY_KEY); 164 try { 165 return parseDuration(rawHedgingDelay); 166 } catch (ParseException e) { 167 throw new RuntimeException(e); 168 } 169 } 170 171 @Nullable getNonFatalStatusCodesFromHedgingPolicy(Map<String, Object> hedgingPolicy)172 static List<String> getNonFatalStatusCodesFromHedgingPolicy(Map<String, Object> hedgingPolicy) { 173 if (!hedgingPolicy.containsKey(HEDGING_POLICY_NON_FATAL_STATUS_CODES_KEY)) { 174 return null; 175 } 176 return checkStringList(getList(hedgingPolicy, HEDGING_POLICY_NON_FATAL_STATUS_CODES_KEY)); 177 } 178 179 @Nullable getServiceFromName(Map<String, Object> name)180 static String getServiceFromName(Map<String, Object> name) { 181 if (!name.containsKey(NAME_SERVICE_KEY)) { 182 return null; 183 } 184 return getString(name, NAME_SERVICE_KEY); 185 } 186 187 @Nullable getMethodFromName(Map<String, Object> name)188 static String getMethodFromName(Map<String, Object> name) { 189 if (!name.containsKey(NAME_METHOD_KEY)) { 190 return null; 191 } 192 return getString(name, NAME_METHOD_KEY); 193 } 194 195 @Nullable getRetryPolicyFromMethodConfig(Map<String, Object> methodConfig)196 static Map<String, Object> getRetryPolicyFromMethodConfig(Map<String, Object> methodConfig) { 197 if (!methodConfig.containsKey(METHOD_CONFIG_RETRY_POLICY_KEY)) { 198 return null; 199 } 200 return getObject(methodConfig, METHOD_CONFIG_RETRY_POLICY_KEY); 201 } 202 203 @Nullable getHedgingPolicyFromMethodConfig(Map<String, Object> methodConfig)204 static Map<String, Object> getHedgingPolicyFromMethodConfig(Map<String, Object> methodConfig) { 205 if (!methodConfig.containsKey(METHOD_CONFIG_HEDGING_POLICY_KEY)) { 206 return null; 207 } 208 return getObject(methodConfig, METHOD_CONFIG_HEDGING_POLICY_KEY); 209 } 210 211 @Nullable getNameListFromMethodConfig(Map<String, Object> methodConfig)212 static List<Map<String, Object>> getNameListFromMethodConfig(Map<String, Object> methodConfig) { 213 if (!methodConfig.containsKey(METHOD_CONFIG_NAME_KEY)) { 214 return null; 215 } 216 return checkObjectList(getList(methodConfig, METHOD_CONFIG_NAME_KEY)); 217 } 218 219 /** 220 * Returns the number of nanoseconds of timeout for the given method config. 221 * 222 * @return duration nanoseconds, or {@code null} if it isn't present. 223 */ 224 @Nullable getTimeoutFromMethodConfig(Map<String, Object> methodConfig)225 static Long getTimeoutFromMethodConfig(Map<String, Object> methodConfig) { 226 if (!methodConfig.containsKey(METHOD_CONFIG_TIMEOUT_KEY)) { 227 return null; 228 } 229 String rawTimeout = getString(methodConfig, METHOD_CONFIG_TIMEOUT_KEY); 230 try { 231 return parseDuration(rawTimeout); 232 } catch (ParseException e) { 233 throw new RuntimeException(e); 234 } 235 } 236 237 @Nullable getWaitForReadyFromMethodConfig(Map<String, Object> methodConfig)238 static Boolean getWaitForReadyFromMethodConfig(Map<String, Object> methodConfig) { 239 if (!methodConfig.containsKey(METHOD_CONFIG_WAIT_FOR_READY_KEY)) { 240 return null; 241 } 242 return getBoolean(methodConfig, METHOD_CONFIG_WAIT_FOR_READY_KEY); 243 } 244 245 @Nullable getMaxRequestMessageBytesFromMethodConfig(Map<String, Object> methodConfig)246 static Integer getMaxRequestMessageBytesFromMethodConfig(Map<String, Object> methodConfig) { 247 if (!methodConfig.containsKey(METHOD_CONFIG_MAX_REQUEST_MESSAGE_BYTES_KEY)) { 248 return null; 249 } 250 return getDouble(methodConfig, METHOD_CONFIG_MAX_REQUEST_MESSAGE_BYTES_KEY).intValue(); 251 } 252 253 @Nullable getMaxResponseMessageBytesFromMethodConfig(Map<String, Object> methodConfig)254 static Integer getMaxResponseMessageBytesFromMethodConfig(Map<String, Object> methodConfig) { 255 if (!methodConfig.containsKey(METHOD_CONFIG_MAX_RESPONSE_MESSAGE_BYTES_KEY)) { 256 return null; 257 } 258 return getDouble(methodConfig, METHOD_CONFIG_MAX_RESPONSE_MESSAGE_BYTES_KEY).intValue(); 259 } 260 261 @Nullable getMethodConfigFromServiceConfig( Map<String, Object> serviceConfig)262 static List<Map<String, Object>> getMethodConfigFromServiceConfig( 263 Map<String, Object> serviceConfig) { 264 if (!serviceConfig.containsKey(SERVICE_CONFIG_METHOD_CONFIG_KEY)) { 265 return null; 266 } 267 return checkObjectList(getList(serviceConfig, SERVICE_CONFIG_METHOD_CONFIG_KEY)); 268 } 269 270 /** 271 * Extracts the load balancing policy from a service config, or {@code null}. 272 */ 273 @Nullable 274 @VisibleForTesting getLoadBalancingPolicyFromServiceConfig(Map<String, Object> serviceConfig)275 public static String getLoadBalancingPolicyFromServiceConfig(Map<String, Object> serviceConfig) { 276 if (!serviceConfig.containsKey(SERVICE_CONFIG_LOAD_BALANCING_POLICY_KEY)) { 277 return null; 278 } 279 return getString(serviceConfig, SERVICE_CONFIG_LOAD_BALANCING_POLICY_KEY); 280 } 281 282 /** 283 * Extracts the stickiness metadata key from a service config, or {@code null}. 284 */ 285 @Nullable getStickinessMetadataKeyFromServiceConfig( Map<String, Object> serviceConfig)286 public static String getStickinessMetadataKeyFromServiceConfig( 287 Map<String, Object> serviceConfig) { 288 if (!serviceConfig.containsKey(SERVICE_CONFIG_STICKINESS_METADATA_KEY)) { 289 return null; 290 } 291 return getString(serviceConfig, SERVICE_CONFIG_STICKINESS_METADATA_KEY); 292 } 293 294 /** 295 * Gets a list from an object for the given key. 296 */ 297 @SuppressWarnings("unchecked") getList(Map<String, Object> obj, String key)298 static List<Object> getList(Map<String, Object> obj, String key) { 299 assert obj.containsKey(key); 300 Object value = checkNotNull(obj.get(key), "no such key %s", key); 301 if (value instanceof List) { 302 return (List<Object>) value; 303 } 304 throw new ClassCastException( 305 String.format("value %s for key %s in %s is not List", value, key, obj)); 306 } 307 308 /** 309 * Gets an object from an object for the given key. 310 */ 311 @SuppressWarnings("unchecked") getObject(Map<String, Object> obj, String key)312 static Map<String, Object> getObject(Map<String, Object> obj, String key) { 313 assert obj.containsKey(key); 314 Object value = checkNotNull(obj.get(key), "no such key %s", key); 315 if (value instanceof Map) { 316 return (Map<String, Object>) value; 317 } 318 throw new ClassCastException( 319 String.format("value %s for key %s in %s is not object", value, key, obj)); 320 } 321 322 /** 323 * Gets a double from an object for the given key. 324 */ 325 @SuppressWarnings("unchecked") getDouble(Map<String, Object> obj, String key)326 static Double getDouble(Map<String, Object> obj, String key) { 327 assert obj.containsKey(key); 328 Object value = checkNotNull(obj.get(key), "no such key %s", key); 329 if (value instanceof Double) { 330 return (Double) value; 331 } 332 throw new ClassCastException( 333 String.format("value %s for key %s in %s is not Double", value, key, obj)); 334 } 335 336 /** 337 * Gets a string from an object for the given key. 338 */ 339 @SuppressWarnings("unchecked") getString(Map<String, Object> obj, String key)340 static String getString(Map<String, Object> obj, String key) { 341 assert obj.containsKey(key); 342 Object value = checkNotNull(obj.get(key), "no such key %s", key); 343 if (value instanceof String) { 344 return (String) value; 345 } 346 throw new ClassCastException( 347 String.format("value %s for key %s in %s is not String", value, key, obj)); 348 } 349 350 /** 351 * Gets a string from an object for the given index. 352 */ 353 @SuppressWarnings("unchecked") getString(List<Object> list, int i)354 static String getString(List<Object> list, int i) { 355 assert i >= 0 && i < list.size(); 356 Object value = checkNotNull(list.get(i), "idx %s in %s is null", i, list); 357 if (value instanceof String) { 358 return (String) value; 359 } 360 throw new ClassCastException( 361 String.format("value %s for idx %d in %s is not String", value, i, list)); 362 } 363 364 /** 365 * Gets a boolean from an object for the given key. 366 */ 367 static Boolean getBoolean(Map<String, Object> obj, String key) { 368 assert obj.containsKey(key); 369 Object value = checkNotNull(obj.get(key), "no such key %s", key); 370 if (value instanceof Boolean) { 371 return (Boolean) value; 372 } 373 throw new ClassCastException( 374 String.format("value %s for key %s in %s is not Boolean", value, key, obj)); 375 } 376 377 @SuppressWarnings("unchecked") 378 private static List<Map<String, Object>> checkObjectList(List<Object> rawList) { 379 for (int i = 0; i < rawList.size(); i++) { 380 if (!(rawList.get(i) instanceof Map)) { 381 throw new ClassCastException( 382 String.format("value %s for idx %d in %s is not object", rawList.get(i), i, rawList)); 383 } 384 } 385 return (List<Map<String, Object>>) (List<?>) rawList; 386 } 387 388 @SuppressWarnings("unchecked") 389 static List<String> checkStringList(List<Object> rawList) { 390 for (int i = 0; i < rawList.size(); i++) { 391 if (!(rawList.get(i) instanceof String)) { 392 throw new ClassCastException( 393 String.format("value %s for idx %d in %s is not string", rawList.get(i), i, rawList)); 394 } 395 } 396 return (List<String>) (List<?>) rawList; 397 } 398 399 /** 400 * Parse from a string to produce a duration. Copy of 401 * {@link com.google.protobuf.util.Durations#parse}. 402 * 403 * @return A Duration parsed from the string. 404 * @throws ParseException if parsing fails. 405 */ 406 private static long parseDuration(String value) throws ParseException { 407 // Must ended with "s". 408 if (value.isEmpty() || value.charAt(value.length() - 1) != 's') { 409 throw new ParseException("Invalid duration string: " + value, 0); 410 } 411 boolean negative = false; 412 if (value.charAt(0) == '-') { 413 negative = true; 414 value = value.substring(1); 415 } 416 String secondValue = value.substring(0, value.length() - 1); 417 String nanoValue = ""; 418 int pointPosition = secondValue.indexOf('.'); 419 if (pointPosition != -1) { 420 nanoValue = secondValue.substring(pointPosition + 1); 421 secondValue = secondValue.substring(0, pointPosition); 422 } 423 long seconds = Long.parseLong(secondValue); 424 int nanos = nanoValue.isEmpty() ? 0 : parseNanos(nanoValue); 425 if (seconds < 0) { 426 throw new ParseException("Invalid duration string: " + value, 0); 427 } 428 if (negative) { 429 seconds = -seconds; 430 nanos = -nanos; 431 } 432 try { 433 return normalizedDuration(seconds, nanos); 434 } catch (IllegalArgumentException e) { 435 throw new ParseException("Duration value is out of range.", 0); 436 } 437 } 438 439 /** 440 * Copy of {@link com.google.protobuf.util.Timestamps#parseNanos}. 441 */ 442 private static int parseNanos(String value) throws ParseException { 443 int result = 0; 444 for (int i = 0; i < 9; ++i) { 445 result = result * 10; 446 if (i < value.length()) { 447 if (value.charAt(i) < '0' || value.charAt(i) > '9') { 448 throw new ParseException("Invalid nanoseconds.", 0); 449 } 450 result += value.charAt(i) - '0'; 451 } 452 } 453 return result; 454 } 455 456 private static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1); 457 458 /** 459 * Copy of {@link com.google.protobuf.util.Durations#normalizedDuration}. 460 */ 461 @SuppressWarnings("NarrowingCompoundAssignment") 462 private static long normalizedDuration(long seconds, int nanos) { 463 if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) { 464 seconds = checkedAdd(seconds, nanos / NANOS_PER_SECOND); 465 nanos %= NANOS_PER_SECOND; 466 } 467 if (seconds > 0 && nanos < 0) { 468 nanos += NANOS_PER_SECOND; // no overflow since nanos is negative (and we're adding) 469 seconds--; // no overflow since seconds is positive (and we're decrementing) 470 } 471 if (seconds < 0 && nanos > 0) { 472 nanos -= NANOS_PER_SECOND; // no overflow since nanos is positive (and we're subtracting) 473 seconds++; // no overflow since seconds is negative (and we're incrementing) 474 } 475 if (!durationIsValid(seconds, nanos)) { 476 throw new IllegalArgumentException(String.format( 477 "Duration is not valid. See proto definition for valid values. " 478 + "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. " 479 + "Nanos (%s) must be in range [-999,999,999, +999,999,999]. " 480 + "Nanos must have the same sign as seconds", seconds, nanos)); 481 } 482 return saturatedAdd(TimeUnit.SECONDS.toNanos(seconds), nanos); 483 } 484 485 /** 486 * Returns true if the given number of seconds and nanos is a valid {@code Duration}. The {@code 487 * seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos} 488 * value must be in the range [-999,999,999, +999,999,999]. 489 * 490 * <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field 491 * and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero 492 * value for the {@code nanos} field must be of the same sign as the {@code seconds} field. 493 * 494 * <p>Copy of {@link com.google.protobuf.util.Duration#isValid}.</p> 495 */ 496 private static boolean durationIsValid(long seconds, int nanos) { 497 if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) { 498 return false; 499 } 500 if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) { 501 return false; 502 } 503 if (seconds < 0 || nanos < 0) { 504 if (seconds > 0 || nanos > 0) { 505 return false; 506 } 507 } 508 return true; 509 } 510 511 /** 512 * Returns the sum of {@code a} and {@code b} unless it would overflow or underflow in which case 513 * {@code Long.MAX_VALUE} or {@code Long.MIN_VALUE} is returned, respectively. 514 * 515 * <p>Copy of {@link com.google.common.math.LongMath#saturatedAdd}.</p> 516 * 517 */ 518 @SuppressWarnings("ShortCircuitBoolean") 519 private static long saturatedAdd(long a, long b) { 520 long naiveSum = a + b; 521 if ((a ^ b) < 0 | (a ^ naiveSum) >= 0) { 522 // If a and b have different signs or a has the same sign as the result then there was no 523 // overflow, return. 524 return naiveSum; 525 } 526 // we did over/under flow, if the sign is negative we should return MAX otherwise MIN 527 return Long.MAX_VALUE + ((naiveSum >>> (Long.SIZE - 1)) ^ 1); 528 } 529 } 530