1 /* 2 * Copyright 2019 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.checkArgument; 20 import static com.google.common.base.Preconditions.checkNotNull; 21 22 import com.google.common.annotations.VisibleForTesting; 23 import com.google.common.base.MoreObjects; 24 import com.google.common.base.Objects; 25 import com.google.common.base.Strings; 26 import io.grpc.CallOptions; 27 import io.grpc.InternalConfigSelector; 28 import io.grpc.LoadBalancer.PickSubchannelArgs; 29 import io.grpc.MethodDescriptor; 30 import io.grpc.Status.Code; 31 import io.grpc.internal.RetriableStream.Throttle; 32 import java.util.Collections; 33 import java.util.HashMap; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.Set; 37 import javax.annotation.Nullable; 38 39 /** 40 * {@link ManagedChannelServiceConfig} is a fully parsed and validated representation of service 41 * configuration data. 42 */ 43 final class ManagedChannelServiceConfig { 44 45 @Nullable 46 private final MethodInfo defaultMethodConfig; 47 private final Map<String, MethodInfo> serviceMethodMap; 48 private final Map<String, MethodInfo> serviceMap; 49 @Nullable 50 private final Throttle retryThrottling; 51 @Nullable 52 private final Object loadBalancingConfig; 53 @Nullable 54 private final Map<String, ?> healthCheckingConfig; 55 ManagedChannelServiceConfig( @ullable MethodInfo defaultMethodConfig, Map<String, MethodInfo> serviceMethodMap, Map<String, MethodInfo> serviceMap, @Nullable Throttle retryThrottling, @Nullable Object loadBalancingConfig, @Nullable Map<String, ?> healthCheckingConfig)56 ManagedChannelServiceConfig( 57 @Nullable MethodInfo defaultMethodConfig, 58 Map<String, MethodInfo> serviceMethodMap, 59 Map<String, MethodInfo> serviceMap, 60 @Nullable Throttle retryThrottling, 61 @Nullable Object loadBalancingConfig, 62 @Nullable Map<String, ?> healthCheckingConfig) { 63 this.defaultMethodConfig = defaultMethodConfig; 64 this.serviceMethodMap = Collections.unmodifiableMap(new HashMap<>(serviceMethodMap)); 65 this.serviceMap = Collections.unmodifiableMap(new HashMap<>(serviceMap)); 66 this.retryThrottling = retryThrottling; 67 this.loadBalancingConfig = loadBalancingConfig; 68 this.healthCheckingConfig = 69 healthCheckingConfig != null 70 ? Collections.unmodifiableMap(new HashMap<>(healthCheckingConfig)) 71 : null; 72 } 73 74 /** Returns an empty {@link ManagedChannelServiceConfig}. */ empty()75 static ManagedChannelServiceConfig empty() { 76 return 77 new ManagedChannelServiceConfig( 78 null, 79 new HashMap<String, MethodInfo>(), 80 new HashMap<String, MethodInfo>(), 81 /* retryThrottling= */ null, 82 /* loadBalancingConfig= */ null, 83 /* healthCheckingConfig= */ null); 84 } 85 86 /** 87 * Parses the Channel level config values (e.g. excludes load balancing) 88 */ fromServiceConfig( Map<String, ?> serviceConfig, boolean retryEnabled, int maxRetryAttemptsLimit, int maxHedgedAttemptsLimit, @Nullable Object loadBalancingConfig)89 static ManagedChannelServiceConfig fromServiceConfig( 90 Map<String, ?> serviceConfig, 91 boolean retryEnabled, 92 int maxRetryAttemptsLimit, 93 int maxHedgedAttemptsLimit, 94 @Nullable Object loadBalancingConfig) { 95 Throttle retryThrottling = null; 96 if (retryEnabled) { 97 retryThrottling = ServiceConfigUtil.getThrottlePolicy(serviceConfig); 98 } 99 Map<String, MethodInfo> serviceMethodMap = new HashMap<>(); 100 Map<String, MethodInfo> serviceMap = new HashMap<>(); 101 Map<String, ?> healthCheckingConfig = 102 ServiceConfigUtil.getHealthCheckedService(serviceConfig); 103 104 // Try and do as much validation here before we swap out the existing configuration. In case 105 // the input is invalid, we don't want to lose the existing configuration. 106 List<Map<String, ?>> methodConfigs = 107 ServiceConfigUtil.getMethodConfigFromServiceConfig(serviceConfig); 108 109 if (methodConfigs == null) { 110 // this is surprising, but possible. 111 return 112 new ManagedChannelServiceConfig( 113 null, 114 serviceMethodMap, 115 serviceMap, 116 retryThrottling, 117 loadBalancingConfig, 118 healthCheckingConfig); 119 } 120 121 MethodInfo defaultMethodConfig = null; 122 for (Map<String, ?> methodConfig : methodConfigs) { 123 MethodInfo info = new MethodInfo( 124 methodConfig, retryEnabled, maxRetryAttemptsLimit, maxHedgedAttemptsLimit); 125 126 List<Map<String, ?>> nameList = 127 ServiceConfigUtil.getNameListFromMethodConfig(methodConfig); 128 129 if (nameList == null || nameList.isEmpty()) { 130 continue; 131 } 132 for (Map<String, ?> name : nameList) { 133 String serviceName = ServiceConfigUtil.getServiceFromName(name); 134 String methodName = ServiceConfigUtil.getMethodFromName(name); 135 if (Strings.isNullOrEmpty(serviceName)) { 136 checkArgument( 137 Strings.isNullOrEmpty(methodName), "missing service name for method %s", methodName); 138 checkArgument( 139 defaultMethodConfig == null, 140 "Duplicate default method config in service config %s", 141 serviceConfig); 142 defaultMethodConfig = info; 143 } else if (Strings.isNullOrEmpty(methodName)) { 144 // Service scoped config 145 checkArgument( 146 !serviceMap.containsKey(serviceName), "Duplicate service %s", serviceName); 147 serviceMap.put(serviceName, info); 148 } else { 149 // Method scoped config 150 String fullMethodName = MethodDescriptor.generateFullMethodName(serviceName, methodName); 151 checkArgument( 152 !serviceMethodMap.containsKey(fullMethodName), 153 "Duplicate method name %s", 154 fullMethodName); 155 serviceMethodMap.put(fullMethodName, info); 156 } 157 } 158 } 159 160 return 161 new ManagedChannelServiceConfig( 162 defaultMethodConfig, 163 serviceMethodMap, 164 serviceMap, 165 retryThrottling, 166 loadBalancingConfig, 167 healthCheckingConfig); 168 } 169 170 @Nullable getHealthCheckingConfig()171 Map<String, ?> getHealthCheckingConfig() { 172 return healthCheckingConfig; 173 } 174 175 /** 176 * Used as a fallback per-RPC config supplier when the attributes value of {@link 177 * InternalConfigSelector#KEY} is not available. Returns {@code null} if there is no method 178 * config in this service config. 179 */ 180 @Nullable getDefaultConfigSelector()181 InternalConfigSelector getDefaultConfigSelector() { 182 if (serviceMap.isEmpty() && serviceMethodMap.isEmpty() && defaultMethodConfig == null) { 183 return null; 184 } 185 return new ServiceConfigConvertedSelector(this); 186 } 187 188 @VisibleForTesting 189 @Nullable getLoadBalancingConfig()190 Object getLoadBalancingConfig() { 191 return loadBalancingConfig; 192 } 193 194 @Nullable getRetryThrottling()195 Throttle getRetryThrottling() { 196 return retryThrottling; 197 } 198 199 @Nullable getMethodConfig(MethodDescriptor<?, ?> method)200 MethodInfo getMethodConfig(MethodDescriptor<?, ?> method) { 201 MethodInfo methodInfo = serviceMethodMap.get(method.getFullMethodName()); 202 if (methodInfo == null) { 203 String serviceName = method.getServiceName(); 204 methodInfo = serviceMap.get(serviceName); 205 } 206 if (methodInfo == null) { 207 methodInfo = defaultMethodConfig; 208 } 209 return methodInfo; 210 } 211 212 @Override equals(Object o)213 public boolean equals(Object o) { 214 if (this == o) { 215 return true; 216 } 217 if (o == null || getClass() != o.getClass()) { 218 return false; 219 } 220 ManagedChannelServiceConfig that = (ManagedChannelServiceConfig) o; 221 return Objects.equal(defaultMethodConfig, that.defaultMethodConfig) 222 && Objects.equal(serviceMethodMap, that.serviceMethodMap) 223 && Objects.equal(serviceMap, that.serviceMap) 224 && Objects.equal(retryThrottling, that.retryThrottling) 225 && Objects.equal(loadBalancingConfig, that.loadBalancingConfig); 226 } 227 228 @Override hashCode()229 public int hashCode() { 230 return Objects.hashCode( 231 defaultMethodConfig, serviceMethodMap, serviceMap, retryThrottling, loadBalancingConfig); 232 } 233 234 @Override toString()235 public String toString() { 236 return MoreObjects.toStringHelper(this) 237 .add("defaultMethodConfig", defaultMethodConfig) 238 .add("serviceMethodMap", serviceMethodMap) 239 .add("serviceMap", serviceMap) 240 .add("retryThrottling", retryThrottling) 241 .add("loadBalancingConfig", loadBalancingConfig) 242 .toString(); 243 } 244 245 /** 246 * Equivalent of MethodConfig from a ServiceConfig with restrictions from Channel setting. 247 */ 248 static final class MethodInfo { 249 static final CallOptions.Key<MethodInfo> KEY = 250 CallOptions.Key.create("io.grpc.internal.ManagedChannelServiceConfig.MethodInfo"); 251 252 // TODO(carl-mastrangelo): add getters for these fields and make them private. 253 final Long timeoutNanos; 254 final Boolean waitForReady; 255 final Integer maxInboundMessageSize; 256 final Integer maxOutboundMessageSize; 257 final RetryPolicy retryPolicy; 258 final HedgingPolicy hedgingPolicy; 259 260 /** 261 * Constructor. 262 * 263 * @param retryEnabled when false, the argument maxRetryAttemptsLimit will have no effect. 264 */ MethodInfo( Map<String, ?> methodConfig, boolean retryEnabled, int maxRetryAttemptsLimit, int maxHedgedAttemptsLimit)265 MethodInfo( 266 Map<String, ?> methodConfig, boolean retryEnabled, int maxRetryAttemptsLimit, 267 int maxHedgedAttemptsLimit) { 268 timeoutNanos = ServiceConfigUtil.getTimeoutFromMethodConfig(methodConfig); 269 waitForReady = ServiceConfigUtil.getWaitForReadyFromMethodConfig(methodConfig); 270 maxInboundMessageSize = 271 ServiceConfigUtil.getMaxResponseMessageBytesFromMethodConfig(methodConfig); 272 if (maxInboundMessageSize != null) { 273 checkArgument( 274 maxInboundMessageSize >= 0, 275 "maxInboundMessageSize %s exceeds bounds", maxInboundMessageSize); 276 } 277 maxOutboundMessageSize = 278 ServiceConfigUtil.getMaxRequestMessageBytesFromMethodConfig(methodConfig); 279 if (maxOutboundMessageSize != null) { 280 checkArgument( 281 maxOutboundMessageSize >= 0, 282 "maxOutboundMessageSize %s exceeds bounds", maxOutboundMessageSize); 283 } 284 285 Map<String, ?> retryPolicyMap = 286 retryEnabled ? ServiceConfigUtil.getRetryPolicyFromMethodConfig(methodConfig) : null; 287 retryPolicy = retryPolicyMap == null 288 ? null : retryPolicy(retryPolicyMap, maxRetryAttemptsLimit); 289 290 Map<String, ?> hedgingPolicyMap = 291 retryEnabled ? ServiceConfigUtil.getHedgingPolicyFromMethodConfig(methodConfig) : null; 292 hedgingPolicy = hedgingPolicyMap == null 293 ? null : hedgingPolicy(hedgingPolicyMap, maxHedgedAttemptsLimit); 294 } 295 296 @Override hashCode()297 public int hashCode() { 298 return Objects.hashCode( 299 timeoutNanos, 300 waitForReady, 301 maxInboundMessageSize, 302 maxOutboundMessageSize, 303 retryPolicy, 304 hedgingPolicy); 305 } 306 307 @Override equals(Object other)308 public boolean equals(Object other) { 309 if (!(other instanceof MethodInfo)) { 310 return false; 311 } 312 MethodInfo that = (MethodInfo) other; 313 return Objects.equal(this.timeoutNanos, that.timeoutNanos) 314 && Objects.equal(this.waitForReady, that.waitForReady) 315 && Objects.equal(this.maxInboundMessageSize, that.maxInboundMessageSize) 316 && Objects.equal(this.maxOutboundMessageSize, that.maxOutboundMessageSize) 317 && Objects.equal(this.retryPolicy, that.retryPolicy) 318 && Objects.equal(this.hedgingPolicy, that.hedgingPolicy); 319 } 320 321 @Override toString()322 public String toString() { 323 return MoreObjects.toStringHelper(this) 324 .add("timeoutNanos", timeoutNanos) 325 .add("waitForReady", waitForReady) 326 .add("maxInboundMessageSize", maxInboundMessageSize) 327 .add("maxOutboundMessageSize", maxOutboundMessageSize) 328 .add("retryPolicy", retryPolicy) 329 .add("hedgingPolicy", hedgingPolicy) 330 .toString(); 331 } 332 retryPolicy(Map<String, ?> retryPolicy, int maxAttemptsLimit)333 private static RetryPolicy retryPolicy(Map<String, ?> retryPolicy, int maxAttemptsLimit) { 334 int maxAttempts = checkNotNull( 335 ServiceConfigUtil.getMaxAttemptsFromRetryPolicy(retryPolicy), 336 "maxAttempts cannot be empty"); 337 checkArgument(maxAttempts >= 2, "maxAttempts must be greater than 1: %s", maxAttempts); 338 maxAttempts = Math.min(maxAttempts, maxAttemptsLimit); 339 340 long initialBackoffNanos = checkNotNull( 341 ServiceConfigUtil.getInitialBackoffNanosFromRetryPolicy(retryPolicy), 342 "initialBackoff cannot be empty"); 343 checkArgument( 344 initialBackoffNanos > 0, 345 "initialBackoffNanos must be greater than 0: %s", 346 initialBackoffNanos); 347 348 long maxBackoffNanos = checkNotNull( 349 ServiceConfigUtil.getMaxBackoffNanosFromRetryPolicy(retryPolicy), 350 "maxBackoff cannot be empty"); 351 checkArgument( 352 maxBackoffNanos > 0, "maxBackoff must be greater than 0: %s", maxBackoffNanos); 353 354 double backoffMultiplier = checkNotNull( 355 ServiceConfigUtil.getBackoffMultiplierFromRetryPolicy(retryPolicy), 356 "backoffMultiplier cannot be empty"); 357 checkArgument( 358 backoffMultiplier > 0, 359 "backoffMultiplier must be greater than 0: %s", 360 backoffMultiplier); 361 362 Long perAttemptRecvTimeout = 363 ServiceConfigUtil.getPerAttemptRecvTimeoutNanosFromRetryPolicy(retryPolicy); 364 checkArgument( 365 perAttemptRecvTimeout == null || perAttemptRecvTimeout >= 0, 366 "perAttemptRecvTimeout cannot be negative: %s", 367 perAttemptRecvTimeout); 368 369 Set<Code> retryableCodes = 370 ServiceConfigUtil.getRetryableStatusCodesFromRetryPolicy(retryPolicy); 371 checkArgument( 372 perAttemptRecvTimeout != null || !retryableCodes.isEmpty(), 373 "retryableStatusCodes cannot be empty without perAttemptRecvTimeout"); 374 375 return new RetryPolicy( 376 maxAttempts, initialBackoffNanos, maxBackoffNanos, backoffMultiplier, 377 perAttemptRecvTimeout, retryableCodes); 378 } 379 hedgingPolicy( Map<String, ?> hedgingPolicy, int maxAttemptsLimit)380 private static HedgingPolicy hedgingPolicy( 381 Map<String, ?> hedgingPolicy, int maxAttemptsLimit) { 382 int maxAttempts = checkNotNull( 383 ServiceConfigUtil.getMaxAttemptsFromHedgingPolicy(hedgingPolicy), 384 "maxAttempts cannot be empty"); 385 checkArgument(maxAttempts >= 2, "maxAttempts must be greater than 1: %s", maxAttempts); 386 maxAttempts = Math.min(maxAttempts, maxAttemptsLimit); 387 388 long hedgingDelayNanos = checkNotNull( 389 ServiceConfigUtil.getHedgingDelayNanosFromHedgingPolicy(hedgingPolicy), 390 "hedgingDelay cannot be empty"); 391 checkArgument( 392 hedgingDelayNanos >= 0, "hedgingDelay must not be negative: %s", hedgingDelayNanos); 393 394 return new HedgingPolicy( 395 maxAttempts, hedgingDelayNanos, 396 ServiceConfigUtil.getNonFatalStatusCodesFromHedgingPolicy(hedgingPolicy)); 397 } 398 } 399 400 static final class ServiceConfigConvertedSelector extends InternalConfigSelector { 401 402 final ManagedChannelServiceConfig config; 403 404 /** Converts the service config to config selector. */ ServiceConfigConvertedSelector(ManagedChannelServiceConfig config)405 private ServiceConfigConvertedSelector(ManagedChannelServiceConfig config) { 406 this.config = config; 407 } 408 409 @Override selectConfig(PickSubchannelArgs args)410 public Result selectConfig(PickSubchannelArgs args) { 411 return Result.newBuilder() 412 .setConfig(config) 413 .build(); 414 } 415 } 416 } 417