Skip to content

Commit fae114e

Browse files
committed
Remapping (almost) all RPC status codes to our exceptions in datastore.
Fixes #2497.
1 parent 1fab756 commit fae114e

File tree

2 files changed

+137
-98
lines changed

2 files changed

+137
-98
lines changed

datastore/google/cloud/datastore/connection.py

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,35 @@
2323
from google.cloud import connection as connection_module
2424
from google.cloud.environment_vars import DISABLE_GRPC
2525
from google.cloud.environment_vars import GCD_HOST
26-
from google.cloud.exceptions import BadRequest
27-
from google.cloud.exceptions import Conflict
28-
from google.cloud.exceptions import GrpcRendezvous
29-
from google.cloud.exceptions import make_exception
26+
from google.cloud import exceptions
3027
from google.cloud.datastore._generated import datastore_pb2 as _datastore_pb2
3128
try:
3229
from grpc import StatusCode
3330
from google.cloud.datastore._generated import datastore_grpc_pb2
3431
except ImportError: # pragma: NO COVER
32+
_GRPC_ERROR_MAPPING = {}
3533
_HAVE_GRPC = False
3634
datastore_grpc_pb2 = None
3735
StatusCode = None
3836
else:
37+
# NOTE: We don't include OK -> 200 or CANCELLED -> 499
38+
_GRPC_ERROR_MAPPING = {
39+
StatusCode.UNKNOWN: exceptions.InternalServerError,
40+
StatusCode.INVALID_ARGUMENT: exceptions.BadRequest,
41+
StatusCode.DEADLINE_EXCEEDED: exceptions.GatewayTimeout,
42+
StatusCode.NOT_FOUND: exceptions.NotFound,
43+
StatusCode.ALREADY_EXISTS: exceptions.Conflict,
44+
StatusCode.PERMISSION_DENIED: exceptions.Forbidden,
45+
StatusCode.UNAUTHENTICATED: exceptions.Unauthorized,
46+
StatusCode.RESOURCE_EXHAUSTED: exceptions.TooManyRequests,
47+
StatusCode.FAILED_PRECONDITION: exceptions.PreconditionFailed,
48+
StatusCode.ABORTED: exceptions.Conflict,
49+
StatusCode.OUT_OF_RANGE: exceptions.BadRequest,
50+
StatusCode.UNIMPLEMENTED: exceptions.MethodNotImplemented,
51+
StatusCode.INTERNAL: exceptions.InternalServerError,
52+
StatusCode.UNAVAILABLE: exceptions.ServiceUnavailable,
53+
StatusCode.DATA_LOSS: exceptions.InternalServerError,
54+
}
3955
_HAVE_GRPC = True
4056

4157

@@ -93,7 +109,8 @@ def _request(self, project, method, data):
93109
status = headers['status']
94110
if status != '200':
95111
error_status = status_pb2.Status.FromString(content)
96-
raise make_exception(headers, error_status.message, use_json=False)
112+
raise exceptions.make_exception(
113+
headers, error_status.message, use_json=False)
97114

98115
return content
99116

@@ -220,6 +237,44 @@ def allocate_ids(self, project, request_pb):
220237
_datastore_pb2.AllocateIdsResponse)
221238

222239

240+
def _grpc_catch_rendezvous(to_call, *args, **kwargs):
241+
"""Call a method/function and re-map gRPC exceptions.
242+
243+
.. _code.proto: https://github.com/googleapis/googleapis/blob/\
244+
master/google/rpc/code.proto
245+
246+
Remaps gRPC exceptions to the classes defined in
247+
:mod:`~google.cloud.exceptions` (according to the description
248+
in `code.proto`_).
249+
250+
:type to_call: callable
251+
:param to_call: Callable that makes a request which may raise a
252+
:class:`~google.cloud.exceptions.GrpcRendezvous`.
253+
254+
:type args: tuple
255+
:param args: Positional arugments to the callable.
256+
257+
:type kwargs: dict
258+
:param kwargs: Keyword arguments to the callable.
259+
260+
:rtype: object
261+
:returns: The value returned from ``to_call``.
262+
:raises: :class:`~google.cloud.exceptions.GrpcRendezvous` if one
263+
is encountered that can't be re-mapped, otherwise maps
264+
to a :class:`~google.cloud.exceptions.GoogleCloudError`
265+
subclass.
266+
"""
267+
try:
268+
return to_call(*args, **kwargs)
269+
except exceptions.GrpcRendezvous as exc:
270+
error_code = exc.code()
271+
error_class = _GRPC_ERROR_MAPPING.get(error_code)
272+
if error_class is None:
273+
raise
274+
else:
275+
raise error_class(exc.details())
276+
277+
223278
class _DatastoreAPIOverGRPC(object):
224279
"""Helper mapping datastore API methods.
225280
@@ -276,13 +331,8 @@ def run_query(self, project, request_pb):
276331
:returns: The returned protobuf response object.
277332
"""
278333
request_pb.project_id = project
279-
try:
280-
return self._stub.RunQuery(request_pb)
281-
except GrpcRendezvous as exc:
282-
error_code = exc.code()
283-
if error_code == StatusCode.INVALID_ARGUMENT:
284-
raise BadRequest(exc.details())
285-
raise
334+
return _grpc_catch_rendezvous(
335+
self._stub.RunQuery, request_pb)
286336

