diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a0f91c0f..ba5f1dc8e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - flake8-bugbear - flake8-builtins - flake8-docstrings - - flake8-picky-parentheses + - flake8-picky-parentheses >= 0.5.3 - flake8-quotes - pep8-naming - repo: https://github.com/pycqa/isort diff --git a/tests/stub/retry/scripts/commit_disconnect.script b/tests/stub/retry/scripts/commit_disconnect.script index 3eaf2e84f..baa0e9790 100644 --- a/tests/stub/retry/scripts/commit_disconnect.script +++ b/tests/stub/retry/scripts/commit_disconnect.script @@ -1,4 +1,4 @@ -!: BOLT 4.3 +!: BOLT 4.4 A: HELLO {"{}": "*"} *: RESET diff --git a/tests/stub/retry/scripts/read.script b/tests/stub/retry/scripts/read.script index fc65ad150..4e143ef68 100644 --- a/tests/stub/retry/scripts/read.script +++ b/tests/stub/retry/scripts/read.script @@ -1,4 +1,4 @@ -!: BOLT 4.3 +!: BOLT 4.4 A: HELLO {"{}": "*"} *: RESET diff --git a/tests/stub/retry/scripts/read_syntax_error.script b/tests/stub/retry/scripts/read_syntax_error.script index 66ebe7e8b..54fc30144 100644 --- a/tests/stub/retry/scripts/read_syntax_error.script +++ b/tests/stub/retry/scripts/read_syntax_error.script @@ -1,4 +1,4 @@ -!: BOLT 4.3 +!: BOLT 4.4 A: HELLO {"{}": "*"} *: RESET diff --git a/tests/stub/retry/scripts/reset_fails_after_pull.script b/tests/stub/retry/scripts/reset_fails_after_pull.script new file mode 100644 index 000000000..446cb39a9 --- /dev/null +++ b/tests/stub/retry/scripts/reset_fails_after_pull.script @@ -0,0 +1,58 @@ +!: BOLT 4.4 +!: ALLOW RESTART +!: PY invalid_responses = round = 0 + +A: HELLO {"{}": "*"} + +IF: round == 0 +{{ + PY: round += 1 + *: RESET + + {? + # Optionally a transaction + C: BEGIN {"{}": "*"} + S: SUCCESS {} + ?} + C: RUN {"U": "*"} {"{}": "*"} {"{}": "*"} + S: SUCCESS {"t_first": 2, "fields": ["n"], "qid": 0} + C: PULL {"n": {"Z": "*"}} + S: FAILURE {"code": "Neo.TransientError.Statement.RemoteExecutionTransientError", "message": "Remote execution failed with code N/A and message 'Failed to obtain connection towards READ server. Known routing table is: Ttl 1703005857129, currentTime 1703005847132, routers [], writers [], readers [], database 'neo4j''"} + {* + C: RESET + #INVALID_RESPONSE# + PY: invalid_responses += 1 + *} +}} +ELIF: round == 1 +{{ + PY: round += 1 + *: RESET + + {{ + C: BEGIN {"{}": "*"} + S: SUCCESS {} + C: RUN {"U": "*"} {"{}": "*"} {"{}": "*"} + S: SUCCESS {"t_first": 2, "fields": ["n"], "qid": 0} + C: PULL {"n": {"Z": "*"}} + S: RECORD [1] + S: SUCCESS {"type": "r"} + C: COMMIT + S: SUCCESS {} + ---- + C: RUN {"U": "*"} {"{}": "*"} {"{}": "*"} + S: SUCCESS {"t_first": 2, "fields": ["n"], "qid": 0} + C: PULL {"n": {"Z": "*"}} + S: RECORD [1] + S: SUCCESS {"type": "r"} + }} + + *: RESET +}} +ELSE: +{{ + PY: round += 1 + S: +}} + +?: GOODBYE diff --git a/tests/stub/retry/scripts/retry_with_fail_after_commit.script b/tests/stub/retry/scripts/retry_with_fail_after_commit.script index cd46beb82..6d25477fc 100644 --- a/tests/stub/retry/scripts/retry_with_fail_after_commit.script +++ b/tests/stub/retry/scripts/retry_with_fail_after_commit.script @@ -1,4 +1,4 @@ -!: BOLT 4.3 +!: BOLT 4.4 A: HELLO {"{}": "*"} C: BEGIN {} diff --git a/tests/stub/retry/scripts/retry_with_fail_after_pull.script b/tests/stub/retry/scripts/retry_with_fail_after_pull.script index 00147c9ca..6552da98e 100644 --- a/tests/stub/retry/scripts/retry_with_fail_after_pull.script +++ b/tests/stub/retry/scripts/retry_with_fail_after_pull.script @@ -1,4 +1,4 @@ -!: BOLT 4.3 +!: BOLT 4.4 A: HELLO {"{}": "*"} C: BEGIN {} diff --git a/tests/stub/retry/scripts/retry_with_fail_after_pull_server1.script b/tests/stub/retry/scripts/retry_with_fail_after_pull_server1.script index 0ef08e5fa..f9e2e9a35 100644 --- a/tests/stub/retry/scripts/retry_with_fail_after_pull_server1.script +++ b/tests/stub/retry/scripts/retry_with_fail_after_pull_server1.script @@ -1,4 +1,4 @@ -!: BOLT 4.3 +!: BOLT 4.4 A: HELLO {"{}": "*"} C: BEGIN {} diff --git a/tests/stub/retry/scripts/retry_with_fail_after_pull_server2.script b/tests/stub/retry/scripts/retry_with_fail_after_pull_server2.script index 8121a367d..f20c64ca4 100644 --- a/tests/stub/retry/scripts/retry_with_fail_after_pull_server2.script +++ b/tests/stub/retry/scripts/retry_with_fail_after_pull_server2.script @@ -1,4 +1,4 @@ -!: BOLT 4.3 +!: BOLT 4.4 A: HELLO {"{}": "*"} C: BEGIN {} diff --git a/tests/stub/retry/scripts/tx_pull_yielding_failure.script b/tests/stub/retry/scripts/tx_pull_yielding_failure.script index b9f10b429..21f81ea21 100644 --- a/tests/stub/retry/scripts/tx_pull_yielding_failure.script +++ b/tests/stub/retry/scripts/tx_pull_yielding_failure.script @@ -1,4 +1,4 @@ -!: BOLT 4.3 +!: BOLT 4.4 A: HELLO {"{}": "*"} C: BEGIN {} diff --git a/tests/stub/retry/scripts/write_syntax_error.script b/tests/stub/retry/scripts/write_syntax_error.script index 9a33c92e6..dfd385e50 100644 --- a/tests/stub/retry/scripts/write_syntax_error.script +++ b/tests/stub/retry/scripts/write_syntax_error.script @@ -1,4 +1,4 @@ -!: BOLT 4.3 +!: BOLT 4.4 A: HELLO {"{}": "*"} *: RESET diff --git a/tests/stub/retry/test_retry.py b/tests/stub/retry/test_retry.py index ee2060d09..1c0f672b4 100644 --- a/tests/stub/retry/test_retry.py +++ b/tests/stub/retry/test_retry.py @@ -9,7 +9,7 @@ class TestRetry(TestkitTestCase): - required_features = types.Feature.BOLT_4_3, + required_features = types.Feature.BOLT_4_4, def setUp(self): super().setUp() @@ -222,4 +222,105 @@ def once(tx): for failure in failures: with self.subTest(failure=failure): _test() - self._server.reset() + + def test_reset_fails_after_pull(self): + def _test(invalid_response_, api_): + def check_exception(exc): + self.assertEqual( + exc.exception.code, + "Neo.TransientError.Statement." + "RemoteExecutionTransientError" + ) + if self.driver_supports_features( + types.Feature.API_RETRYABLE_EXCEPTION + ): + self.assertTrue(exc.exception.retryable) + + def api_call(session_): + if api_ == "session": + with self.assertRaises(types.DriverError) as exc: + result = session_.run("RETURN 1 AS n") + list(result) + check_exception(exc) + elif api_ == "explicit_tx": + tx = session_.begin_transaction() + try: + with self.assertRaises(types.DriverError) as exc: + result = tx.run("RETURN 1 AS n") + list(result) + check_exception(exc) + finally: + tx.close() + elif api_ == "managed_tx": + run = 0 + + def work(tx): + nonlocal run + run += 1 + if run == 1: + with self.assertRaises(types.DriverError) as exc: + result = tx.run("RETURN 1 AS n") + list(result) + check_exception(exc) + raise exc.exception + else: + result = tx.run("RETURN 1 AS n") + return list(result) + + records = session_.execute_write(work) + assert len(records) == 1 + self.assertEqual(records, [ + types.Record(values=[types.CypherInt(1)]) + ]) + else: + raise ValueError(f"Unknown API: {api_}") + + self._server.start( + path=self.script_path("reset_fails_after_pull.script"), + vars_={ + "#INVALID_RESPONSE#": invalid_response_, + } + ) + auth = types.AuthorizationToken("basic", principal="", + credentials="") + driver = Driver(self._backend, + "bolt://%s" % self._server.address, auth) + try: + session = driver.session("r") + try: + api_call(session) + + finally: + session.close() + # driver should've killed the misbehaving connection + try: + self.assertEqual( + self._server.count_responses(""), 1 + ) + finally: + self._server._dump() + finally: + driver.close() + self._server.done() + + invalid_responses = ( + ( + 'S: FAILURE {"code": "Neo.ClientError.General.Unknown", ' + '"message": "The driver should ignore this error!"}' + ), + "S: IGNORED", + ( + "# MIXED \n" + "IF: invalid_responses <= 1\n" + ' S: FAILURE {"code": "Neo.ClientError.General.Unknown", ' + '"message": "The driver should ignore this error!"}\n' + "ELSE:\n" + " S: IGNORED\n" + ) + ) + for invalid_response in invalid_responses: + for api in ("session", "explicit_tx", "managed_tx"): + with self.subTest(response=invalid_response[2:10].strip(), + api=api): + _test(invalid_response, api) + self._server.reset() diff --git a/tests/stub/retry/test_retry_clustering.py b/tests/stub/retry/test_retry_clustering.py index cc5ccc9d3..e1e739fad 100644 --- a/tests/stub/retry/test_retry_clustering.py +++ b/tests/stub/retry/test_retry_clustering.py @@ -9,7 +9,7 @@ class TestRetryClustering(TestkitTestCase): - required_features = types.Feature.BOLT_4_3, + required_features = types.Feature.BOLT_4_4, def setUp(self): super().setUp() @@ -283,7 +283,7 @@ def twice(tx): def get_vars(self): host = self._routingServer.host v = { - "#VERSION#": "4.3", + "#VERSION#": "4.4", "#HOST#": host, "#ROUTINGCTX#": '{"address": "' + host