From 76c6c4cdccc854d497c7d2e6c5fc2dd0d4a6b245 Mon Sep 17 00:00:00 2001 From: Rouven Bauer Date: Mon, 22 Mar 2021 12:02:38 +0100 Subject: [PATCH 1/3] Don't retry failed commit --- neo4j/work/simple.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/neo4j/work/simple.py b/neo4j/work/simple.py index 0ee46e7b8..1015a587d 100644 --- a/neo4j/work/simple.py +++ b/neo4j/work/simple.py @@ -302,6 +302,7 @@ def _run_transaction(self, access_mode, transaction_function, *args, **kwargs): t0 = -1 # Timer while True: + committing = False try: self._open_transaction(access_mode=access_mode, database=self._config.database, metadata=metadata, timeout=timeout) tx = self._transaction @@ -311,8 +312,13 @@ def _run_transaction(self, access_mode, transaction_function, *args, **kwargs): tx.rollback() raise else: + committing = True tx.commit() except (ServiceUnavailable, SessionExpired) as error: + if committing: + # Failed commit might have been executed on the server + # => no retry + raise errors.append(error) self._disconnect() except TransientError as transient_error: From 48a005cc7122722d3ac5ba6fe5f39af6fe0bcbe2 Mon Sep 17 00:00:00 2001 From: Rouven Bauer Date: Thu, 1 Apr 2021 17:32:43 +0200 Subject: [PATCH 2/3] Introduce new public error: IncompleteCommit --- neo4j/_exceptions.py | 10 +--------- neo4j/exceptions.py | 14 ++++++++++++-- neo4j/io/_bolt3.py | 17 +++++++---------- neo4j/io/_bolt4.py | 14 ++++++-------- neo4j/work/simple.py | 32 +++++++++++++++----------------- neo4j/work/transaction.py | 10 +++------- tests/unit/test_exceptions.py | 2 +- 7 files changed, 45 insertions(+), 54 deletions(-) diff --git a/neo4j/_exceptions.py b/neo4j/_exceptions.py index 1bfbd420e..d50978547 100644 --- a/neo4j/_exceptions.py +++ b/neo4j/_exceptions.py @@ -174,14 +174,6 @@ def transaction(self): return None -class BoltIncompleteCommitError(BoltError): - """ Raised when a disconnection occurs while still waiting for a commit - response. For non-idempotent write transactions, this leaves the data - in an unknown state with regard to whether the transaction completed - successfully or not. - """ - - class BoltProtocolError(BoltError): """ Raised when an unexpected or unsupported protocol event occurs. - """ \ No newline at end of file + """ diff --git a/neo4j/exceptions.py b/neo4j/exceptions.py index 5771b5b8d..6a98bcb35 100644 --- a/neo4j/exceptions.py +++ b/neo4j/exceptions.py @@ -45,6 +45,7 @@ + RoutingServiceUnavailable + WriteServiceUnavailable + ReadServiceUnavailable + + IncompleteCommit + ConfigurationError + AuthConfigurationError + CertificateConfigurationError @@ -60,7 +61,6 @@ + BoltConnectionBroken + BoltConnectionClosed + BoltFailure - + BoltIncompleteCommitError + BoltProtocolError + Bolt* @@ -286,6 +286,16 @@ class ReadServiceUnavailable(ServiceUnavailable): """ +class IncompleteCommit(ServiceUnavailable): + """ Raised when the client looses connection while committing a transaction + + Raised when a disconnection occurs while still waiting for a commit + response. For non-idempotent write transactions, this leaves the data + in an unknown state with regard to whether the transaction completed + successfully or not. + """ + + class ResultConsumedError(DriverError): """ Raised when trying to access records after the records have been consumed. """ @@ -308,4 +318,4 @@ class CertificateConfigurationError(ConfigurationError): class UnsupportedServerProduct(Exception): """ Raised when an unsupported server product is detected. - """ \ No newline at end of file + """ diff --git a/neo4j/io/_bolt3.py b/neo4j/io/_bolt3.py index e81c6aea9..815e7bba1 100644 --- a/neo4j/io/_bolt3.py +++ b/neo4j/io/_bolt3.py @@ -35,18 +35,15 @@ from neo4j.meta import get_user_agent from neo4j.exceptions import ( AuthError, - ServiceUnavailable, DatabaseUnavailable, - NotALeader, + ConfigurationError, ForbiddenOnReadOnlyDatabase, + IncompleteCommit, + NotALeader, + ServiceUnavailable, SessionExpired, - ConfigurationError, - UnsupportedServerProduct, -) -from neo4j._exceptions import ( - BoltIncompleteCommitError, - BoltProtocolError, ) +from neo4j._exceptions import BoltProtocolError from neo4j.packstream import ( Unpacker, Packer, @@ -403,9 +400,9 @@ def _set_defunct(self, error=None): for response in self.responses: if isinstance(response, CommitResponse): if error: - raise BoltIncompleteCommitError(message, address=None) from error + raise IncompleteCommit(message) from error else: - raise BoltIncompleteCommitError(message, address=None) + raise IncompleteCommit(message) if direct_driver: if error: diff --git a/neo4j/io/_bolt4.py b/neo4j/io/_bolt4.py index 4968332c3..65603df2e 100644 --- a/neo4j/io/_bolt4.py +++ b/neo4j/io/_bolt4.py @@ -36,16 +36,14 @@ from neo4j.meta import get_user_agent from neo4j.exceptions import ( AuthError, - ServiceUnavailable, DatabaseUnavailable, - NotALeader, ForbiddenOnReadOnlyDatabase, + IncompleteCommit, + NotALeader, + ServiceUnavailable, SessionExpired, ) -from neo4j._exceptions import ( - BoltIncompleteCommitError, - BoltProtocolError, -) +from neo4j._exceptions import BoltProtocolError from neo4j.packstream import ( Unpacker, Packer, @@ -412,9 +410,9 @@ def _set_defunct(self, error=None): for response in self.responses: if isinstance(response, CommitResponse): if error: - raise BoltIncompleteCommitError(message, address=None) from error + raise IncompleteCommit(message) from error else: - raise BoltIncompleteCommitError(message, address=None) + raise IncompleteCommit(message) if direct_driver: if error: diff --git a/neo4j/work/simple.py b/neo4j/work/simple.py index 1015a587d..ead00b6e1 100644 --- a/neo4j/work/simple.py +++ b/neo4j/work/simple.py @@ -19,29 +19,31 @@ # limitations under the License. -from collections import deque from logging import getLogger from random import random -from time import perf_counter, sleep -from warnings import warn +from time import ( + perf_counter, + sleep, +) from neo4j.conf import SessionConfig -from neo4j.api import READ_ACCESS, WRITE_ACCESS -from neo4j.data import DataHydrator, DataDehydrator +from neo4j.api import ( + READ_ACCESS, + WRITE_ACCESS, +) +from neo4j.data import DataHydrator from neo4j.exceptions import ( + ClientError, + IncompleteCommit, Neo4jError, ServiceUnavailable, - TransientError, SessionExpired, + TransientError, TransactionError, - ClientError, ) -from neo4j._exceptions import BoltIncompleteCommitError from neo4j.work import Workspace -from neo4j.work.summary import ResultSummary -from neo4j.work.transaction import Transaction from neo4j.work.result import Result -from neo4j.io._bolt3 import Bolt3 +from neo4j.work.transaction import Transaction log = getLogger("neo4j") @@ -302,7 +304,6 @@ def _run_transaction(self, access_mode, transaction_function, *args, **kwargs): t0 = -1 # Timer while True: - committing = False try: self._open_transaction(access_mode=access_mode, database=self._config.database, metadata=metadata, timeout=timeout) tx = self._transaction @@ -312,13 +313,10 @@ def _run_transaction(self, access_mode, transaction_function, *args, **kwargs): tx.rollback() raise else: - committing = True tx.commit() + except IncompleteCommit: + raise except (ServiceUnavailable, SessionExpired) as error: - if committing: - # Failed commit might have been executed on the server - # => no retry - raise errors.append(error) self._disconnect() except TransientError as transient_error: diff --git a/neo4j/work/transaction.py b/neo4j/work/transaction.py index 7cb7dc968..7c11139cf 100644 --- a/neo4j/work/transaction.py +++ b/neo4j/work/transaction.py @@ -21,9 +21,8 @@ from neo4j.work.result import Result from neo4j.data import DataHydrator -from neo4j._exceptions import BoltIncompleteCommitError from neo4j.exceptions import ( - ServiceUnavailable, + IncompleteCommit, TransactionError, ) @@ -132,13 +131,10 @@ def commit(self): self._connection.commit(on_success=metadata.update) self._connection.send_all() self._connection.fetch_all() - except BoltIncompleteCommitError: + self._bookmark = metadata.get("bookmark") + finally: self._closed = True self._on_closed() - raise ServiceUnavailable("Connection closed during commit") - self._bookmark = metadata.get("bookmark") - self._closed = True - self._on_closed() return self._bookmark diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index fda28b460..3681a82b8 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -42,6 +42,7 @@ RoutingServiceUnavailable, WriteServiceUnavailable, ReadServiceUnavailable, + IncompleteCommit, ConfigurationError, AuthConfigurationError, CertificateConfigurationError, @@ -60,7 +61,6 @@ BoltConnectionBroken, BoltConnectionClosed, BoltFailure, - BoltIncompleteCommitError, BoltProtocolError, ) From 240c1e692d3268d244f86a6f84735a032fccae31 Mon Sep 17 00:00:00 2001 From: Rouven Bauer Date: Thu, 1 Apr 2021 17:50:44 +0200 Subject: [PATCH 3/3] Don't import imports... --- tests/unit/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index ccffd37f3..1431914b4 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -23,7 +23,7 @@ from uuid import uuid4 import neo4j.api -from neo4j.work.simple import DataDehydrator +from neo4j.data import DataDehydrator from neo4j.exceptions import ( ConfigurationError, )