Skip to content

Commit 3432e01

Browse files
changed the timer so it is initiated after the first try (#401)
* changed the timer so it is initiated after the first try * added is_retriable method to TransientError
1 parent 00e86ec commit 3432e01

File tree

4 files changed

+72
-19
lines changed

4 files changed

+72
-19
lines changed

neo4j/exceptions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,17 @@ class TransientError(Neo4jError):
139139
""" The database cannot service the request right now, retrying later might yield a successful outcome.
140140
"""
141141

142+
def is_retriable(self):
143+
"""These are really client errors but classification on the server is not entirely correct and they are classified as transient.
144+
145+
:return: True if it is a retriable TransientError, otherwise False.
146+
:rtype: bool
147+
"""
148+
return not (self.code in (
149+
"Neo.TransientError.Transaction.Terminated",
150+
"Neo.TransientError.Transaction.LockClientStopped",
151+
))
152+
142153

143154
class DatabaseUnavailable(TransientError):
144155
"""

neo4j/work/simple.py

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -280,11 +280,12 @@ def _run_transaction(self, access_mode, unit_of_work, *args, **kwargs):
280280
metadata = getattr(unit_of_work, "metadata", None)
281281
timeout = getattr(unit_of_work, "timeout", None)
282282

283-
retry_delay = retry_delay_generator(self._config.initial_retry_delay,
284-
self._config.retry_delay_multiplier,
285-
self._config.retry_delay_jitter_factor)
283+
retry_delay = retry_delay_generator(self._config.initial_retry_delay, self._config.retry_delay_multiplier, self._config.retry_delay_jitter_factor)
284+
286285
errors = []
287-
t0 = perf_counter()
286+
287+
t0 = -1 # Timer
288+
288289
while True:
289290
try:
290291
self._open_transaction(access_mode=access_mode, database=self._config.database, metadata=metadata, timeout=timeout)
@@ -299,20 +300,21 @@ def _run_transaction(self, access_mode, unit_of_work, *args, **kwargs):
299300
except (ServiceUnavailable, SessionExpired) as error:
300301
errors.append(error)
301302
self._disconnect()
302-
except TransientError as error:
303-
if is_retriable_transient_error(error):
304-
errors.append(error)
305-
else:
303+
except TransientError as transient_error:
304+
if not transient_error.is_retriable():
306305
raise
306+
errors.append(transient_error)
307307
else:
308308
return result
309+
if t0 == -1:
310+
t0 = perf_counter() # The timer should be started after the first attempt
309311
t1 = perf_counter()
310312
if t1 - t0 > self._config.max_transaction_retry_time:
311313
break
312314
delay = next(retry_delay)
313-
log.warning("Transaction failed and will be retried in {}s "
314-
"({})".format(delay, "; ".join(errors[-1].args)))
315+
log.warning("Transaction failed and will be retried in {}s ({})".format(delay, "; ".join(errors[-1].args)))
315316
sleep(delay)
317+
316318
if errors:
317319
raise errors[-1]
318320
else:
@@ -418,10 +420,3 @@ def retry_delay_generator(initial_delay, multiplier, jitter_factor):
418420
jitter = jitter_factor * delay
419421
yield delay - jitter + (2 * jitter * random())
420422
delay *= multiplier
421-
422-
423-
def is_retriable_transient_error(error):
424-
"""
425-
:type error: TransientError
426-
"""
427-
return not (error.code in ("Neo.TransientError.Transaction.Terminated", "Neo.TransientError.Transaction.LockClientStopped"))

tests/integration/test_tx_functions.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
import pytest
2323
from uuid import uuid4
2424

25-
2625
from neo4j.work.simple import unit_of_work
27-
from neo4j.exceptions import ClientError
26+
from neo4j.exceptions import (
27+
Neo4jError,
28+
ClientError,
29+
)
2830

2931
# python -m pytest tests/integration/test_tx_functions.py -s -v
3032

@@ -100,3 +102,27 @@ def f(tx, uuid):
100102

101103
with pytest.raises(TypeError):
102104
session.write_transaction(f)
105+
106+
107+
def test_retry_logic(driver):
108+
# python -m pytest tests/integration/test_tx_functions.py -s -v -k test_retry_logic
109+
110+
pytest.global_counter = 0
111+
112+
def get_one(tx):
113+
result = tx.run("UNWIND [1,2,3,4] AS x RETURN x")
114+
records = list(result)
115+
pytest.global_counter += 1
116+
117+
if pytest.global_counter < 3:
118+
database_unavailable = Neo4jError.hydrate(message="The database is not currently available to serve your request, refer to the database logs for more details. Retrying your request at a later time may succeed.", code="Neo.TransientError.Database.DatabaseUnavailable")
119+
raise database_unavailable
120+
121+
return records
122+
123+
with driver.session() as session:
124+
records = session.read_transaction(get_one)
125+
126+
assert pytest.global_counter == 3
127+
128+
del pytest.global_counter

tests/unit/test_exceptions.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,24 @@ def test_neo4jerror_hydrate_with_message_and_code_client():
217217
assert error.metadata == {}
218218
assert error.message == "Test error message"
219219
assert error.code == "Neo.{}.General.TestError".format(CLASSIFICATION_CLIENT)
220+
221+
222+
def test_transient_error_is_retriable_case_1():
223+
error = Neo4jError.hydrate(message="Test error message", code="Neo.TransientError.Transaction.Terminated")
224+
225+
assert isinstance(error, TransientError)
226+
assert error.is_retriable() is False
227+
228+
229+
def test_transient_error_is_retriable_case_2():
230+
error = Neo4jError.hydrate(message="Test error message", code="Neo.TransientError.Transaction.LockClientStopped")
231+
232+
assert isinstance(error, TransientError)
233+
assert error.is_retriable() is False
234+
235+
236+
def test_transient_error_is_retriable_case_3():
237+
error = Neo4jError.hydrate(message="Test error message", code="Neo.TransientError.General.TestError")
238+
239+
assert isinstance(error, TransientError)
240+
assert error.is_retriable() is True

0 commit comments

Comments
 (0)