Skip to content

Commit 3389c4a

Browse files
authored
Add .is_retriable() to Neo4jError and DriverError (#682)
This should help users to implement custom retry policies together with explicit transactions. This PR also improves documentation around errors and changes the driver's session code to also use the new flag for determining when to retry a tx. This will make sure that it's a reliable feature + simplify the driver's code.
1 parent 2e139d9 commit 3389c4a

File tree

5 files changed

+81
-49
lines changed

5 files changed

+81
-49
lines changed

docs/source/api.rst

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ Will result in:
417417
Sessions & Transactions
418418
***********************
419419
All database activity is co-ordinated through two mechanisms:
420-
**sessions** (:class:`neo4j.AsyncSession`) and **transactions**
420+
**sessions** (:class:`neo4j.Session`) and **transactions**
421421
(:class:`neo4j.Transaction`, :class:`neo4j.ManagedTransaction`).
422422

423423
A **session** is a logical container for any number of causally-related transactional units of work.
@@ -1263,18 +1263,7 @@ Neo4j Execution Errors
12631263
12641264
12651265
.. autoclass:: neo4j.exceptions.Neo4jError
1266-
1267-
.. autoproperty:: message
1268-
1269-
.. autoproperty:: code
1270-
1271-
There are many Neo4j status codes, see `status code <https://neo4j.com/docs/status-codes/current/>`_.
1272-
1273-
.. autoproperty:: classification
1274-
1275-
.. autoproperty:: category
1276-
1277-
.. autoproperty:: title
1266+
:members: message, code, is_retriable
12781267
12791268
12801269
.. autoclass:: neo4j.exceptions.ClientError
@@ -1332,7 +1321,7 @@ Connectivity Errors
13321321
13331322
13341323
.. autoclass:: neo4j.exceptions.DriverError
1335-
1324+
:members: is_retriable
13361325
13371326
.. autoclass:: neo4j.exceptions.TransactionError
13381327
:show-inheritance:

neo4j/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"DEFAULT_DATABASE",
4040
"Driver",
4141
"ExperimentalWarning",
42+
"get_user_agent",
4243
"GraphDatabase",
4344
"IPv4Address",
4445
"IPv6Address",

neo4j/_async/work/session.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,11 @@
3030
from ...data import DataHydrator
3131
from ...exceptions import (
3232
ClientError,
33-
IncompleteCommit,
33+
DriverError,
3434
Neo4jError,
3535
ServiceUnavailable,
3636
SessionExpired,
3737
TransactionError,
38-
TransientError,
3938
)
4039
from ...meta import (
4140
deprecated,
@@ -328,8 +327,9 @@ async def _transaction_error_handler(self, _):
328327
self._transaction = None
329328
await self._disconnect()
330329

331-
async def _open_transaction(self, *, tx_cls, access_mode, metadata=None,
332-
timeout=None):
330+
async def _open_transaction(
331+
self, *, tx_cls, access_mode, metadata=None, timeout=None
332+
):
333333
await self._connect(access_mode=access_mode)
334334
self._transaction = tx_cls(
335335
self._connection, self._config.fetch_size,
@@ -393,7 +393,11 @@ async def _run_transaction(
393393
metadata = getattr(transaction_function, "metadata", None)
394394
timeout = getattr(transaction_function, "timeout", None)
395395

396-
retry_delay = retry_delay_generator(self._config.initial_retry_delay, self._config.retry_delay_multiplier, self._config.retry_delay_jitter_factor)
396+
retry_delay = retry_delay_generator(
397+
self._config.initial_retry_delay,
398+
self._config.retry_delay_multiplier,
399+
self._config.retry_delay_jitter_factor
400+
)
397401

398402
errors = []
399403

@@ -414,24 +418,22 @@ async def _run_transaction(
414418
raise
415419
else:
416420
await tx._commit()
417-
except IncompleteCommit:
418-
raise
419-
except (ServiceUnavailable, SessionExpired) as error:
420-
errors.append(error)
421+
except (DriverError, Neo4jError) as error:
421422
await self._disconnect()
422-
except TransientError as transient_error:
423-
if not transient_error.is_retriable():
423+
if not error.is_retriable():
424424
raise
425-
errors.append(transient_error)
425+
errors.append(error)
426426
else:
427427
return result
428428
if t0 == -1:
429-
t0 = perf_counter() # The timer should be started after the first attempt
429+
# The timer should be started after the first attempt
430+
t0 = perf_counter()
430431
t1 = perf_counter()
431432
if t1 - t0 > self._config.max_transaction_retry_time:
432433
break
433434
delay = next(retry_delay)
434-
log.warning("Transaction failed and will be retried in {}s ({})".format(delay, "; ".join(errors[-1].args)))
435+
log.warning("Transaction failed and will be retried in {}s ({})"
436+
"".format(delay, "; ".join(errors[-1].args)))
435437
await async_sleep(delay)
436438

437439
if errors:

neo4j/_sync/work/session.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,11 @@
3030
from ...data import DataHydrator
3131
from ...exceptions import (
3232
ClientError,
33-
IncompleteCommit,
33+
DriverError,
3434
Neo4jError,
3535
ServiceUnavailable,
3636
SessionExpired,
3737
TransactionError,
38-
TransientError,
3938
)
4039
from ...meta import (
4140
deprecated,
@@ -328,8 +327,9 @@ def _transaction_error_handler(self, _):
328327
self._transaction = None
329328
self._disconnect()
330329

331-
def _open_transaction(self, *, tx_cls, access_mode, metadata=None,
332-
timeout=None):
330+
def _open_transaction(
331+
self, *, tx_cls, access_mode, metadata=None, timeout=None
332+
):
333333
self._connect(access_mode=access_mode)
334334
self._transaction = tx_cls(
335335
self._connection, self._config.fetch_size,
@@ -393,7 +393,11 @@ def _run_transaction(
393393
metadata = getattr(transaction_function, "metadata", None)
394394
timeout = getattr(transaction_function, "timeout", None)
395395

396-
retry_delay = retry_delay_generator(self._config.initial_retry_delay, self._config.retry_delay_multiplier, self._config.retry_delay_jitter_factor)
396+
retry_delay = retry_delay_generator(
397+
self._config.initial_retry_delay,
398+
self._config.retry_delay_multiplier,
399+
self._config.retry_delay_jitter_factor
400+
)
397401

398402
errors = []
399403

@@ -414,24 +418,22 @@ def _run_transaction(
414418
raise
415419
else:
416420
tx._commit()
417-
except IncompleteCommit:
418-
raise
419-
except (ServiceUnavailable, SessionExpired) as error:
420-
errors.append(error)
421+
except (DriverError, Neo4jError) as error:
421422
self._disconnect()
422-
except TransientError as transient_error:
423-
if not transient_error.is_retriable():
423+
if not error.is_retriable():
424424
raise
425-
errors.append(transient_error)
425+
errors.append(error)
426426
else:
427427
return result
428428
if t0 == -1:
429-
t0 = perf_counter() # The timer should be started after the first attempt
429+
# The timer should be started after the first attempt
430+
t0 = perf_counter()
430431
t1 = perf_counter()
431432
if t1 - t0 > self._config.max_transaction_retry_time:
432433
break
433434
delay = next(retry_delay)
434-
log.warning("Transaction failed and will be retried in {}s ({})".format(delay, "; ".join(errors[-1].args)))
435+
log.warning("Transaction failed and will be retried in {}s ({})"
436+
"".format(delay, "; ".join(errors[-1].args)))
435437
sleep(delay)
436438

437439
if errors:

neo4j/exceptions.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,16 @@ class Neo4jError(Exception):
7575
""" Raised when the Cypher engine returns an error to the client.
7676
"""
7777

78+
#: (str or None) The error message returned by the server.
7879
message = None
80+
#: (str or None) The error code returned by the server.
81+
#: There are many Neo4j status codes, see
82+
#: `status codes <https://neo4j.com/docs/status-codes/current/>`_.
7983
code = None
8084
classification = None
8185
category = None
8286
title = None
87+
#: (dict) Any additional information returned by the server.
8388
metadata = None
8489

8590
@classmethod
@@ -126,6 +131,19 @@ def _extract_error_class(cls, classification, code):
126131
else:
127132
return cls
128133

134+
def is_retriable(self):
135+
"""Whether the error is retryable.
136+
137+
Indicated whether a transaction that yielded this error makes sense to
138+
retry. This methods makes mostly sense when implementing a custom
139+
retry policy in conjunction with :ref:`explicit-transactions-ref`.
140+
141+
:return: :const:`True` if the error is retryable,
142+
:const:`False` otherwise.
143+
:rtype: bool
144+
"""
145+
return False
146+
129147
def invalidates_all_connections(self):
130148
return self.code == "Neo.ClientError.Security.AuthorizationExpired"
131149

@@ -163,15 +181,13 @@ class TransientError(Neo4jError):
163181
"""
164182

165183
def is_retriable(self):
166-
"""These are really client errors but classification on the server is not entirely correct and they are classified as transient.
167-
168-
:return: True if it is a retriable TransientError, otherwise False.
169-
:rtype: bool
170-
"""
171-
return not (self.code in (
184+
# Transient errors are always retriable.
185+
# However, there are some errors that are misclassified by the server.
186+
# They should really be ClientErrors.
187+
return self.code not in (
172188
"Neo.TransientError.Transaction.Terminated",
173189
"Neo.TransientError.Transaction.LockClientStopped",
174-
))
190+
)
175191

176192

177193
class DatabaseUnavailable(TransientError):
@@ -220,6 +236,7 @@ class TokenExpired(AuthError):
220236
A new driver instance with a fresh authentication token needs to be created.
221237
"""
222238

239+
223240
client_errors = {
224241

225242
# ConstraintError
@@ -266,6 +283,18 @@ class TokenExpired(AuthError):
266283
class DriverError(Exception):
267284
""" Raised when the Driver raises an error.
268285
"""
286+
def is_retriable(self):
287+
"""Whether the error is retryable.
288+
289+
Indicated whether a transaction that yielded this error makes sense to
290+
retry. This methods makes mostly sense when implementing a custom
291+
retry policy in conjunction with :ref:`explicit-transactions-ref`.
292+
293+
:return: :const:`True` if the error is retryable,
294+
:const:`False` otherwise.
295+
:rtype: bool
296+
"""
297+
return False
269298

270299

271300
class SessionExpired(DriverError):
@@ -276,6 +305,9 @@ class SessionExpired(DriverError):
276305
def __init__(self, session, *args, **kwargs):
277306
super(SessionExpired, self).__init__(session, *args, **kwargs)
278307

308+
def is_retriable(self):
309+
return True
310+
279311

280312
class TransactionError(DriverError):
281313
""" Raised when an error occurs while using a transaction.
@@ -315,6 +347,9 @@ class ServiceUnavailable(DriverError):
315347
""" Raised when no database service is available.
316348
"""
317349

350+
def is_retriable(self):
351+
return True
352+
318353

319354
class RoutingServiceUnavailable(ServiceUnavailable):
320355
""" Raised when no routing service is available.
@@ -340,6 +375,9 @@ class IncompleteCommit(ServiceUnavailable):
340375
successfully or not.
341376
"""
342377

378+
def is_retriable(self):
379+
return False
380+
343381

344382
class ConfigurationError(DriverError):
345383
""" Raised when there is an error concerning a configuration.

0 commit comments

Comments
 (0)