|
|
|
@ -40,30 +40,16 @@ stub = hash_name_pb2_grpc.HashFinderStub(channel) |
|
|
|
|
future = stub.Find.future(hash_name_pb2.HashNameRequest(desired_name=name)) |
|
|
|
|
def cancel_request(unused_signum, unused_frame): |
|
|
|
|
future.cancel() |
|
|
|
|
sys.exit(0) |
|
|
|
|
signal.signal(signal.SIGINT, cancel_request) |
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
It's also important that you not block indefinitely on the RPC. Otherwise, the |
|
|
|
|
signal handler will never have a chance to run. |
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
while True: |
|
|
|
|
try: |
|
|
|
|
result = future.result(timeout=_TIMEOUT_SECONDS) |
|
|
|
|
except grpc.FutureTimeoutError: |
|
|
|
|
continue |
|
|
|
|
except grpc.FutureCancelledError: |
|
|
|
|
break |
|
|
|
|
print("Got response: \n{}".format(result)) |
|
|
|
|
break |
|
|
|
|
result = future.result() |
|
|
|
|
print(result) |
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
Here, we repeatedly block on a result for up to `_TIMEOUT_SECONDS`. Doing so |
|
|
|
|
gives the signal handlers a chance to run. In the case that our timeout |
|
|
|
|
was reached, we simply continue on in the loop. In the case that the RPC was |
|
|
|
|
cancelled (by our user's ctrl+c), we break out of the loop cleanly. Finally, if |
|
|
|
|
we received the result of the RPC, we print it out for the user and exit the |
|
|
|
|
loop. |
|
|
|
|
We also call `sys.exit(0)` to terminate the process. If we do not do this, then |
|
|
|
|
`future.result()` with throw an `RpcError`. Alternatively, you may catch this |
|
|
|
|
exception. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
##### Cancelling a Server-Side Streaming RPC from the Client |
|
|
|
@ -78,53 +64,15 @@ stub = hash_name_pb2_grpc.HashFinderStub(channel) |
|
|
|
|
result_generator = stub.FindRange(hash_name_pb2.HashNameRequest(desired_name=name)) |
|
|
|
|
def cancel_request(unused_signum, unused_frame): |
|
|
|
|
result_generator.cancel() |
|
|
|
|
sys.exit(0) |
|
|
|
|
signal.signal(signal.SIGINT, cancel_request) |
|
|
|
|
for result in result_generator: |
|
|
|
|
print(result) |
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
However, the streaming case is complicated by the fact that there is no way to |
|
|
|
|
propagate a timeout to Python generators. As a result, simply iterating over the |
|
|
|
|
results of the RPC can block indefinitely and the signal handler may never run. |
|
|
|
|
Instead, we iterate over the generator on another thread and retrieve the |
|
|
|
|
results on the main thread with a synchronized `Queue`. |
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
result_queue = Queue() |
|
|
|
|
def iterate_responses(result_generator, result_queue): |
|
|
|
|
try: |
|
|
|
|
for result in result_generator: |
|
|
|
|
result_queue.put(result) |
|
|
|
|
except grpc.RpcError as rpc_error: |
|
|
|
|
if rpc_error.code() != grpc.StatusCode.CANCELLED: |
|
|
|
|
result_queue.put(None) |
|
|
|
|
raise rpc_error |
|
|
|
|
result_queue.put(None) |
|
|
|
|
print("RPC complete") |
|
|
|
|
response_thread = threading.Thread(target=iterate_responses, args=(result_generator, result_queue)) |
|
|
|
|
response_thread.daemon = True |
|
|
|
|
response_thread.start() |
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
While this thread iterating over the results may block indefinitely, we can |
|
|
|
|
structure the code running on our main thread in such a way that signal handlers |
|
|
|
|
are guaranteed to be run at least every `_TIMEOUT_SECONDS` seconds. |
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
while result_generator.running(): |
|
|
|
|
try: |
|
|
|
|
result = result_queue.get(timeout=_TIMEOUT_SECONDS) |
|
|
|
|
except QueueEmpty: |
|
|
|
|
continue |
|
|
|
|
if result is None: |
|
|
|
|
break |
|
|
|
|
print("Got result: {}".format(result)) |
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
Similarly to the unary example above, we continue in a loop waiting for results, |
|
|
|
|
taking care to block for intervals of `_TIMEOUT_SECONDS` at the longest. |
|
|
|
|
Finally, we use `None` as a sentinel value to signal the end of the stream. |
|
|
|
|
We also call `sys.exit(0)` here to terminate the process. Alternatively, you may |
|
|
|
|
catch the `RpcError` raised by the for loop upon cancellation. |
|
|
|
|
|
|
|
|
|
Using this scheme, our process responds nicely to `SIGINT`s while also |
|
|
|
|
explicitly cancelling its RPCs. |
|
|
|
|
|
|
|
|
|
#### Cancellation on the Server Side |
|
|
|
|
|
|
|
|
|