mirror of https://github.com/grpc/grpc.git
Merge pull request #22688 from lidizheng/aio-status
[Aio] Add AsyncIO support to grpcio-statuspull/22740/head
commit
4e7f3376d8
9 changed files with 322 additions and 17 deletions
@ -0,0 +1,56 @@ |
|||||||
|
# Copyright 2020 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. |
||||||
|
"""Reference implementation for status mapping in gRPC Python.""" |
||||||
|
|
||||||
|
from grpc.experimental import aio |
||||||
|
|
||||||
|
from google.rpc import status_pb2 |
||||||
|
|
||||||
|
from ._common import code_to_grpc_status_code, GRPC_DETAILS_METADATA_KEY |
||||||
|
|
||||||
|
|
||||||
|
async def from_call(call: aio.Call): |
||||||
|
"""Returns a google.rpc.status.Status message from a given grpc.aio.Call. |
||||||
|
|
||||||
|
This is an EXPERIMENTAL API. |
||||||
|
|
||||||
|
Args: |
||||||
|
call: An grpc.aio.Call instance. |
||||||
|
|
||||||
|
Returns: |
||||||
|
A google.rpc.status.Status message representing the status of the RPC. |
||||||
|
""" |
||||||
|
code = await call.code() |
||||||
|
details = await call.details() |
||||||
|
trailing_metadata = await call.trailing_metadata() |
||||||
|
if trailing_metadata is None: |
||||||
|
return None |
||||||
|
for key, value in trailing_metadata: |
||||||
|
if key == GRPC_DETAILS_METADATA_KEY: |
||||||
|
rich_status = status_pb2.Status.FromString(value) |
||||||
|
if code.value[0] != rich_status.code: |
||||||
|
raise ValueError( |
||||||
|
'Code in Status proto (%s) doesn\'t match status code (%s)' |
||||||
|
% (code_to_grpc_status_code(rich_status.code), code)) |
||||||
|
if details != rich_status.message: |
||||||
|
raise ValueError( |
||||||
|
'Message in Status proto (%s) doesn\'t match status details (%s)' |
||||||
|
% (rich_status.message, details)) |
||||||
|
return rich_status |
||||||
|
return None |
||||||
|
|
||||||
|
|
||||||
|
__all__ = [ |
||||||
|
'from_call', |
||||||
|
] |
@ -0,0 +1,27 @@ |
|||||||
|
# Copyright 2020 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. |
||||||
|
"""Reference implementation for status mapping in gRPC Python.""" |
||||||
|
|
||||||
|
import grpc |
||||||
|
|
||||||
|
_CODE_TO_GRPC_CODE_MAPPING = {x.value[0]: x for x in grpc.StatusCode} |
||||||
|
|
||||||
|
GRPC_DETAILS_METADATA_KEY = 'grpc-status-details-bin' |
||||||
|
|
||||||
|
|
||||||
|
def code_to_grpc_status_code(code): |
||||||
|
try: |
||||||
|
return _CODE_TO_GRPC_CODE_MAPPING[code] |
||||||
|
except KeyError: |
||||||
|
raise ValueError('Invalid status code %s' % code) |
@ -0,0 +1,30 @@ |
|||||||
|
# Copyright 2020 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. |
||||||
|
|
||||||
|
load("@grpc_python_dependencies//:requirements.bzl", "requirement") |
||||||
|
|
||||||
|
py_test( |
||||||
|
name = "grpc_status_test", |
||||||
|
size = "small", |
||||||
|
srcs = ["grpc_status_test.py"], |
||||||
|
imports = ["../../"], |
||||||
|
python_version = "PY3", |
||||||
|
deps = [ |
||||||
|
"//src/python/grpcio/grpc:grpcio", |
||||||
|
"//src/python/grpcio_status/grpc_status", |
||||||
|
"//src/python/grpcio_tests/tests_aio/unit:_test_base", |
||||||
|
requirement("protobuf"), |
||||||
|
requirement("googleapis-common-protos"), |
||||||
|
], |
||||||
|
) |
@ -0,0 +1,13 @@ |
|||||||
|
# Copyright 2020 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,175 @@ |
|||||||
|
# Copyright 2020 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. |
||||||
|
"""Tests of grpc_status with gRPC AsyncIO stack.""" |
||||||
|
|
||||||
|
import logging |
||||||
|
import traceback |
||||||
|
import unittest |
||||||
|
|
||||||
|
import grpc |
||||||
|
from google.protobuf import any_pb2 |
||||||
|
from google.rpc import code_pb2, error_details_pb2, status_pb2 |
||||||
|
from grpc.experimental import aio |
||||||
|
|
||||||
|
from grpc_status import rpc_status |
||||||
|
from tests_aio.unit._test_base import AioTestBase |
||||||
|
|
||||||
|
_STATUS_OK = '/test/StatusOK' |
||||||
|
_STATUS_NOT_OK = '/test/StatusNotOk' |
||||||
|
_ERROR_DETAILS = '/test/ErrorDetails' |
||||||
|
_INCONSISTENT = '/test/Inconsistent' |
||||||
|
_INVALID_CODE = '/test/InvalidCode' |
||||||
|
|
||||||
|
_REQUEST = b'\x00\x00\x00' |
||||||
|
_RESPONSE = b'\x01\x01\x01' |
||||||
|
|
||||||
|
_GRPC_DETAILS_METADATA_KEY = 'grpc-status-details-bin' |
||||||
|
|
||||||
|
_STATUS_DETAILS = 'This is an error detail' |
||||||
|
_STATUS_DETAILS_ANOTHER = 'This is another error detail' |
||||||
|
|
||||||
|
|
||||||
|
async def _ok_unary_unary(request, servicer_context): |
||||||
|
return _RESPONSE |
||||||
|
|
||||||
|
|
||||||
|
async def _not_ok_unary_unary(request, servicer_context): |
||||||
|
await servicer_context.abort(grpc.StatusCode.INTERNAL, _STATUS_DETAILS) |
||||||
|
|
||||||
|
|
||||||
|
async def _error_details_unary_unary(request, servicer_context): |
||||||
|
details = any_pb2.Any() |
||||||
|
details.Pack( |
||||||
|
error_details_pb2.DebugInfo(stack_entries=traceback.format_stack(), |
||||||
|
detail='Intentionally invoked')) |
||||||
|
rich_status = status_pb2.Status( |
||||||
|
code=code_pb2.INTERNAL, |
||||||
|
message=_STATUS_DETAILS, |
||||||
|
details=[details], |
||||||
|
) |
||||||
|
await servicer_context.abort_with_status(rpc_status.to_status(rich_status)) |
||||||
|
|
||||||
|
|
||||||
|
async def _inconsistent_unary_unary(request, servicer_context): |
||||||
|
rich_status = status_pb2.Status( |
||||||
|
code=code_pb2.INTERNAL, |
||||||
|
message=_STATUS_DETAILS, |
||||||
|
) |
||||||
|
servicer_context.set_code(grpc.StatusCode.NOT_FOUND) |
||||||
|
servicer_context.set_details(_STATUS_DETAILS_ANOTHER) |
||||||
|
# User put inconsistent status information in trailing metadata |
||||||
|
servicer_context.set_trailing_metadata( |
||||||
|
((_GRPC_DETAILS_METADATA_KEY, rich_status.SerializeToString()),)) |
||||||
|
|
||||||
|
|
||||||
|
async def _invalid_code_unary_unary(request, servicer_context): |
||||||
|
rich_status = status_pb2.Status( |
||||||
|
code=42, |
||||||
|
message='Invalid code', |
||||||
|
) |
||||||
|
await servicer_context.abort_with_status(rpc_status.to_status(rich_status)) |
||||||
|
|
||||||
|
|
||||||
|
class _GenericHandler(grpc.GenericRpcHandler): |
||||||
|
|
||||||
|
def service(self, handler_call_details): |
||||||
|
if handler_call_details.method == _STATUS_OK: |
||||||
|
return grpc.unary_unary_rpc_method_handler(_ok_unary_unary) |
||||||
|
elif handler_call_details.method == _STATUS_NOT_OK: |
||||||
|
return grpc.unary_unary_rpc_method_handler(_not_ok_unary_unary) |
||||||
|
elif handler_call_details.method == _ERROR_DETAILS: |
||||||
|
return grpc.unary_unary_rpc_method_handler( |
||||||
|
_error_details_unary_unary) |
||||||
|
elif handler_call_details.method == _INCONSISTENT: |
||||||
|
return grpc.unary_unary_rpc_method_handler( |
||||||
|
_inconsistent_unary_unary) |
||||||
|
elif handler_call_details.method == _INVALID_CODE: |
||||||
|
return grpc.unary_unary_rpc_method_handler( |
||||||
|
_invalid_code_unary_unary) |
||||||
|
else: |
||||||
|
return None |
||||||
|
|
||||||
|
|
||||||
|
class StatusTest(AioTestBase): |
||||||
|
|
||||||
|
async def setUp(self): |
||||||
|
self._server = aio.server() |
||||||
|
self._server.add_generic_rpc_handlers((_GenericHandler(),)) |
||||||
|
port = self._server.add_insecure_port('[::]:0') |
||||||
|
await self._server.start() |
||||||
|
|
||||||
|
self._channel = aio.insecure_channel('localhost:%d' % port) |
||||||
|
|
||||||
|
async def tearDown(self): |
||||||
|
await self._server.stop(None) |
||||||
|
await self._channel.close() |
||||||
|
|
||||||
|
async def test_status_ok(self): |
||||||
|
call = self._channel.unary_unary(_STATUS_OK)(_REQUEST) |
||||||
|
|
||||||
|
# Succeed RPC doesn't have status |
||||||
|
status = await rpc_status.aio.from_call(call) |
||||||
|
self.assertIs(status, None) |
||||||
|
|
||||||
|
async def test_status_not_ok(self): |
||||||
|
call = self._channel.unary_unary(_STATUS_NOT_OK)(_REQUEST) |
||||||
|
with self.assertRaises(aio.AioRpcError) as exception_context: |
||||||
|
await call |
||||||
|
rpc_error = exception_context.exception |
||||||
|
|
||||||
|
self.assertEqual(rpc_error.code(), grpc.StatusCode.INTERNAL) |
||||||
|
# Failed RPC doesn't automatically generate status |
||||||
|
status = await rpc_status.aio.from_call(call) |
||||||
|
self.assertIs(status, None) |
||||||
|
|
||||||
|
async def test_error_details(self): |
||||||
|
call = self._channel.unary_unary(_ERROR_DETAILS)(_REQUEST) |
||||||
|
with self.assertRaises(aio.AioRpcError) as exception_context: |
||||||
|
await call |
||||||
|
rpc_error = exception_context.exception |
||||||
|
|
||||||
|
status = await rpc_status.aio.from_call(call) |
||||||
|
self.assertEqual(rpc_error.code(), grpc.StatusCode.INTERNAL) |
||||||
|
self.assertEqual(status.code, code_pb2.Code.Value('INTERNAL')) |
||||||
|
|
||||||
|
# Check if the underlying proto message is intact |
||||||
|
self.assertTrue(status.details[0].Is( |
||||||
|
error_details_pb2.DebugInfo.DESCRIPTOR)) |
||||||
|
info = error_details_pb2.DebugInfo() |
||||||
|
status.details[0].Unpack(info) |
||||||
|
self.assertIn('_error_details_unary_unary', info.stack_entries[-1]) |
||||||
|
|
||||||
|
async def test_code_message_validation(self): |
||||||
|
call = self._channel.unary_unary(_INCONSISTENT)(_REQUEST) |
||||||
|
with self.assertRaises(aio.AioRpcError) as exception_context: |
||||||
|
await call |
||||||
|
rpc_error = exception_context.exception |
||||||
|
self.assertEqual(rpc_error.code(), grpc.StatusCode.NOT_FOUND) |
||||||
|
|
||||||
|
# Code/Message validation failed |
||||||
|
with self.assertRaises(ValueError): |
||||||
|
await rpc_status.aio.from_call(call) |
||||||
|
|
||||||
|
async def test_invalid_code(self): |
||||||
|
with self.assertRaises(aio.AioRpcError) as exception_context: |
||||||
|
await self._channel.unary_unary(_INVALID_CODE)(_REQUEST) |
||||||
|
rpc_error = exception_context.exception |
||||||
|
self.assertEqual(rpc_error.code(), grpc.StatusCode.UNKNOWN) |
||||||
|
# Invalid status code exception raised during coversion |
||||||
|
self.assertIn('Invalid status code', rpc_error.details()) |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__': |
||||||
|
logging.basicConfig() |
||||||
|
unittest.main(verbosity=2) |
Loading…
Reference in new issue