Handle gevent exception in gevent poller (#26058)
* Handle gevent exception in gevent poller Currently the gevent poller ignores exceptions raised by `gevent.wait()`, which causes greenlets to be unkilable while waiting. This change handles exceptions raised while waiting in the gevent poller, cancels the gRPC call and propagates the error back to the application. Co-authored-by: Kostis Lolos <klolos@arrikto.com> * Fix imports in header files * Lint gevent tests * Set grpc event type to GRPC_QUEUE_SHUTDOWN upon cancel error To prevent `grpc_completion_queue_next()` to be called indefinitely when the queue is shut down. * Remove unnecessary `except *` * Improve gevent tests * Format code * Remove unnecessary import Co-authored-by: Kostis Lolos <klolos@arrikto.com>reviewable/pr25586/r5^2
parent
c97da0fc25
commit
aaa7f13b17
18 changed files with 245 additions and 23 deletions
@ -0,0 +1,13 @@ |
||||
# Copyright 2021 The gRPC Authors |
||||
# |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||
# you may not use this file except in compliance with the License. |
||||
# You may obtain a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, |
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
# See the License for the specific language governing permissions and |
||||
# limitations under the License. |
@ -0,0 +1 @@ |
||||
["unit.close_channel_test.CloseChannelTest"] |
@ -0,0 +1,13 @@ |
||||
# Copyright 2021 The gRPC Authors |
||||
# |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||
# you may not use this file except in compliance with the License. |
||||
# You may obtain a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, |
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
# See the License for the specific language governing permissions and |
||||
# limitations under the License. |
@ -0,0 +1,58 @@ |
||||
# Copyright 2021 The gRPC Authors |
||||
# |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||
# you may not use this file except in compliance with the License. |
||||
# You may obtain a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, |
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
# See the License for the specific language governing permissions and |
||||
# limitations under the License. |
||||
|
||||
from concurrent import futures |
||||
from src.proto.grpc.testing import messages_pb2, test_pb2_grpc |
||||
import grpc |
||||
import gevent |
||||
from typing import Any, Tuple |
||||
|
||||
LONG_UNARY_CALL_WITH_SLEEP_VALUE = 1 |
||||
|
||||
|
||||
class TestServiceServicer(test_pb2_grpc.TestServiceServicer): |
||||
|
||||
def UnaryCall(self, request, context): |
||||
return messages_pb2.SimpleResponse() |
||||
|
||||
def UnaryCallWithSleep(self, unused_request, unused_context): |
||||
gevent.sleep(LONG_UNARY_CALL_WITH_SLEEP_VALUE) |
||||
return messages_pb2.SimpleResponse() |
||||
|
||||
|
||||
def start_test_server(port: int = 0) -> Tuple[str, Any]: |
||||
server = grpc.server(futures.ThreadPoolExecutor()) |
||||
servicer = TestServiceServicer() |
||||
test_pb2_grpc.add_TestServiceServicer_to_server(TestServiceServicer(), |
||||
server) |
||||
|
||||
server.add_generic_rpc_handlers((_create_extra_generic_handler(servicer),)) |
||||
port = server.add_insecure_port('[::]:%d' % port) |
||||
server.start() |
||||
return 'localhost:%d' % port, server |
||||
|
||||
|
||||
def _create_extra_generic_handler(servicer: TestServiceServicer) -> Any: |
||||
# Add programatically extra methods not provided by the proto file |
||||
# that are used during the tests |
||||
rpc_method_handlers = { |
||||
'UnaryCallWithSleep': |
||||
grpc.unary_unary_rpc_method_handler( |
||||
servicer.UnaryCallWithSleep, |
||||
request_deserializer=messages_pb2.SimpleRequest.FromString, |
||||
response_serializer=messages_pb2.SimpleResponse. |
||||
SerializeToString) |
||||
} |
||||
return grpc.method_handlers_generic_handler('grpc.testing.TestService', |
||||
rpc_method_handlers) |
@ -0,0 +1,102 @@ |
||||
# Copyright 2021 The gRPC Authors |
||||
# |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||
# you may not use this file except in compliance with the License. |
||||
# You may obtain a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, |
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
# See the License for the specific language governing permissions and |
||||
# limitations under the License. |
||||
|
||||
import unittest |
||||
from src.proto.grpc.testing import messages_pb2, test_pb2_grpc |
||||
import grpc |
||||
import gevent |
||||
import sys |
||||
from gevent.pool import Group |
||||
from tests_gevent.unit._test_server import start_test_server |
||||
|
||||
_UNARY_CALL_METHOD_WITH_SLEEP = '/grpc.testing.TestService/UnaryCallWithSleep' |
||||
|
||||
|
||||
class CloseChannelTest(unittest.TestCase): |
||||
|
||||
def setUp(self): |
||||
self._server_target, self._server = start_test_server() |
||||
self._channel = grpc.insecure_channel(self._server_target) |
||||
self._unhandled_exception = False |
||||
sys.excepthook = self._global_exception_handler |
||||
|
||||
def tearDown(self): |
||||
self._channel.close() |
||||
self._server.stop(None) |
||||
|
||||
def test_graceful_close(self): |
||||
stub = test_pb2_grpc.TestServiceStub(self._channel) |
||||
_, response = stub.UnaryCall.with_call(messages_pb2.SimpleRequest()) |
||||
|
||||
self._channel.close() |
||||
|
||||
self.assertEqual(grpc.StatusCode.OK, response.code()) |
||||
|
||||
def test_graceful_close_in_greenlet(self): |
||||
group = Group() |
||||
stub = test_pb2_grpc.TestServiceStub(self._channel) |
||||
greenlet = group.spawn(self._run_client, stub.UnaryCall) |
||||
# release loop so that greenlet can take control |
||||
gevent.sleep() |
||||
self._channel.close() |
||||
group.killone(greenlet) |
||||
self.assertFalse(self._unhandled_exception, "Unhandled GreenletExit") |
||||
try: |
||||
greenlet.get() |
||||
except Exception as e: # pylint: disable=broad-except |
||||
self.fail(f"Unexpected exception in greenlet: {e}") |
||||
|
||||
def test_ungraceful_close_in_greenlet(self): |
||||
group = Group() |
||||
UnaryCallWithSleep = self._channel.unary_unary( |
||||
_UNARY_CALL_METHOD_WITH_SLEEP, |
||||
request_serializer=messages_pb2.SimpleRequest.SerializeToString, |
||||
response_deserializer=messages_pb2.SimpleResponse.FromString, |
||||
) |
||||
greenlet = group.spawn(self._run_client, UnaryCallWithSleep) |
||||
# release loop so that greenlet can take control |
||||
gevent.sleep() |
||||
group.killone(greenlet) |
||||
self.assertFalse(self._unhandled_exception, "Unhandled GreenletExit") |
||||
|
||||
def test_kill_greenlet_with_generic_exception(self): |
||||
group = Group() |
||||
UnaryCallWithSleep = self._channel.unary_unary( |
||||
_UNARY_CALL_METHOD_WITH_SLEEP, |
||||
request_serializer=messages_pb2.SimpleRequest.SerializeToString, |
||||
response_deserializer=messages_pb2.SimpleResponse.FromString, |
||||
) |
||||
greenlet = group.spawn(self._run_client, UnaryCallWithSleep) |
||||
# release loop so that greenlet can take control |
||||
gevent.sleep() |
||||
group.killone(greenlet, exception=Exception) |
||||
self.assertFalse(self._unhandled_exception, "Unhandled exception") |
||||
self.assertRaises(Exception, greenlet.get) |
||||
|
||||
def _run_client(self, call): |
||||
try: |
||||
call.with_call(messages_pb2.SimpleRequest()) |
||||
except grpc.RpcError as e: |
||||
if e.code() != grpc.StatusCode.CANCELLED: |
||||
raise |
||||
|
||||
def _global_exception_handler(self, exctype, value, tb): |
||||
if exctype == gevent.GreenletExit: |
||||
self._unhandled_exception = True |
||||
return |
||||
sys.__excepthook__(exctype, value, tb) |
||||
|
||||
|
||||
if __name__ == '__main__': |
||||
unittest.main(verbosity=2) |
Loading…
Reference in new issue