Initiate an asynchronous call to a gRPC stub, with retry logic. This is similar to the `async_call` API, except that the call is handled asynchronously, and the completion may be handled by another thread. The caller must provide a `done_callback` argument which will handle the resu
(api_method, request, clock=None)
| 110 | |
| 111 | |
| 112 | def async_call_with_retries(api_method, request, clock=None): |
| 113 | """Initiate an asynchronous call to a gRPC stub, with retry logic. |
| 114 | |
| 115 | This is similar to the `async_call` API, except that the call is handled |
| 116 | asynchronously, and the completion may be handled by another thread. The |
| 117 | caller must provide a `done_callback` argument which will handle the |
| 118 | result or exception rising from the gRPC completion. |
| 119 | |
| 120 | Retries are handled with jittered exponential backoff to spread out failures |
| 121 | due to request spikes. |
| 122 | |
| 123 | This only supports unary-unary RPCs: i.e., no streaming on either end. |
| 124 | |
| 125 | Args: |
| 126 | api_method: Callable for the API method to invoke. |
| 127 | request: Request protocol buffer to pass to the API method. |
| 128 | clock: an interface object supporting `time()` and `sleep()` methods |
| 129 | like the standard `time` module; if not passed, uses the normal module. |
| 130 | |
| 131 | Returns: |
| 132 | An `AsyncCallFuture` which will encapsulate the `grpc.Future` |
| 133 | corresponding to the gRPC call which either completes successfully or |
| 134 | represents the final try. |
| 135 | """ |
| 136 | if clock is None: |
| 137 | clock = time |
| 138 | logger.debug("Async RPC call %s with request: %r", api_method, request) |
| 139 | |
| 140 | completion_event = threading.Event() |
| 141 | async_future = AsyncCallFuture(completion_event) |
| 142 | |
| 143 | def async_call(handler): |
| 144 | """Invokes the gRPC future and orchestrates it via the AsyncCallFuture.""" |
| 145 | future = api_method.future( |
| 146 | request, |
| 147 | timeout=_GRPC_DEFAULT_TIMEOUT_SECS, |
| 148 | metadata=version_metadata(), |
| 149 | ) |
| 150 | # Ensure we set the active future before invoking the done callback, to |
| 151 | # avoid the case where the done callback completes immediately and |
| 152 | # triggers completion event while async_future still holds the old |
| 153 | # future. |
| 154 | async_future._set_active_future(future) |
| 155 | future.add_done_callback(handler) |
| 156 | |
| 157 | # retry_handler is the continuation of the `async_call`. It should: |
| 158 | # * If the grpc call succeeds: trigger the `completion_event`. |
| 159 | # * If there are no more retries: trigger the `completion_event`. |
| 160 | # * Otherwise, invoke a new async_call with the same |
| 161 | # retry_handler. |
| 162 | def retry_handler(future, num_attempts): |
| 163 | e = future.exception() |
| 164 | if e is None: |
| 165 | completion_event.set() |
| 166 | return |
| 167 | else: |
| 168 | logger.info("RPC call %s got error %s", api_method, e) |
| 169 | # If unable to retry, proceed to completion. |
nothing calls this directly
no test coverage detected
searching dependent graphs…