diff --git a/docs/source/api.rst b/docs/source/api.rst
index 018c76d3b..020a7e887 100644
--- a/docs/source/api.rst
+++ b/docs/source/api.rst
@@ -417,7 +417,7 @@ Will result in:
Sessions & Transactions
***********************
All database activity is co-ordinated through two mechanisms:
-**sessions** (:class:`neo4j.AsyncSession`) and **transactions**
+**sessions** (:class:`neo4j.Session`) and **transactions**
(:class:`neo4j.Transaction`, :class:`neo4j.ManagedTransaction`).
A **session** is a logical container for any number of causally-related transactional units of work.
@@ -1263,18 +1263,7 @@ Neo4j Execution Errors
.. autoclass:: neo4j.exceptions.Neo4jError
-
- .. autoproperty:: message
-
- .. autoproperty:: code
-
- There are many Neo4j status codes, see `status code `_.
-
- .. autoproperty:: classification
-
- .. autoproperty:: category
-
- .. autoproperty:: title
+ :members: message, code, is_retriable
.. autoclass:: neo4j.exceptions.ClientError
@@ -1332,7 +1321,7 @@ Connectivity Errors
.. autoclass:: neo4j.exceptions.DriverError
-
+ :members: is_retriable
.. autoclass:: neo4j.exceptions.TransactionError
:show-inheritance:
diff --git a/neo4j/__init__.py b/neo4j/__init__.py
index f683775fc..6d5171a91 100644
--- a/neo4j/__init__.py
+++ b/neo4j/__init__.py
@@ -39,6 +39,7 @@
"DEFAULT_DATABASE",
"Driver",
"ExperimentalWarning",
+ "get_user_agent",
"GraphDatabase",
"IPv4Address",
"IPv6Address",
diff --git a/neo4j/_async/work/session.py b/neo4j/_async/work/session.py
index 9cb54fed6..b77fea09a 100644
--- a/neo4j/_async/work/session.py
+++ b/neo4j/_async/work/session.py
@@ -30,12 +30,11 @@
from ...data import DataHydrator
from ...exceptions import (
ClientError,
- IncompleteCommit,
+ DriverError,
Neo4jError,
ServiceUnavailable,
SessionExpired,
TransactionError,
- TransientError,
)
from ...meta import (
deprecated,
@@ -328,8 +327,9 @@ async def _transaction_error_handler(self, _):
self._transaction = None
await self._disconnect()
- async def _open_transaction(self, *, tx_cls, access_mode, metadata=None,
- timeout=None):
+ async def _open_transaction(
+ self, *, tx_cls, access_mode, metadata=None, timeout=None
+ ):
await self._connect(access_mode=access_mode)
self._transaction = tx_cls(
self._connection, self._config.fetch_size,
@@ -393,7 +393,11 @@ async def _run_transaction(
metadata = getattr(transaction_function, "metadata", None)
timeout = getattr(transaction_function, "timeout", None)
- retry_delay = retry_delay_generator(self._config.initial_retry_delay, self._config.retry_delay_multiplier, self._config.retry_delay_jitter_factor)
+ retry_delay = retry_delay_generator(
+ self._config.initial_retry_delay,
+ self._config.retry_delay_multiplier,
+ self._config.retry_delay_jitter_factor
+ )
errors = []
@@ -414,24 +418,22 @@ async def _run_transaction(
raise
else:
await tx._commit()
- except IncompleteCommit:
- raise
- except (ServiceUnavailable, SessionExpired) as error:
- errors.append(error)
+ except (DriverError, Neo4jError) as error:
await self._disconnect()
- except TransientError as transient_error:
- if not transient_error.is_retriable():
+ if not error.is_retriable():
raise
- errors.append(transient_error)
+ errors.append(error)
else:
return result
if t0 == -1:
- t0 = perf_counter() # The timer should be started after the first attempt
+ # The timer should be started after the first attempt
+ t0 = perf_counter()
t1 = perf_counter()
if t1 - t0 > self._config.max_transaction_retry_time:
break
delay = next(retry_delay)
- log.warning("Transaction failed and will be retried in {}s ({})".format(delay, "; ".join(errors[-1].args)))
+ log.warning("Transaction failed and will be retried in {}s ({})"
+ "".format(delay, "; ".join(errors[-1].args)))
await async_sleep(delay)
if errors:
diff --git a/neo4j/_sync/work/session.py b/neo4j/_sync/work/session.py
index fd2f99cc4..9ae88ca36 100644
--- a/neo4j/_sync/work/session.py
+++ b/neo4j/_sync/work/session.py
@@ -30,12 +30,11 @@
from ...data import DataHydrator
from ...exceptions import (
ClientError,
- IncompleteCommit,
+ DriverError,
Neo4jError,
ServiceUnavailable,
SessionExpired,
TransactionError,
- TransientError,
)
from ...meta import (
deprecated,
@@ -328,8 +327,9 @@ def _transaction_error_handler(self, _):
self._transaction = None
self._disconnect()
- def _open_transaction(self, *, tx_cls, access_mode, metadata=None,
- timeout=None):
+ def _open_transaction(
+ self, *, tx_cls, access_mode, metadata=None, timeout=None
+ ):
self._connect(access_mode=access_mode)
self._transaction = tx_cls(
self._connection, self._config.fetch_size,
@@ -393,7 +393,11 @@ def _run_transaction(
metadata = getattr(transaction_function, "metadata", None)
timeout = getattr(transaction_function, "timeout", None)
- retry_delay = retry_delay_generator(self._config.initial_retry_delay, self._config.retry_delay_multiplier, self._config.retry_delay_jitter_factor)
+ retry_delay = retry_delay_generator(
+ self._config.initial_retry_delay,
+ self._config.retry_delay_multiplier,
+ self._config.retry_delay_jitter_factor
+ )
errors = []
@@ -414,24 +418,22 @@ def _run_transaction(
raise
else:
tx._commit()
- except IncompleteCommit:
- raise
- except (ServiceUnavailable, SessionExpired) as error:
- errors.append(error)
+ except (DriverError, Neo4jError) as error:
self._disconnect()
- except TransientError as transient_error:
- if not transient_error.is_retriable():
+ if not error.is_retriable():
raise
- errors.append(transient_error)
+ errors.append(error)
else:
return result
if t0 == -1:
- t0 = perf_counter() # The timer should be started after the first attempt
+ # The timer should be started after the first attempt
+ t0 = perf_counter()
t1 = perf_counter()
if t1 - t0 > self._config.max_transaction_retry_time:
break
delay = next(retry_delay)
- log.warning("Transaction failed and will be retried in {}s ({})".format(delay, "; ".join(errors[-1].args)))
+ log.warning("Transaction failed and will be retried in {}s ({})"
+ "".format(delay, "; ".join(errors[-1].args)))
sleep(delay)
if errors:
diff --git a/neo4j/exceptions.py b/neo4j/exceptions.py
index bafe97371..81e64d2ee 100644
--- a/neo4j/exceptions.py
+++ b/neo4j/exceptions.py
@@ -75,11 +75,16 @@ class Neo4jError(Exception):
""" Raised when the Cypher engine returns an error to the client.
"""
+ #: (str or None) The error message returned by the server.
message = None
+ #: (str or None) The error code returned by the server.
+ #: There are many Neo4j status codes, see
+ #: `status codes `_.
code = None
classification = None
category = None
title = None
+ #: (dict) Any additional information returned by the server.
metadata = None
@classmethod
@@ -126,6 +131,19 @@ def _extract_error_class(cls, classification, code):
else:
return cls
+ def is_retriable(self):
+ """Whether the error is retryable.
+
+ Indicated whether a transaction that yielded this error makes sense to
+ retry. This methods makes mostly sense when implementing a custom
+ retry policy in conjunction with :ref:`explicit-transactions-ref`.
+
+ :return: :const:`True` if the error is retryable,
+ :const:`False` otherwise.
+ :rtype: bool
+ """
+ return False
+
def invalidates_all_connections(self):
return self.code == "Neo.ClientError.Security.AuthorizationExpired"
@@ -163,15 +181,13 @@ class TransientError(Neo4jError):
"""
def is_retriable(self):
- """These are really client errors but classification on the server is not entirely correct and they are classified as transient.
-
- :return: True if it is a retriable TransientError, otherwise False.
- :rtype: bool
- """
- return not (self.code in (
+ # Transient errors are always retriable.
+ # However, there are some errors that are misclassified by the server.
+ # They should really be ClientErrors.
+ return self.code not in (
"Neo.TransientError.Transaction.Terminated",
"Neo.TransientError.Transaction.LockClientStopped",
- ))
+ )
class DatabaseUnavailable(TransientError):
@@ -220,6 +236,7 @@ class TokenExpired(AuthError):
A new driver instance with a fresh authentication token needs to be created.
"""
+
client_errors = {
# ConstraintError
@@ -266,6 +283,18 @@ class TokenExpired(AuthError):
class DriverError(Exception):
""" Raised when the Driver raises an error.
"""
+ def is_retriable(self):
+ """Whether the error is retryable.
+
+ Indicated whether a transaction that yielded this error makes sense to
+ retry. This methods makes mostly sense when implementing a custom
+ retry policy in conjunction with :ref:`explicit-transactions-ref`.
+
+ :return: :const:`True` if the error is retryable,
+ :const:`False` otherwise.
+ :rtype: bool
+ """
+ return False
class SessionExpired(DriverError):
@@ -276,6 +305,9 @@ class SessionExpired(DriverError):
def __init__(self, session, *args, **kwargs):
super(SessionExpired, self).__init__(session, *args, **kwargs)
+ def is_retriable(self):
+ return True
+
class TransactionError(DriverError):
""" Raised when an error occurs while using a transaction.
@@ -315,6 +347,9 @@ class ServiceUnavailable(DriverError):
""" Raised when no database service is available.
"""
+ def is_retriable(self):
+ return True
+
class RoutingServiceUnavailable(ServiceUnavailable):
""" Raised when no routing service is available.
@@ -340,6 +375,9 @@ class IncompleteCommit(ServiceUnavailable):
successfully or not.
"""
+ def is_retriable(self):
+ return False
+
class ConfigurationError(DriverError):
""" Raised when there is an error concerning a configuration.