• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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