Skip to content
This repository was archived by the owner on May 6, 2026. It is now read-only.

Commit 042cf6c

Browse files
author
Chris Rossi
authored
feat: convert grpc errors to api core exceptions (#457)
This brings NDB into line with other API libraries by calling `google.api_core.exceptions.from_grpc_error` to convert grpc errors to distinct exceptions from `google.api_core.exceptions`. Closes #416
1 parent e329276 commit 042cf6c

5 files changed

Lines changed: 72 additions & 54 deletions

File tree

google/cloud/ndb/_datastore_api.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414

1515
"""Functions that interact with Datastore backend."""
1616

17+
import grpc
1718
import itertools
1819
import logging
1920

21+
from google.api_core import exceptions as core_exceptions
2022
from google.cloud.datastore import helpers
2123
from google.cloud.datastore_v1.proto import datastore_pb2
2224
from google.cloud.datastore_v1.proto import entity_pb2
@@ -85,7 +87,13 @@ def rpc_call():
8587
log.debug(rpc)
8688
log.debug("timeout={}".format(timeout))
8789

88-
result = yield rpc
90+
try:
91+
result = yield rpc
92+
except Exception as error:
93+
if isinstance(error, grpc.Call):
94+
error = core_exceptions.from_grpc_error(error)
95+
raise error
96+
8997
raise tasklets.Return(result)
9098

9199
if retries:

google/cloud/ndb/_retry.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"""Retry functions."""
1616

1717
import functools
18-
import grpc
1918
import itertools
2019

2120
from google.api_core import retry as core_retry
@@ -91,18 +90,18 @@ def retry_wrapper(*args, **kwargs):
9190
return retry_wrapper
9291

9392

94-
# Possibly we should include DEADLINE_EXCEEDED. The caveat is that I think the
93+
# Possibly we should include DeadlineExceeded. The caveat is that I think the
9594
# timeout is enforced on the client side, so it might be possible that a Commit
9695
# request times out on the client side, but still writes data on the server
9796
# side, in which case we don't want to retry, since we can't commit the same
9897
# transaction more than once. Some more research is needed here. If we discover
99-
# that a DEADLINE_EXCEEDED status code guarantees the operation was cancelled,
100-
# then we can add DEADLINE_EXCEEDED to our retryable status codes. Not knowing
101-
# the answer, it's best not to take that risk.
102-
TRANSIENT_CODES = (
103-
grpc.StatusCode.UNAVAILABLE,
104-
grpc.StatusCode.INTERNAL,
105-
grpc.StatusCode.ABORTED,
98+
# that a DeadlineExceeded error guarantees the operation was cancelled, then we
99+
# can add DeadlineExceeded to our retryable errors. Not knowing the answer,
100+
# it's best not to take that risk.
101+
TRANSIENT_ERRORS = (
102+
core_exceptions.ServiceUnavailable,
103+
core_exceptions.InternalServerError,
104+
core_exceptions.Aborted,
106105
)
107106

108107

@@ -115,10 +114,4 @@ def is_transient_error(error):
115114
if core_retry.if_transient_error(error):
116115
return True
117116

118-
if isinstance(error, grpc.Call):
119-
method = getattr(error, "code", None)
120-
if callable(method):
121-
code = method()
122-
return code in TRANSIENT_CODES
123-
124-
return False
117+
return isinstance(error, TRANSIENT_ERRORS)

tests/system/test_query.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
import datetime
2020
import operator
2121

22-
import grpc
2322
import pytest
2423
import pytz
2524

2625
import test_utils.system
2726

27+
from google.api_core import exceptions as core_exceptions
2828
from google.cloud import ndb
2929
from google.cloud.datastore import key as ds_key_module
3030

@@ -61,7 +61,7 @@ class SomeKind(ndb.Model):
6161
with pytest.raises(Exception) as error_context:
6262
query.fetch(timeout=timeout)
6363

64-
assert error_context.value.code() == grpc.StatusCode.DEADLINE_EXCEEDED
64+
assert isinstance(error_context.value, core_exceptions.DeadlineExceeded)
6565

6666

6767
@pytest.mark.usefixtures("client_context")

tests/unit/test__datastore_api.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
except ImportError: # pragma: NO PY3 COVER
1818
import mock
1919

20+
import grpc
2021
import pytest
2122

2223
from google.api_core import client_info
24+
from google.api_core import exceptions as core_exceptions
2325
from google.cloud.datastore import entity
2426
from google.cloud.datastore import helpers
2527
from google.cloud.datastore import key as ds_key_module
@@ -132,6 +134,50 @@ def test_explicit_timeout(stub, _retry):
132134
assert call.result() == "bar"
133135
api.foo.future.assert_called_once_with(request, timeout=20)
134136

137+
@staticmethod
138+
@pytest.mark.usefixtures("in_context")
139+
@mock.patch("google.cloud.ndb._datastore_api.stub")
140+
def test_grpc_error(stub):
141+
api = stub.return_value
142+
future = tasklets.Future()
143+
api.foo.future.return_value = future
144+
145+
class DummyError(grpc.Call, Exception):
146+
def code(self):
147+
return grpc.StatusCode.UNAVAILABLE
148+
149+
def details(self):
150+
return "Where is the devil in?"
151+
152+
try:
153+
raise DummyError("Have to raise in order to get traceback")
154+
except Exception as error:
155+
future.set_exception(error)
156+
157+
request = object()
158+
with pytest.raises(core_exceptions.ServiceUnavailable):
159+
_api.make_call("foo", request, retries=0).result()
160+
161+
@staticmethod
162+
@pytest.mark.usefixtures("in_context")
163+
@mock.patch("google.cloud.ndb._datastore_api.stub")
164+
def test_other_error(stub):
165+
api = stub.return_value
166+
future = tasklets.Future()
167+
api.foo.future.return_value = future
168+
169+
class DummyException(Exception):
170+
pass
171+
172+
try:
173+
raise DummyException("Have to raise in order to get traceback")
174+
except Exception as error:
175+
future.set_exception(error)
176+
177+
request = object()
178+
with pytest.raises(DummyException):
179+
_api.make_call("foo", request, retries=0).result()
180+
135181

136182
def _mock_key(key_str):
137183
key = mock.Mock(kind="SomeKind", spec=("to_protobuf", "kind"))

tests/unit/test__retry.py

Lines changed: 6 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
except ImportError: # pragma: NO PY3 COVER
2020
import mock
2121

22-
import grpc
2322
import pytest
2423

2524
from google.api_core import exceptions as core_exceptions
@@ -142,68 +141,40 @@ def test_core_says_yes(core_retry):
142141

143142
@staticmethod
144143
@mock.patch("google.cloud.ndb._retry.core_retry")
145-
def test_not_a_grpc_call(core_retry):
146-
error = object()
147-
core_retry.if_transient_error.return_value = False
148-
assert _retry.is_transient_error(error) is False
149-
core_retry.if_transient_error.assert_called_once_with(error)
150-
151-
@staticmethod
152-
@mock.patch("google.cloud.ndb._retry.core_retry")
153-
def test_code_is_not_callable(core_retry):
154-
error = mock.Mock(spec=grpc.Call, code=404)
155-
core_retry.if_transient_error.return_value = False
156-
assert _retry.is_transient_error(error) is False
157-
core_retry.if_transient_error.assert_called_once_with(error)
158-
159-
@staticmethod
160-
@mock.patch("google.cloud.ndb._retry.core_retry")
161-
def test_code_is_not_transient(core_retry):
162-
error = mock.Mock(spec=grpc.Call, code=mock.Mock(return_value=42))
144+
def test_error_is_not_transient(core_retry):
145+
error = Exception("whatever")
163146
core_retry.if_transient_error.return_value = False
164147
assert _retry.is_transient_error(error) is False
165148
core_retry.if_transient_error.assert_called_once_with(error)
166149

167150
@staticmethod
168151
@mock.patch("google.cloud.ndb._retry.core_retry")
169152
def test_unavailable(core_retry):
170-
error = mock.Mock(
171-
spec=grpc.Call,
172-
code=mock.Mock(return_value=grpc.StatusCode.UNAVAILABLE),
173-
)
153+
error = core_exceptions.ServiceUnavailable("testing")
174154
core_retry.if_transient_error.return_value = False
175155
assert _retry.is_transient_error(error) is True
176156
core_retry.if_transient_error.assert_called_once_with(error)
177157

178158
@staticmethod
179159
@mock.patch("google.cloud.ndb._retry.core_retry")
180160
def test_internal(core_retry):
181-
error = mock.Mock(
182-
spec=grpc.Call,
183-
code=mock.Mock(return_value=grpc.StatusCode.INTERNAL),
184-
)
161+
error = core_exceptions.InternalServerError("testing")
185162
core_retry.if_transient_error.return_value = False
186163
assert _retry.is_transient_error(error) is True
187164
core_retry.if_transient_error.assert_called_once_with(error)
188165

189166
@staticmethod
190167
@mock.patch("google.cloud.ndb._retry.core_retry")
191168
def test_unauthenticated(core_retry):
192-
error = mock.Mock(
193-
spec=grpc.Call,
194-
code=mock.Mock(return_value=grpc.StatusCode.UNAUTHENTICATED),
195-
)
169+
error = core_exceptions.Unauthenticated("testing")
196170
core_retry.if_transient_error.return_value = False
197171
assert _retry.is_transient_error(error) is False
198172
core_retry.if_transient_error.assert_called_once_with(error)
199173

200174
@staticmethod
201175
@mock.patch("google.cloud.ndb._retry.core_retry")
202176
def test_aborted(core_retry):
203-
error = mock.Mock(
204-
spec=grpc.Call,
205-
code=mock.Mock(return_value=grpc.StatusCode.ABORTED),
206-
)
177+
error = core_exceptions.Aborted("testing")
207178
core_retry.if_transient_error.return_value = False
208179
assert _retry.is_transient_error(error) is True
209180
core_retry.if_transient_error.assert_called_once_with(error)

0 commit comments

Comments
 (0)