Call a gRPC stub API method, with automatic retry logic. This only supports unary-unary RPCs: i.e., no streaming on either end. Streamed RPCs will generally need application-level pagination support, because after a gRPC error one must retry the entire request; there is no "retry-re
(api_method, request, clock=None)
| 194 | |
| 195 | |
| 196 | def call_with_retries(api_method, request, clock=None): |
| 197 | """Call a gRPC stub API method, with automatic retry logic. |
| 198 | |
| 199 | This only supports unary-unary RPCs: i.e., no streaming on either end. |
| 200 | Streamed RPCs will generally need application-level pagination support, |
| 201 | because after a gRPC error one must retry the entire request; there is no |
| 202 | "retry-resume" functionality. |
| 203 | |
| 204 | Retries are handled with jittered exponential backoff to spread out failures |
| 205 | due to request spikes. |
| 206 | |
| 207 | Args: |
| 208 | api_method: Callable for the API method to invoke. |
| 209 | request: Request protocol buffer to pass to the API method. |
| 210 | clock: an interface object supporting `time()` and `sleep()` methods |
| 211 | like the standard `time` module; if not passed, uses the normal module. |
| 212 | |
| 213 | Returns: |
| 214 | Response protocol buffer returned by the API method. |
| 215 | |
| 216 | Raises: |
| 217 | grpc.RpcError: if a non-retryable error is returned, or if all retry |
| 218 | attempts have been exhausted. |
| 219 | """ |
| 220 | if clock is None: |
| 221 | clock = time |
| 222 | # We can't actually use api_method.__name__ because it's not a real method, |
| 223 | # it's a special gRPC callable instance that doesn't expose the method name. |
| 224 | rpc_name = request.__class__.__name__.replace("Request", "") |
| 225 | logger.debug("RPC call %s with request: %r", rpc_name, request) |
| 226 | num_attempts = 0 |
| 227 | while True: |
| 228 | num_attempts += 1 |
| 229 | try: |
| 230 | return api_method( |
| 231 | request, |
| 232 | timeout=_GRPC_DEFAULT_TIMEOUT_SECS, |
| 233 | metadata=version_metadata(), |
| 234 | ) |
| 235 | except grpc.RpcError as e: |
| 236 | logger.info("RPC call %s got error %s", rpc_name, e) |
| 237 | if e.code() not in _GRPC_RETRYABLE_STATUS_CODES: |
| 238 | raise |
| 239 | if num_attempts >= _GRPC_RETRY_MAX_ATTEMPTS: |
| 240 | raise |
| 241 | backoff_secs = _compute_backoff_seconds(num_attempts) |
| 242 | logger.info( |
| 243 | "RPC call %s attempted %d times, retrying in %.1f seconds", |
| 244 | rpc_name, |
| 245 | num_attempts, |
| 246 | backoff_secs, |
| 247 | ) |
| 248 | clock.sleep(backoff_secs) |
| 249 | |
| 250 | |
| 251 | def version_metadata(): |
nothing calls this directly
no test coverage detected
searching dependent graphs…