Skip to content

Commit 67cc766

Browse files
authored
Adjust Retryable Errors (#742)
* Deprecate `is_retriable()` in favor of `is_retryable()` * Rewrite errors for backwards compatibility * Neo.TransientError.Transaction.Terminated -> Neo.ClientError.Transaction.Terminated * Neo.TransientError.Transaction.LockClientStopped -> Neo.ClientError.Transaction.LockClientStopped
1 parent a959c96 commit 67cc766

File tree

8 files changed

+116
-44
lines changed

8 files changed

+116
-44
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@
9696
- ANSI colour codes for log output are now opt-in
9797
- Prepend log format with log-level (if colours are disabled)
9898
- Prepend log format with thread name and id
99+
- Deprecated `neo4j.exceptions.Neo4jError.is_retriable()`.
100+
Use `neo4j.exceptions.Neo4jError.is_retryable()` instead.
99101
- Importing submodules from `neo4j.time` (`neo4j.time.xyz`) has been deprecated.
100102
Everything needed should be imported from `neo4j.time` directly.
101103
- `neo4j.spatial.hydrate_point` and `neo4j.spatial.dehydrate_point` have been

docs/source/api.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,7 +1246,7 @@ Neo4j Execution Errors
12461246

12471247

12481248
.. autoclass:: neo4j.exceptions.Neo4jError
1249-
:members: message, code, is_retriable
1249+
:members: message, code, is_retriable, is_retryable
12501250

12511251

12521252
.. autoclass:: neo4j.exceptions.ClientError
@@ -1304,7 +1304,7 @@ Connectivity Errors
13041304

13051305

13061306
.. autoclass:: neo4j.exceptions.DriverError
1307-
:members: is_retriable
1307+
:members: is_retryable
13081308

13091309
.. autoclass:: neo4j.exceptions.TransactionError
13101310
:show-inheritance:

neo4j/_async/work/session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ async def _run_transaction(
417417
await tx._commit()
418418
except (DriverError, Neo4jError) as error:
419419
await self._disconnect()
420-
if not error.is_retriable():
420+
if not error.is_retryable():
421421
raise
422422
errors.append(error)
423423
else:

neo4j/_async/work/transaction.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ class AsyncManagedTransaction(_AsyncTransactionBase):
245245
Note that transaction functions have to be idempotent (i.e., the result
246246
of running the function once has to be the same as running it any number
247247
of times). This is, because the driver will retry the transaction function
248-
if the error is classified as retriable.
248+
if the error is classified as retryable.
249249
250250
.. versionadded:: 5.0
251251

neo4j/_sync/work/session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ def _run_transaction(
417417
tx._commit()
418418
except (DriverError, Neo4jError) as error:
419419
self._disconnect()
420-
if not error.is_retriable():
420+
if not error.is_retryable():
421421
raise
422422
errors.append(error)
423423
else:

neo4j/_sync/work/transaction.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ class ManagedTransaction(_TransactionBase):
245245
Note that transaction functions have to be idempotent (i.e., the result
246246
of running the function once has to be the same as running it any number
247247
of times). This is, because the driver will retry the transaction function
248-
if the error is classified as retriable.
248+
if the error is classified as retryable.
249249
250250
.. versionadded:: 5.0
251251

neo4j/exceptions.py

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,38 @@
6363
+ BoltFailure
6464
+ BoltProtocolError
6565
+ Bolt*
66-
6766
"""
6867

68+
69+
from .meta import deprecated
70+
71+
6972
CLASSIFICATION_CLIENT = "ClientError"
7073
CLASSIFICATION_TRANSIENT = "TransientError"
7174
CLASSIFICATION_DATABASE = "DatabaseError"
7275

7376

77+
ERROR_REWRITE_MAP = {
78+
# This error can be retried ed. The driver just needs to re-authenticate
79+
# with the same credentials.
80+
"Neo.ClientError.Security.AuthorizationExpired": (
81+
CLASSIFICATION_TRANSIENT, None
82+
),
83+
# In 5.0, this error has been re-classified as ClientError.
84+
# For backwards compatibility with Neo4j 4.4 and earlier, we re-map it in
85+
# the driver, too.
86+
"Neo.TransientError.Transaction.Terminated": (
87+
CLASSIFICATION_CLIENT, "Neo.ClientError.Transaction.Terminated"
88+
),
89+
# In 5.0, this error has been re-classified as ClientError.
90+
# For backwards compatibility with Neo4j 4.4 and earlier, we re-map it in
91+
# the driver, too.
92+
"Neo.TransientError.Transaction.LockClientStopped": (
93+
CLASSIFICATION_CLIENT, "Neo.ClientError.Transaction.LockClientStopped"
94+
),
95+
}
96+
97+
7498
class Neo4jError(Exception):
7599
""" Raised when the Cypher engine returns an error to the client.
76100
"""
@@ -93,12 +117,17 @@ def hydrate(cls, message=None, code=None, **metadata):
93117
code = code or "Neo.DatabaseError.General.UnknownError"
94118
try:
95119
_, classification, category, title = code.split(".")
96-
if code == "Neo.ClientError.Security.AuthorizationExpired":
97-
classification = CLASSIFICATION_TRANSIENT
98120
except ValueError:
99121
classification = CLASSIFICATION_DATABASE
100122
category = "General"
101123
title = "UnknownError"
124+
else:
125+
classification_override, code_override = \
126+
ERROR_REWRITE_MAP.get(code, (None, None))
127+
if classification_override is not None:
128+
classification = classification_override
129+
if code_override is not None:
130+
code = code_override
102131

103132
error_class = cls._extract_error_class(classification, code)
104133

@@ -131,11 +160,31 @@ def _extract_error_class(cls, classification, code):
131160
else:
132161
return cls
133162

163+
# TODO 6.0: Remove this alias
164+
@deprecated(
165+
"Neo4jError.is_retriable is deprecated and will be removed in a "
166+
"future version. Please use Neo4jError.is_retryable instead."
167+
)
134168
def is_retriable(self):
135169
"""Whether the error is retryable.
136170
137-
Indicated whether a transaction that yielded this error makes sense to
138-
retry. This methods makes mostly sense when implementing a custom
171+
See :meth:`.is_retryable`.
172+
173+
:return: :const:`True` if the error is retryable,
174+
:const:`False` otherwise.
175+
:rtype: bool
176+
177+
.. deprecated:: 5.0
178+
This method will be removed in a future version.
179+
Please use :meth:`.is_retryable` instead.
180+
"""
181+
return self.is_retryable()
182+
183+
def is_retryable(self):
184+
"""Whether the error is retryable.
185+
186+
Indicates whether a transaction that yielded this error makes sense to
187+
retry. This method makes mostly sense when implementing a custom
139188
retry policy in conjunction with :ref:`explicit-transactions-ref`.
140189
141190
:return: :const:`True` if the error is retryable,
@@ -182,14 +231,8 @@ class TransientError(Neo4jError):
182231
""" The database cannot service the request right now, retrying later might yield a successful outcome.
183232
"""
184233

185-
def is_retriable(self):
186-
# Transient errors are always retriable.
187-
# However, there are some errors that are misclassified by the server.
188-
# They should really be ClientErrors.
189-
return self.code not in (
190-
"Neo.TransientError.Transaction.Terminated",
191-
"Neo.TransientError.Transaction.LockClientStopped",
192-
)
234+
def is_retryable(self):
235+
return True
193236

194237

195238
class DatabaseUnavailable(TransientError):
@@ -285,11 +328,11 @@ class TokenExpired(AuthError):
285328
class DriverError(Exception):
286329
""" Raised when the Driver raises an error.
287330
"""
288-
def is_retriable(self):
331+
def is_retryable(self):
289332
"""Whether the error is retryable.
290333
291-
Indicated whether a transaction that yielded this error makes sense to
292-
retry. This methods makes mostly sense when implementing a custom
334+
Indicates whether a transaction that yielded this error makes sense to
335+
retry. This method makes mostly sense when implementing a custom
293336
retry policy in conjunction with :ref:`explicit-transactions-ref`.
294337
295338
:return: :const:`True` if the error is retryable,
@@ -307,7 +350,7 @@ class SessionExpired(DriverError):
307350
def __init__(self, session, *args, **kwargs):
308351
super(SessionExpired, self).__init__(session, *args, **kwargs)
309352

310-
def is_retriable(self):
353+
def is_retryable(self):
311354
return True
312355

313356

@@ -349,7 +392,7 @@ class ServiceUnavailable(DriverError):
349392
""" Raised when no database service is available.
350393
"""
351394

352-
def is_retriable(self):
395+
def is_retryable(self):
353396
return True
354397

355398

@@ -377,7 +420,7 @@ class IncompleteCommit(ServiceUnavailable):
377420
successfully or not.
378421
"""
379422

380-
def is_retriable(self):
423+
def is_retryable(self):
381424
return False
382425

383426

tests/unit/common/test_exceptions.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -211,22 +211,49 @@ def test_neo4jerror_hydrate_with_message_and_code_client():
211211
assert error.code == "Neo.{}.General.TestError".format(CLASSIFICATION_CLIENT)
212212

213213

214-
def test_transient_error_is_retriable_case_1():
215-
error = Neo4jError.hydrate(message="Test error message", code="Neo.TransientError.Transaction.Terminated")
216-
217-
assert isinstance(error, TransientError)
218-
assert error.is_retriable() is False
219-
220-
221-
def test_transient_error_is_retriable_case_2():
222-
error = Neo4jError.hydrate(message="Test error message", code="Neo.TransientError.Transaction.LockClientStopped")
223-
224-
assert isinstance(error, TransientError)
225-
assert error.is_retriable() is False
226-
227-
228-
def test_transient_error_is_retriable_case_3():
229-
error = Neo4jError.hydrate(message="Test error message", code="Neo.TransientError.General.TestError")
230-
231-
assert isinstance(error, TransientError)
232-
assert error.is_retriable() is True
214+
@pytest.mark.parametrize(
215+
("code", "expected_cls", "expected_code"),
216+
(
217+
(
218+
"Neo.TransientError.Transaction.Terminated",
219+
ClientError,
220+
"Neo.ClientError.Transaction.Terminated"
221+
),
222+
(
223+
"Neo.ClientError.Transaction.Terminated",
224+
ClientError,
225+
"Neo.ClientError.Transaction.Terminated"
226+
),
227+
(
228+
"Neo.TransientError.Transaction.LockClientStopped",
229+
ClientError,
230+
"Neo.ClientError.Transaction.LockClientStopped"
231+
),
232+
(
233+
"Neo.ClientError.Transaction.LockClientStopped",
234+
ClientError,
235+
"Neo.ClientError.Transaction.LockClientStopped"
236+
),
237+
(
238+
"Neo.ClientError.Security.AuthorizationExpired",
239+
TransientError,
240+
"Neo.ClientError.Security.AuthorizationExpired"
241+
),
242+
(
243+
"Neo.TransientError.General.TestError",
244+
TransientError,
245+
"Neo.TransientError.General.TestError"
246+
)
247+
)
248+
)
249+
def test_error_rewrite(code, expected_cls, expected_code):
250+
message = "Test error message"
251+
error = Neo4jError.hydrate(message=message, code=code)
252+
253+
expected_retryable = expected_cls is TransientError
254+
assert error.__class__ is expected_cls
255+
assert error.code == expected_code
256+
assert error.message == message
257+
assert error.is_retryable() is expected_retryable
258+
with pytest.warns(DeprecationWarning, match=".*is_retryable.*"):
259+
assert error.is_retriable() is expected_retryable

0 commit comments

Comments
 (0)