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.base.Verify.verify; 22 23 import com.google.common.annotations.VisibleForTesting; 24 import com.google.common.base.MoreObjects; 25 import com.google.common.base.Objects; 26 import com.google.common.base.VerifyException; 27 import io.grpc.LoadBalancerProvider; 28 import io.grpc.LoadBalancerRegistry; 29 import io.grpc.NameResolver.ConfigOrError; 30 import io.grpc.Status; 31 import io.grpc.internal.RetriableStream.Throttle; 32 import java.util.ArrayList; 33 import java.util.Collections; 34 import java.util.EnumSet; 35 import java.util.List; 36 import java.util.Locale; 37 import java.util.Map; 38 import java.util.Set; 39 import java.util.logging.Level; 40 import java.util.logging.Logger; 41 import javax.annotation.Nullable; 42 43 /** 44 * Helper utility to work with service configs. 45 * 46 * <p>This class contains helper methods to parse service config JSON values into Java types. 47 */ 48 public final class ServiceConfigUtil { 49 ServiceConfigUtil()50 private ServiceConfigUtil() {} 51 52 /** 53 * Fetches the health-checked service config from service config. {@code null} if can't find one. 54 */ 55 @Nullable getHealthCheckedService(@ullable Map<String, ?> serviceConfig)56 public static Map<String, ?> getHealthCheckedService(@Nullable Map<String, ?> serviceConfig) { 57 if (serviceConfig == null) { 58 return null; 59 } 60 61 /* schema as follows 62 { 63 "healthCheckConfig": { 64 // Service name to use in the health-checking request. 65 "serviceName": string 66 } 67 } 68 */ 69 return JsonUtil.getObject(serviceConfig, "healthCheckConfig"); 70 } 71 72 /** 73 * Fetches the health-checked service name from health-checked service config. {@code null} if 74 * can't find one. 75 */ 76 @Nullable getHealthCheckedServiceName( @ullable Map<String, ?> healthCheckedServiceConfig)77 public static String getHealthCheckedServiceName( 78 @Nullable Map<String, ?> healthCheckedServiceConfig) { 79 if (healthCheckedServiceConfig == null) { 80 return null; 81 } 82 return JsonUtil.getString(healthCheckedServiceConfig, "serviceName"); 83 } 84 85 @Nullable getThrottlePolicy(@ullable Map<String, ?> serviceConfig)86 static Throttle getThrottlePolicy(@Nullable Map<String, ?> serviceConfig) { 87 if (serviceConfig == null) { 88 return null; 89 } 90 91 /* schema as follows 92 { 93 "retryThrottling": { 94 // The number of tokens starts at maxTokens. The token_count will always be 95 // between 0 and maxTokens. 96 // 97 // This field is required and must be greater than zero. 98 "maxTokens": number, 99 100 // The amount of tokens to add on each successful RPC. Typically this will 101 // be some number between 0 and 1, e.g., 0.1. 102 // 103 // This field is required and must be greater than zero. Up to 3 decimal 104 // places are supported. 105 "tokenRatio": number 106 } 107 } 108 */ 109 110 Map<String, ?> throttling = JsonUtil.getObject(serviceConfig, "retryThrottling"); 111 if (throttling == null) { 112 return null; 113 } 114 115 // TODO(dapengzhang0): check if this is null. 116 float maxTokens = JsonUtil.getNumberAsDouble(throttling, "maxTokens").floatValue(); 117 float tokenRatio = JsonUtil.getNumberAsDouble(throttling, "tokenRatio").floatValue(); 118 checkState(maxTokens > 0f, "maxToken should be greater than zero"); 119 checkState(tokenRatio > 0f, "tokenRatio should be greater than zero"); 120 return new Throttle(maxTokens, tokenRatio); 121 } 122 123 @Nullable getMaxAttemptsFromRetryPolicy(Map<String, ?> retryPolicy)124 static Integer getMaxAttemptsFromRetryPolicy(Map<String, ?> retryPolicy) { 125 return JsonUtil.getNumberAsInteger(retryPolicy, "maxAttempts"); 126 } 127 128 @Nullable getInitialBackoffNanosFromRetryPolicy(Map<String, ?> retryPolicy)129 static Long getInitialBackoffNanosFromRetryPolicy(Map<String, ?> retryPolicy) { 130 return JsonUtil.getStringAsDuration(retryPolicy, "initialBackoff"); 131 } 132 133 @Nullable getMaxBackoffNanosFromRetryPolicy(Map<String, ?> retryPolicy)134 static Long getMaxBackoffNanosFromRetryPolicy(Map<String, ?> retryPolicy) { 135 return JsonUtil.getStringAsDuration(retryPolicy, "maxBackoff"); 136 } 137 138 @Nullable getBackoffMultiplierFromRetryPolicy(Map<String, ?> retryPolicy)139 static Double getBackoffMultiplierFromRetryPolicy(Map<String, ?> retryPolicy) { 140 return JsonUtil.getNumberAsDouble(retryPolicy, "backoffMultiplier"); 141 } 142 143 @Nullable getPerAttemptRecvTimeoutNanosFromRetryPolicy(Map<String, ?> retryPolicy)144 static Long getPerAttemptRecvTimeoutNanosFromRetryPolicy(Map<String, ?> retryPolicy) { 145 return JsonUtil.getStringAsDuration(retryPolicy, "perAttemptRecvTimeout"); 146 } 147 getListOfStatusCodesAsSet(Map<String, ?> obj, String key)148 private static Set<Status.Code> getListOfStatusCodesAsSet(Map<String, ?> obj, String key) { 149 List<?> statuses = JsonUtil.getList(obj, key); 150 if (statuses == null) { 151 return null; 152 } 153 return getStatusCodesFromList(statuses); 154 } 155 getStatusCodesFromList(List<?> statuses)156 private static Set<Status.Code> getStatusCodesFromList(List<?> statuses) { 157 EnumSet<Status.Code> codes = EnumSet.noneOf(Status.Code.class); 158 for (Object status : statuses) { 159 Status.Code code; 160 if (status instanceof Double) { 161 Double statusD = (Double) status; 162 int codeValue = statusD.intValue(); 163 verify((double) codeValue == statusD, "Status code %s is not integral", status); 164 code = Status.fromCodeValue(codeValue).getCode(); 165 verify(code.value() == statusD.intValue(), "Status code %s is not valid", status); 166 } else if (status instanceof String) { 167 try { 168 code = Status.Code.valueOf((String) status); 169 } catch (IllegalArgumentException iae) { 170 throw new VerifyException("Status code " + status + " is not valid", iae); 171 } 172 } else { 173 throw new VerifyException( 174 "Can not convert status code " + status + " to Status.Code, because its type is " 175 + status.getClass()); 176 } 177 codes.add(code); 178 } 179 return Collections.unmodifiableSet(codes); 180 } 181 getRetryableStatusCodesFromRetryPolicy(Map<String, ?> retryPolicy)182 static Set<Status.Code> getRetryableStatusCodesFromRetryPolicy(Map<String, ?> retryPolicy) { 183 String retryableStatusCodesKey = "retryableStatusCodes"; 184 Set<Status.Code> codes = getListOfStatusCodesAsSet(retryPolicy, retryableStatusCodesKey); 185 verify(codes != null, "%s is required in retry policy", retryableStatusCodesKey); 186 verify(!codes.contains(Status.Code.OK), "%s must not contain OK", retryableStatusCodesKey); 187 return codes; 188 } 189 190 @Nullable getMaxAttemptsFromHedgingPolicy(Map<String, ?> hedgingPolicy)191 static Integer getMaxAttemptsFromHedgingPolicy(Map<String, ?> hedgingPolicy) { 192 return JsonUtil.getNumberAsInteger(hedgingPolicy, "maxAttempts"); 193 } 194 195 @Nullable getHedgingDelayNanosFromHedgingPolicy(Map<String, ?> hedgingPolicy)196 static Long getHedgingDelayNanosFromHedgingPolicy(Map<String, ?> hedgingPolicy) { 197 return JsonUtil.getStringAsDuration(hedgingPolicy, "hedgingDelay"); 198 } 199 getNonFatalStatusCodesFromHedgingPolicy(Map<String, ?> hedgingPolicy)200 static Set<Status.Code> getNonFatalStatusCodesFromHedgingPolicy(Map<String, ?> hedgingPolicy) { 201 String nonFatalStatusCodesKey = "nonFatalStatusCodes"; 202 Set<Status.Code> codes = getListOfStatusCodesAsSet(hedgingPolicy, nonFatalStatusCodesKey); 203 if (codes == null) { 204 return Collections.unmodifiableSet(EnumSet.noneOf(Status.Code.class)); 205 } 206 verify(!codes.contains(Status.Code.OK), "%s must not contain OK", nonFatalStatusCodesKey); 207 return codes; 208 } 209 210 @Nullable getServiceFromName(Map<String, ?> name)211 static String getServiceFromName(Map<String, ?> name) { 212 return JsonUtil.getString(name, "service"); 213 } 214 215 @Nullable getMethodFromName(Map<String, ?> name)216 static String getMethodFromName(Map<String, ?> name) { 217 return JsonUtil.getString(name, "method"); 218 } 219 220 @Nullable getRetryPolicyFromMethodConfig(Map<String, ?> methodConfig)221 static Map<String, ?> getRetryPolicyFromMethodConfig(Map<String, ?> methodConfig) { 222 return JsonUtil.getObject(methodConfig, "retryPolicy"); 223 } 224 225 @Nullable getHedgingPolicyFromMethodConfig(Map<String, ?> methodConfig)226 static Map<String, ?> getHedgingPolicyFromMethodConfig(Map<String, ?> methodConfig) { 227 return JsonUtil.getObject(methodConfig, "hedgingPolicy"); 228 } 229 230 @Nullable getNameListFromMethodConfig( Map<String, ?> methodConfig)231 static List<Map<String, ?>> getNameListFromMethodConfig( 232 Map<String, ?> methodConfig) { 233 return JsonUtil.getListOfObjects(methodConfig, "name"); 234 } 235 236 /** 237 * Returns the number of nanoseconds of timeout for the given method config. 238 * 239 * @return duration nanoseconds, or {@code null} if it isn't present. 240 */ 241 @Nullable getTimeoutFromMethodConfig(Map<String, ?> methodConfig)242 static Long getTimeoutFromMethodConfig(Map<String, ?> methodConfig) { 243 return JsonUtil.getStringAsDuration(methodConfig, "timeout"); 244 } 245 246 @Nullable getWaitForReadyFromMethodConfig(Map<String, ?> methodConfig)247 static Boolean getWaitForReadyFromMethodConfig(Map<String, ?> methodConfig) { 248 return JsonUtil.getBoolean(methodConfig, "waitForReady"); 249 } 250 251 @Nullable getMaxRequestMessageBytesFromMethodConfig(Map<String, ?> methodConfig)252 static Integer getMaxRequestMessageBytesFromMethodConfig(Map<String, ?> methodConfig) { 253 return JsonUtil.getNumberAsInteger(methodConfig, "maxRequestMessageBytes"); 254 } 255 256 @Nullable getMaxResponseMessageBytesFromMethodConfig(Map<String, ?> methodConfig)257 static Integer getMaxResponseMessageBytesFromMethodConfig(Map<String, ?> methodConfig) { 258 return JsonUtil.getNumberAsInteger(methodConfig, "maxResponseMessageBytes"); 259 } 260 261 @Nullable getMethodConfigFromServiceConfig( Map<String, ?> serviceConfig)262 static List<Map<String, ?>> getMethodConfigFromServiceConfig( 263 Map<String, ?> serviceConfig) { 264 return JsonUtil.getListOfObjects(serviceConfig, "methodConfig"); 265 } 266 267 /** 268 * Extracts load balancing configs from a service config. 269 */ 270 @VisibleForTesting getLoadBalancingConfigsFromServiceConfig( Map<String, ?> serviceConfig)271 public static List<Map<String, ?>> getLoadBalancingConfigsFromServiceConfig( 272 Map<String, ?> serviceConfig) { 273 /* schema as follows 274 { 275 "loadBalancingConfig": [ 276 {"xds" : 277 { 278 "childPolicy": [...], 279 "fallbackPolicy": [...], 280 } 281 }, 282 {"round_robin": {}} 283 ], 284 "loadBalancingPolicy": "ROUND_ROBIN" // The deprecated policy key 285 } 286 */ 287 List<Map<String, ?>> lbConfigs = new ArrayList<>(); 288 String loadBalancingConfigKey = "loadBalancingConfig"; 289 if (serviceConfig.containsKey(loadBalancingConfigKey)) { 290 lbConfigs.addAll(JsonUtil.getListOfObjects( 291 serviceConfig, loadBalancingConfigKey)); 292 } 293 if (lbConfigs.isEmpty()) { 294 // No LoadBalancingConfig found. Fall back to the deprecated LoadBalancingPolicy 295 String policy = JsonUtil.getString(serviceConfig, "loadBalancingPolicy"); 296 if (policy != null) { 297 // Convert the policy to a config, so that the caller can handle them in the same way. 298 policy = policy.toLowerCase(Locale.ROOT); 299 Map<String, ?> fakeConfig = Collections.singletonMap(policy, Collections.emptyMap()); 300 lbConfigs.add(fakeConfig); 301 } 302 } 303 return Collections.unmodifiableList(lbConfigs); 304 } 305 306 /** 307 * Unwrap a LoadBalancingConfig JSON object into a {@link LbConfig}. The input is a JSON object 308 * (map) with exactly one entry, where the key is the policy name and the value is a config object 309 * for that policy. 310 */ unwrapLoadBalancingConfig(Map<String, ?> lbConfig)311 public static LbConfig unwrapLoadBalancingConfig(Map<String, ?> lbConfig) { 312 if (lbConfig.size() != 1) { 313 throw new RuntimeException( 314 "There are " + lbConfig.size() + " fields in a LoadBalancingConfig object. Exactly one" 315 + " is expected. Config=" + lbConfig); 316 } 317 String key = lbConfig.entrySet().iterator().next().getKey(); 318 return new LbConfig(key, JsonUtil.getObject(lbConfig, key)); 319 } 320 321 /** 322 * Given a JSON list of LoadBalancingConfigs, and convert it into a list of LbConfig. 323 */ unwrapLoadBalancingConfigList(List<Map<String, ?>> list)324 public static List<LbConfig> unwrapLoadBalancingConfigList(List<Map<String, ?>> list) { 325 if (list == null) { 326 return null; 327 } 328 ArrayList<LbConfig> result = new ArrayList<>(); 329 for (Map<String, ?> rawChildPolicy : list) { 330 result.add(unwrapLoadBalancingConfig(rawChildPolicy)); 331 } 332 return Collections.unmodifiableList(result); 333 } 334 335 /** 336 * Parses and selects a load balancing policy from a non-empty list of raw configs. If selection 337 * is successful, the returned ConfigOrError object will include a {@link 338 * ServiceConfigUtil.PolicySelection} as its config value. 339 */ selectLbPolicyFromList( List<LbConfig> lbConfigs, LoadBalancerRegistry lbRegistry)340 public static ConfigOrError selectLbPolicyFromList( 341 List<LbConfig> lbConfigs, LoadBalancerRegistry lbRegistry) { 342 List<String> policiesTried = new ArrayList<>(); 343 for (LbConfig lbConfig : lbConfigs) { 344 String policy = lbConfig.getPolicyName(); 345 LoadBalancerProvider provider = lbRegistry.getProvider(policy); 346 if (provider == null) { 347 policiesTried.add(policy); 348 } else { 349 if (!policiesTried.isEmpty()) { 350 Logger.getLogger(ServiceConfigUtil.class.getName()).log( 351 Level.FINEST, 352 "{0} specified by Service Config are not available", policiesTried); 353 } 354 ConfigOrError parsedLbPolicyConfig = 355 provider.parseLoadBalancingPolicyConfig(lbConfig.getRawConfigValue()); 356 if (parsedLbPolicyConfig.getError() != null) { 357 return parsedLbPolicyConfig; 358 } 359 return ConfigOrError.fromConfig( 360 new PolicySelection(provider, parsedLbPolicyConfig.getConfig())); 361 } 362 } 363 return ConfigOrError.fromError( 364 Status.UNKNOWN.withDescription( 365 "None of " + policiesTried + " specified by Service Config are available.")); 366 } 367 368 /** 369 * A LoadBalancingConfig that includes the policy name (the key) and its raw config value (parsed 370 * JSON). 371 */ 372 public static final class LbConfig { 373 private final String policyName; 374 private final Map<String, ?> rawConfigValue; 375 LbConfig(String policyName, Map<String, ?> rawConfigValue)376 public LbConfig(String policyName, Map<String, ?> rawConfigValue) { 377 this.policyName = checkNotNull(policyName, "policyName"); 378 this.rawConfigValue = checkNotNull(rawConfigValue, "rawConfigValue"); 379 } 380 getPolicyName()381 public String getPolicyName() { 382 return policyName; 383 } 384 getRawConfigValue()385 public Map<String, ?> getRawConfigValue() { 386 return rawConfigValue; 387 } 388 389 @Override equals(Object o)390 public boolean equals(Object o) { 391 if (o instanceof LbConfig) { 392 LbConfig other = (LbConfig) o; 393 return policyName.equals(other.policyName) 394 && rawConfigValue.equals(other.rawConfigValue); 395 } 396 return false; 397 } 398 399 @Override hashCode()400 public int hashCode() { 401 return Objects.hashCode(policyName, rawConfigValue); 402 } 403 404 @Override toString()405 public String toString() { 406 return MoreObjects.toStringHelper(this) 407 .add("policyName", policyName) 408 .add("rawConfigValue", rawConfigValue) 409 .toString(); 410 } 411 } 412 413 public static final class PolicySelection { 414 final LoadBalancerProvider provider; 415 @Nullable 416 final Object config; 417 418 /** Constructs a PolicySelection with selected LB provider and the deeply parsed LB config. */ PolicySelection( LoadBalancerProvider provider, @Nullable Object config)419 public PolicySelection( 420 LoadBalancerProvider provider, 421 @Nullable Object config) { 422 this.provider = checkNotNull(provider, "provider"); 423 this.config = config; 424 } 425 getProvider()426 public LoadBalancerProvider getProvider() { 427 return provider; 428 } 429 430 @Nullable getConfig()431 public Object getConfig() { 432 return config; 433 } 434 435 @Override equals(Object o)436 public boolean equals(Object o) { 437 if (this == o) { 438 return true; 439 } 440 if (o == null || getClass() != o.getClass()) { 441 return false; 442 } 443 PolicySelection that = (PolicySelection) o; 444 return Objects.equal(provider, that.provider) 445 && Objects.equal(config, that.config); 446 } 447 448 @Override hashCode()449 public int hashCode() { 450 return Objects.hashCode(provider, config); 451 } 452 453 @Override toString()454 public String toString() { 455 return MoreObjects.toStringHelper(this) 456 .add("provider", provider) 457 .add("config", config) 458 .toString(); 459 } 460 } 461 } 462