287337
def begin_transaction(self, project, request_pb):
288338
"""Perform a ``beginTransaction`` request.
@@ -299,7 +349,8 @@ def begin_transaction(self, project, request_pb):
299349
:returns: The returned protobuf response object.
300350
"""
301351
request_pb.project_id = project
302-
return self._stub.BeginTransaction(request_pb)
352+
return _grpc_catch_rendezvous(
353+
self._stub.BeginTransaction, request_pb)
303354

304355
def commit(self, project, request_pb):
305356
"""Perform a ``commit`` request.
@@ -315,15 +366,8 @@ def commit(self, project, request_pb):
315366
:returns: The returned protobuf response object.
316367
"""
317368
request_pb.project_id = project
318-
try:
319-
return self._stub.Commit(request_pb)
320-
except GrpcRendezvous as exc:
321-
error_code = exc.code()
322-
if error_code == StatusCode.ABORTED:
323-
raise Conflict(exc.details())
324-
if error_code == StatusCode.INVALID_ARGUMENT:
325-
raise BadRequest(exc.details())
326-
raise
369+
return _grpc_catch_rendezvous(
370+
self._stub.Commit, request_pb)
327371

328372
def rollback(self, project, request_pb):
329373
"""Perform a ``rollback`` request.
@@ -339,7 +383,8 @@ def rollback(self, project, request_pb):
339383
:returns: The returned protobuf response object.
340384
"""
341385
request_pb.project_id = project
342-
return self._stub.Rollback(request_pb)
386+
return _grpc_catch_rendezvous(
387+
self._stub.Rollback, request_pb)
343388

344389
def allocate_ids(self, project, request_pb):
345390
"""Perform an ``allocateIds`` request.
@@ -355,7 +400,8 @@ def allocate_ids(self, project, request_pb):
355400
:returns: The returned protobuf response object.
356401
"""
357402
request_pb.project_id = project
358-
return self._stub.AllocateIds(request_pb)
403+
return _grpc_catch_rendezvous(
404+
self._stub.AllocateIds, request_pb)
359405

360406

361407
class Connection(connection_module.Connection):

datastore/unit_tests/test_connection.py

Lines changed: 67 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,67 @@ def test__request_not_200(self):
106106
[{'method': METHOD, 'project': PROJECT}])
107107

108108

109+
@unittest.skipUnless(_HAVE_GRPC, 'No gRPC')
110+
class Test__grpc_catch_rendezvous(unittest.TestCase):
111+
112+
def _callFUT(self, to_call, *args, **kwargs):
113+
from google.cloud.datastore.connection import _grpc_catch_rendezvous
114+
return _grpc_catch_rendezvous(to_call, *args, **kwargs)
115+
116+
@staticmethod
117+
def _fake_method(exc, result=None):
118+
if exc is None:
119+
return result
120+
else:
121+
raise exc
122+
123+
def test_success(self):
124+
expected = object()
125+
result = self._callFUT(self._fake_method, None, expected)
126+
self.assertIs(result, expected)
127+
128+
def test_failure_aborted(self):
129+
from grpc import StatusCode
130+
from grpc._channel import _RPCState
131+
from google.cloud.exceptions import Conflict
132+
from google.cloud.exceptions import GrpcRendezvous
133+
134+
details = 'Bad things.'
135+
exc_state = _RPCState((), None, None, StatusCode.ABORTED, details)
136+
exc = GrpcRendezvous(exc_state, None, None, None)
137+
with self.assertRaises(Conflict):
138+
self._callFUT(self._fake_method, exc)
139+
140+
def test_failure_invalid_argument(self):
141+
from grpc import StatusCode
142+
from grpc._channel import _RPCState
143+
from google.cloud.exceptions import BadRequest
144+
from google.cloud.exceptions import GrpcRendezvous
145+
146+
details = ('Cannot have inequality filters on multiple '
147+
'properties: [created, priority]')
148+
exc_state = _RPCState((), None, None,
149+
StatusCode.INVALID_ARGUMENT, details)
150+
exc = GrpcRendezvous(exc_state, None, None, None)
151+
with self.assertRaises(BadRequest):
152+
self._callFUT(self._fake_method, exc)
153+
154+
def test_failure_cancelled(self):
155+
from grpc import StatusCode
156+
from grpc._channel import _RPCState
157+
from google.cloud.exceptions import GrpcRendezvous
158+
159+
exc_state = _RPCState((), None, None, StatusCode.CANCELLED, None)
160+
exc = GrpcRendezvous(exc_state, None, None, None)
161+
with self.assertRaises(GrpcRendezvous):
162+
self._callFUT(self._fake_method, exc)
163+
164+
def test_commit_failure_non_grpc_err(self):
165+
exc = RuntimeError('Not a gRPC error')
166+
with self.assertRaises(RuntimeError):
167+
self._callFUT(self._fake_method, exc)
168+
169+
109170
class Test_DatastoreAPIOverGRPC(unittest.TestCase):
110171

111172
def _getTargetClass(self):
@@ -227,16 +288,6 @@ def test_run_query_invalid_argument(self):
227288
exc = GrpcRendezvous(exc_state, None, None, None)
228289
self._run_query_failure_helper(exc, BadRequest)
229290

230-
@unittest.skipUnless(_HAVE_GRPC, 'No gRPC')
231-
def test_run_query_cancelled(self):
232-
from grpc import StatusCode
233-
from grpc._channel import _RPCState
234-
from google.cloud.exceptions import GrpcRendezvous
235-
236-
exc_state = _RPCState((), None, None, StatusCode.CANCELLED, None)
237-
exc = GrpcRendezvous(exc_state, None, None, None)
238-
self._run_query_failure_helper(exc, GrpcRendezvous)
239-
240291
def test_begin_transaction(self):
241292
return_val = object()
242293
stub = _GRPCStub(return_val)
@@ -264,59 +315,6 @@ def test_commit_success(self):
264315
self.assertEqual(stub.method_calls,
265316
[(request_pb, 'Commit')])
266317

267-
def _commit_failure_helper(self, exc, err_class):
268-
stub = _GRPCStub(side_effect=exc)
269-
datastore_api = self._makeOne(stub=stub)
270-
271-
request_pb = _RequestPB()
272-
project = 'PROJECT'
273-
with self.assertRaises(err_class):
274-
datastore_api.commit(project, request_pb)
275-
276-
self.assertEqual(request_pb.project_id, project)
277-
self.assertEqual(stub.method_calls,
278-
[(request_pb, 'Commit')])
279-
280-
@unittest.skipUnless(_HAVE_GRPC, 'No gRPC')
281-
def test_commit_failure_aborted(self):
282-
from grpc import StatusCode
283-
from grpc._channel import _RPCState
284-
from google.cloud.exceptions import Conflict
285-
from google.cloud.exceptions import GrpcRendezvous
286-
287-
details = 'Bad things.'
288-
exc_state = _RPCState((), None, None, StatusCode.ABORTED, details)
289-
exc = GrpcRendezvous(exc_state, None, None, None)
290-
self._commit_failure_helper(exc, Conflict)
291-
292-
@unittest.skipUnless(_HAVE_GRPC, 'No gRPC')
293-
def test_commit_failure_invalid_argument(self):
294-
from grpc import StatusCode
295-
from grpc._channel import _RPCState
296-
from google.cloud.exceptions import BadRequest
297-
from google.cloud.exceptions import GrpcRendezvous
298-
299-
details = 'Too long content.'
300-
exc_state = _RPCState((), None, None,
301-
StatusCode.INVALID_ARGUMENT, details)
302-
exc = GrpcRendezvous(exc_state, None, None, None)
303-
self._commit_failure_helper(exc, BadRequest)
304-
305-
@unittest.skipUnless(_HAVE_GRPC, 'No gRPC')
306-
def test_commit_failure_cancelled(self):
307-
from grpc import StatusCode
308-
from grpc._channel import _RPCState
309-
from google.cloud.exceptions import GrpcRendezvous
310-
311-
exc_state = _RPCState((), None, None, StatusCode.CANCELLED, None)
312-
exc = GrpcRendezvous(exc_state, None, None, None)
313-
self._commit_failure_helper(exc, GrpcRendezvous)
314-
315-
@unittest.skipUnless(_HAVE_GRPC, 'No gRPC')
316-
def test_commit_failure_non_grpc_err(self):
317-
exc = RuntimeError('Not a gRPC error')
318-
self._commit_failure_helper(exc, RuntimeError)
319-
320318
def test_rollback(self):
321319
return_val = object()
322320
stub = _GRPCStub(return_val)
@@ -1161,27 +1159,22 @@ def __init__(self, return_val=None, side_effect=Exception):
11611159

11621160
def _method(self, request_pb, name):
11631161
self.method_calls.append((request_pb, name))
1164-
return self.return_val
1162+
if self.side_effect is Exception:
1163+
return self.return_val
1164+
else:
1165+
raise self.side_effect
11651166

11661167
def Lookup(self, request_pb):
11671168
return self._method(request_pb, 'Lookup')
11681169

11691170
def RunQuery(self, request_pb):
1170-
result = self._method(request_pb, 'RunQuery')
1171-
if self.side_effect is Exception:
1172-
return result
1173-
else:
1174-
raise self.side_effect
1171+
return self._method(request_pb, 'RunQuery')
11751172

11761173
def BeginTransaction(self, request_pb):
11771174
return self._method(request_pb, 'BeginTransaction')
11781175

11791176
def Commit(self, request_pb):
1180-
result = self._method(request_pb, 'Commit')
1181-
if self.side_effect is Exception:
1182-
return result
1183-
else:
1184-
raise self.side_effect
1177+
return self._method(request_pb, 'Commit')
11851178

11861179
def Rollback(self, request_pb):
11871180
return self._method(request_pb, 'Rollback')

0 commit comments

Comments
 (0)