From 263b8a5cc07788ebb43a10d95a2463f13381d467 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Tue, 2 Apr 2024 14:16:50 +0200 Subject: [PATCH 1/4] Fix handling of sub-ms transaction timeouts Transaction timeouts are specified in seconds as float. However, the server expects it in milliseconds as int. This would lead to 1) rounding issues: previously, the driver would multiply by 1000 and then truncate to int. E.g., 256.4 seconds would be turned into 256399 ms because of float imprecision. Therefore, the built-in `round` is now used instead. 2) values below 1 ms (e.g., 0.0001) would be rounded down to 0. However, 0 is a special value that instructs the server to not apply any timeout. This is likely to surprise the user which specified a non-zero timeout. In this special case, the driver now rounds up to 1 ms. Back-port of: https://github.com/neo4j/neo4j-python-driver/pull/940 --- neo4j/io/_bolt3.py | 16 +++----- neo4j/io/_bolt4.py | 32 +++++----------- neo4j/io/_common.py | 30 +++++++++++++++ tests/unit/io/test_class_bolt3.py | 57 +++++++++++++++++++++++++++++ tests/unit/io/test_class_bolt4x0.py | 57 +++++++++++++++++++++++++++++ tests/unit/io/test_class_bolt4x1.py | 57 +++++++++++++++++++++++++++++ tests/unit/io/test_class_bolt4x2.py | 57 +++++++++++++++++++++++++++++ tests/unit/io/test_class_bolt4x3.py | 57 +++++++++++++++++++++++++++++ tests/unit/io/test_class_bolt4x4.py | 57 +++++++++++++++++++++++++++++ 9 files changed, 386 insertions(+), 34 deletions(-) diff --git a/neo4j/io/_bolt3.py b/neo4j/io/_bolt3.py index 21f71b180..e180c4425 100644 --- a/neo4j/io/_bolt3.py +++ b/neo4j/io/_bolt3.py @@ -47,6 +47,7 @@ CommitResponse, InitResponse, Response, + tx_timeout_as_ms, ) @@ -225,11 +226,8 @@ def run(self, query, parameters=None, mode=None, bookmarks=None, extra["tx_metadata"] = dict(metadata) except TypeError: raise TypeError("Metadata must be coercible to a dict") - if timeout: - try: - extra["tx_timeout"] = int(1000 * timeout) - except TypeError: - raise TypeError("Timeout must be specified as a number of seconds") + if timeout or timeout == 0: + extra["tx_timeout"] = tx_timeout_as_ms(timeout) fields = (query, parameters, extra) log.debug("[#%04X] C: RUN %s", self.local_port, " ".join(map(repr, fields))) if query.upper() == u"COMMIT": @@ -277,12 +275,8 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None, extra["tx_metadata"] = dict(metadata) except TypeError: raise TypeError("Metadata must be coercible to a dict") - if timeout: - try: - extra["tx_timeout"] = int(1000 * timeout) - except TypeError: - raise TypeError("Timeout must be specified as a number of seconds") - log.debug("[#%04X] C: BEGIN %r", self.local_port, extra) + if timeout or timeout == 0: + extra["tx_timeout"] = tx_timeout_as_ms(timeout) self._append(b"\x11", (extra,), Response(self, "begin", **handlers)) def commit(self, **handlers): diff --git a/neo4j/io/_bolt4.py b/neo4j/io/_bolt4.py index 2feeea14b..b06e01383 100644 --- a/neo4j/io/_bolt4.py +++ b/neo4j/io/_bolt4.py @@ -34,7 +34,6 @@ from neo4j.exceptions import ( ConfigurationError, DatabaseUnavailable, - DriverError, ForbiddenOnReadOnlyDatabase, Neo4jError, NotALeader, @@ -48,6 +47,7 @@ CommitResponse, InitResponse, Response, + tx_timeout_as_ms, ) from neo4j.io._bolt3 import ( ServerStateManager, @@ -178,11 +178,8 @@ def run(self, query, parameters=None, mode=None, bookmarks=None, extra["tx_metadata"] = dict(metadata) except TypeError: raise TypeError("Metadata must be coercible to a dict") - if timeout: - try: - extra["tx_timeout"] = int(1000 * timeout) - except TypeError: - raise TypeError("Timeout must be specified as a number of seconds") + if timeout or timeout == 0: + extra["tx_timeout"] = tx_timeout_as_ms(timeout) fields = (query, parameters, extra) log.debug("[#%04X] C: RUN %s", self.local_port, " ".join(map(repr, fields))) if query.upper() == u"COMMIT": @@ -229,11 +226,8 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None, extra["tx_metadata"] = dict(metadata) except TypeError: raise TypeError("Metadata must be coercible to a dict") - if timeout: - try: - extra["tx_timeout"] = int(1000 * timeout) - except TypeError: - raise TypeError("Timeout must be specified as a number of seconds") + if timeout or timeout == 0: + extra["tx_timeout"] = tx_timeout_as_ms(timeout) log.debug("[#%04X] C: BEGIN %r", self.local_port, extra) self._append(b"\x11", (extra,), Response(self, "begin", **handlers)) @@ -490,12 +484,8 @@ def run(self, query, parameters=None, mode=None, bookmarks=None, extra["tx_metadata"] = dict(metadata) except TypeError: raise TypeError("Metadata must be coercible to a dict") - if timeout: - try: - extra["tx_timeout"] = int(1000 * timeout) - except TypeError: - raise TypeError("Timeout must be specified as a number of " - "seconds") + if timeout or timeout == 0: + extra["tx_timeout"] = tx_timeout_as_ms(timeout) fields = (query, parameters, extra) log.debug("[#%04X] C: RUN %s", self.local_port, " ".join(map(repr, fields))) @@ -525,11 +515,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None, extra["tx_metadata"] = dict(metadata) except TypeError: raise TypeError("Metadata must be coercible to a dict") - if timeout: - try: - extra["tx_timeout"] = int(1000 * timeout) - except TypeError: - raise TypeError("Timeout must be specified as a number of " - "seconds") + if timeout or timeout == 0: + extra["tx_timeout"] = tx_timeout_as_ms(timeout) log.debug("[#%04X] C: BEGIN %r", self.local_port, extra) self._append(b"\x11", (extra,), Response(self, "begin", **handlers)) diff --git a/neo4j/io/_common.py b/neo4j/io/_common.py index f11ddd8e0..23d16196a 100644 --- a/neo4j/io/_common.py +++ b/neo4j/io/_common.py @@ -270,3 +270,33 @@ def on_failure(self, metadata): class CommitResponse(Response): pass + + +def tx_timeout_as_ms(timeout: float) -> int: + """ + Round transaction timeout to milliseconds. + + Values in (0, 1], else values are rounded using the built-in round() + function (round n.5 values to nearest even). + + :param timeout: timeout in seconds (must be >= 0) + + :returns: timeout in milliseconds (rounded) + + :raise ValueError: if timeout is negative + """ + try: + timeout = float(timeout) + except (TypeError, ValueError) as e: + err_type = type(e) + msg = "Timeout must be specified as a number of seconds" + raise err_type(msg) from None + if timeout < 0: + raise ValueError("Timeout must be a positive number or 0.") + ms = int(round(1000 * timeout)) + if ms == 0 and timeout > 0: + # Special case for 0 < timeout < 0.5 ms. + # This would be rounded to 0 ms, but the server interprets this as + # infinite timeout. So we round to the smallest possible timeout: 1 ms. + ms = 1 + return ms diff --git a/tests/unit/io/test_class_bolt3.py b/tests/unit/io/test_class_bolt3.py index f7d63e855..de145d306 100644 --- a/tests/unit/io/test_class_bolt3.py +++ b/tests/unit/io/test_class_bolt3.py @@ -110,3 +110,60 @@ def test_hint_recv_timeout_seconds_gets_ignored(fake_socket_pair, recv_timeout): PoolConfig.max_connection_lifetime) connection.hello() sockets.client.settimeout.assert_not_called() + + +@pytest.mark.parametrize( + ("func", "args", "extra_idx"), + ( + ("run", ("RETURN 1",), 2), + ("begin", (), 0), + ) +) +@pytest.mark.parametrize( + ("timeout", "res"), + ( + (None, None), + (0, 0), + (0.1, 100), + (0.001, 1), + (1e-15, 1), + (0.0005, 1), + (0.0001, 1), + (1.0015, 1002), + (1.000499, 1000), + (1.0025, 1002), + (3.0005, 3000), + (3.456, 3456), + (1, 1000), + ( + -1e-15, + ValueError("Timeout must be a positive number or 0") + ), + ( + "foo", + ValueError("Timeout must be specified as a number of seconds") + ), + ( + [1, 2], + TypeError("Timeout must be specified as a number of seconds") + ) + ) +) +def test_tx_timeout(fake_socket_pair, func, args, extra_idx, timeout, res): + address = ("127.0.0.1", 7687) + sockets = fake_socket_pair(address) + sockets.server.send_message(0x70, {}) + connection = Bolt3(address, sockets.client, 0) + func = getattr(connection, func) + if isinstance(res, Exception): + with pytest.raises(type(res), match=str(res)): + func(*args, timeout=timeout) + else: + func(*args, timeout=timeout) + connection.send_all() + tag, fields = sockets.server.pop_message() + extra = fields[extra_idx] + if timeout is None: + assert "tx_timeout" not in extra + else: + assert extra["tx_timeout"] == res diff --git a/tests/unit/io/test_class_bolt4x0.py b/tests/unit/io/test_class_bolt4x0.py index 3879acb0c..7db2d4bde 100644 --- a/tests/unit/io/test_class_bolt4x0.py +++ b/tests/unit/io/test_class_bolt4x0.py @@ -197,3 +197,60 @@ def test_hint_recv_timeout_seconds_gets_ignored(fake_socket_pair, recv_timeout): PoolConfig.max_connection_lifetime) connection.hello() sockets.client.settimeout.assert_not_called() + + +@pytest.mark.parametrize( + ("func", "args", "extra_idx"), + ( + ("run", ("RETURN 1",), 2), + ("begin", (), 0), + ) +) +@pytest.mark.parametrize( + ("timeout", "res"), + ( + (None, None), + (0, 0), + (0.1, 100), + (0.001, 1), + (1e-15, 1), + (0.0005, 1), + (0.0001, 1), + (1.0015, 1002), + (1.000499, 1000), + (1.0025, 1002), + (3.0005, 3000), + (3.456, 3456), + (1, 1000), + ( + -1e-15, + ValueError("Timeout must be a positive number or 0") + ), + ( + "foo", + ValueError("Timeout must be specified as a number of seconds") + ), + ( + [1, 2], + TypeError("Timeout must be specified as a number of seconds") + ) + ) +) +def test_tx_timeout(fake_socket_pair, func, args, extra_idx, timeout, res): + address = ("127.0.0.1", 7687) + sockets = fake_socket_pair(address) + sockets.server.send_message(0x70, {}) + connection = Bolt4x0(address, sockets.client, 0) + func = getattr(connection, func) + if isinstance(res, Exception): + with pytest.raises(type(res), match=str(res)): + func(*args, timeout=timeout) + else: + func(*args, timeout=timeout) + connection.send_all() + tag, fields = sockets.server.pop_message() + extra = fields[extra_idx] + if timeout is None: + assert "tx_timeout" not in extra + else: + assert extra["tx_timeout"] == res diff --git a/tests/unit/io/test_class_bolt4x1.py b/tests/unit/io/test_class_bolt4x1.py index 663d3cbe1..458e68089 100644 --- a/tests/unit/io/test_class_bolt4x1.py +++ b/tests/unit/io/test_class_bolt4x1.py @@ -210,3 +210,60 @@ def test_hint_recv_timeout_seconds_gets_ignored(fake_socket_pair, recv_timeout): PoolConfig.max_connection_lifetime) connection.hello() sockets.client.settimeout.assert_not_called() + + +@pytest.mark.parametrize( + ("func", "args", "extra_idx"), + ( + ("run", ("RETURN 1",), 2), + ("begin", (), 0), + ) +) +@pytest.mark.parametrize( + ("timeout", "res"), + ( + (None, None), + (0, 0), + (0.1, 100), + (0.001, 1), + (1e-15, 1), + (0.0005, 1), + (0.0001, 1), + (1.0015, 1002), + (1.000499, 1000), + (1.0025, 1002), + (3.0005, 3000), + (3.456, 3456), + (1, 1000), + ( + -1e-15, + ValueError("Timeout must be a positive number or 0") + ), + ( + "foo", + ValueError("Timeout must be specified as a number of seconds") + ), + ( + [1, 2], + TypeError("Timeout must be specified as a number of seconds") + ) + ) +) +def test_tx_timeout(fake_socket_pair, func, args, extra_idx, timeout, res): + address = ("127.0.0.1", 7687) + sockets = fake_socket_pair(address) + sockets.server.send_message(0x70, {}) + connection = Bolt4x1(address, sockets.client, 0) + func = getattr(connection, func) + if isinstance(res, Exception): + with pytest.raises(type(res), match=str(res)): + func(*args, timeout=timeout) + else: + func(*args, timeout=timeout) + connection.send_all() + tag, fields = sockets.server.pop_message() + extra = fields[extra_idx] + if timeout is None: + assert "tx_timeout" not in extra + else: + assert extra["tx_timeout"] == res diff --git a/tests/unit/io/test_class_bolt4x2.py b/tests/unit/io/test_class_bolt4x2.py index 470adf5c7..104db411b 100644 --- a/tests/unit/io/test_class_bolt4x2.py +++ b/tests/unit/io/test_class_bolt4x2.py @@ -211,3 +211,60 @@ def test_hint_recv_timeout_seconds_gets_ignored(fake_socket_pair, recv_timeout): PoolConfig.max_connection_lifetime) connection.hello() sockets.client.settimeout.assert_not_called() + + +@pytest.mark.parametrize( + ("func", "args", "extra_idx"), + ( + ("run", ("RETURN 1",), 2), + ("begin", (), 0), + ) +) +@pytest.mark.parametrize( + ("timeout", "res"), + ( + (None, None), + (0, 0), + (0.1, 100), + (0.001, 1), + (1e-15, 1), + (0.0005, 1), + (0.0001, 1), + (1.0015, 1002), + (1.000499, 1000), + (1.0025, 1002), + (3.0005, 3000), + (3.456, 3456), + (1, 1000), + ( + -1e-15, + ValueError("Timeout must be a positive number or 0") + ), + ( + "foo", + ValueError("Timeout must be specified as a number of seconds") + ), + ( + [1, 2], + TypeError("Timeout must be specified as a number of seconds") + ) + ) +) +def test_tx_timeout(fake_socket_pair, func, args, extra_idx, timeout, res): + address = ("127.0.0.1", 7687) + sockets = fake_socket_pair(address) + sockets.server.send_message(0x70, {}) + connection = Bolt4x2(address, sockets.client, 0) + func = getattr(connection, func) + if isinstance(res, Exception): + with pytest.raises(type(res), match=str(res)): + func(*args, timeout=timeout) + else: + func(*args, timeout=timeout) + connection.send_all() + tag, fields = sockets.server.pop_message() + extra = fields[extra_idx] + if timeout is None: + assert "tx_timeout" not in extra + else: + assert extra["tx_timeout"] == res diff --git a/tests/unit/io/test_class_bolt4x3.py b/tests/unit/io/test_class_bolt4x3.py index fc08f5b92..75b52cef8 100644 --- a/tests/unit/io/test_class_bolt4x3.py +++ b/tests/unit/io/test_class_bolt4x3.py @@ -237,3 +237,60 @@ def test_hint_recv_timeout_seconds(fake_socket_pair, hints, valid, and "recv_timeout_seconds" in msg and "invalid" in msg for msg in caplog.messages) + + +@pytest.mark.parametrize( + ("func", "args", "extra_idx"), + ( + ("run", ("RETURN 1",), 2), + ("begin", (), 0), + ) +) +@pytest.mark.parametrize( + ("timeout", "res"), + ( + (None, None), + (0, 0), + (0.1, 100), + (0.001, 1), + (1e-15, 1), + (0.0005, 1), + (0.0001, 1), + (1.0015, 1002), + (1.000499, 1000), + (1.0025, 1002), + (3.0005, 3000), + (3.456, 3456), + (1, 1000), + ( + -1e-15, + ValueError("Timeout must be a positive number or 0") + ), + ( + "foo", + ValueError("Timeout must be specified as a number of seconds") + ), + ( + [1, 2], + TypeError("Timeout must be specified as a number of seconds") + ) + ) +) +def test_tx_timeout(fake_socket_pair, func, args, extra_idx, timeout, res): + address = ("127.0.0.1", 7687) + sockets = fake_socket_pair(address) + sockets.server.send_message(0x70, {}) + connection = Bolt4x3(address, sockets.client, 0) + func = getattr(connection, func) + if isinstance(res, Exception): + with pytest.raises(type(res), match=str(res)): + func(*args, timeout=timeout) + else: + func(*args, timeout=timeout) + connection.send_all() + tag, fields = sockets.server.pop_message() + extra = fields[extra_idx] + if timeout is None: + assert "tx_timeout" not in extra + else: + assert extra["tx_timeout"] == res diff --git a/tests/unit/io/test_class_bolt4x4.py b/tests/unit/io/test_class_bolt4x4.py index 19378a1cd..9e1b2fe39 100644 --- a/tests/unit/io/test_class_bolt4x4.py +++ b/tests/unit/io/test_class_bolt4x4.py @@ -249,3 +249,60 @@ def test_hint_recv_timeout_seconds(fake_socket_pair, hints, valid, and "recv_timeout_seconds" in msg and "invalid" in msg for msg in caplog.messages) + + +@pytest.mark.parametrize( + ("func", "args", "extra_idx"), + ( + ("run", ("RETURN 1",), 2), + ("begin", (), 0), + ) +) +@pytest.mark.parametrize( + ("timeout", "res"), + ( + (None, None), + (0, 0), + (0.1, 100), + (0.001, 1), + (1e-15, 1), + (0.0005, 1), + (0.0001, 1), + (1.0015, 1002), + (1.000499, 1000), + (1.0025, 1002), + (3.0005, 3000), + (3.456, 3456), + (1, 1000), + ( + -1e-15, + ValueError("Timeout must be a positive number or 0") + ), + ( + "foo", + ValueError("Timeout must be specified as a number of seconds") + ), + ( + [1, 2], + TypeError("Timeout must be specified as a number of seconds") + ) + ) +) +def test_tx_timeout(fake_socket_pair, func, args, extra_idx, timeout, res): + address = ("127.0.0.1", 7687) + sockets = fake_socket_pair(address) + sockets.server.send_message(0x70, {}) + connection = Bolt4x4(address, sockets.client, 0) + func = getattr(connection, func) + if isinstance(res, Exception): + with pytest.raises(type(res), match=str(res)): + func(*args, timeout=timeout) + else: + func(*args, timeout=timeout) + connection.send_all() + tag, fields = sockets.server.pop_message() + extra = fields[extra_idx] + if timeout is None: + assert "tx_timeout" not in extra + else: + assert extra["tx_timeout"] == res From 1800a43366e46e6a4356b7ed57495ff0c08352de Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Wed, 10 Apr 2024 17:59:21 +0200 Subject: [PATCH 2/4] Improve backwards-compatibility --- neo4j/io/_bolt3.py | 4 ++-- neo4j/io/_bolt4.py | 8 ++++---- neo4j/io/_common.py | 6 ++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/neo4j/io/_bolt3.py b/neo4j/io/_bolt3.py index e180c4425..7664a10e3 100644 --- a/neo4j/io/_bolt3.py +++ b/neo4j/io/_bolt3.py @@ -226,7 +226,7 @@ def run(self, query, parameters=None, mode=None, bookmarks=None, extra["tx_metadata"] = dict(metadata) except TypeError: raise TypeError("Metadata must be coercible to a dict") - if timeout or timeout == 0: + if timeout or (isinstance(timeout, (float, int)) and timeout == 0): extra["tx_timeout"] = tx_timeout_as_ms(timeout) fields = (query, parameters, extra) log.debug("[#%04X] C: RUN %s", self.local_port, " ".join(map(repr, fields))) @@ -275,7 +275,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None, extra["tx_metadata"] = dict(metadata) except TypeError: raise TypeError("Metadata must be coercible to a dict") - if timeout or timeout == 0: + if timeout or (isinstance(timeout, (float, int)) and timeout == 0): extra["tx_timeout"] = tx_timeout_as_ms(timeout) self._append(b"\x11", (extra,), Response(self, "begin", **handlers)) diff --git a/neo4j/io/_bolt4.py b/neo4j/io/_bolt4.py index b06e01383..370a9ccff 100644 --- a/neo4j/io/_bolt4.py +++ b/neo4j/io/_bolt4.py @@ -178,7 +178,7 @@ def run(self, query, parameters=None, mode=None, bookmarks=None, extra["tx_metadata"] = dict(metadata) except TypeError: raise TypeError("Metadata must be coercible to a dict") - if timeout or timeout == 0: + if timeout or (isinstance(timeout, (float, int)) and timeout == 0): extra["tx_timeout"] = tx_timeout_as_ms(timeout) fields = (query, parameters, extra) log.debug("[#%04X] C: RUN %s", self.local_port, " ".join(map(repr, fields))) @@ -226,7 +226,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None, extra["tx_metadata"] = dict(metadata) except TypeError: raise TypeError("Metadata must be coercible to a dict") - if timeout or timeout == 0: + if timeout or (isinstance(timeout, (float, int)) and timeout == 0): extra["tx_timeout"] = tx_timeout_as_ms(timeout) log.debug("[#%04X] C: BEGIN %r", self.local_port, extra) self._append(b"\x11", (extra,), Response(self, "begin", **handlers)) @@ -484,7 +484,7 @@ def run(self, query, parameters=None, mode=None, bookmarks=None, extra["tx_metadata"] = dict(metadata) except TypeError: raise TypeError("Metadata must be coercible to a dict") - if timeout or timeout == 0: + if timeout or (isinstance(timeout, (float, int)) and timeout == 0): extra["tx_timeout"] = tx_timeout_as_ms(timeout) fields = (query, parameters, extra) log.debug("[#%04X] C: RUN %s", self.local_port, @@ -515,7 +515,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None, extra["tx_metadata"] = dict(metadata) except TypeError: raise TypeError("Metadata must be coercible to a dict") - if timeout or timeout == 0: + if timeout or (isinstance(timeout, (float, int)) and timeout == 0): extra["tx_timeout"] = tx_timeout_as_ms(timeout) log.debug("[#%04X] C: BEGIN %r", self.local_port, extra) self._append(b"\x11", (extra,), Response(self, "begin", **handlers)) diff --git a/neo4j/io/_common.py b/neo4j/io/_common.py index 23d16196a..fbf8fe126 100644 --- a/neo4j/io/_common.py +++ b/neo4j/io/_common.py @@ -279,7 +279,7 @@ def tx_timeout_as_ms(timeout: float) -> int: Values in (0, 1], else values are rounded using the built-in round() function (round n.5 values to nearest even). - :param timeout: timeout in seconds (must be >= 0) + :param timeout: timeout in seconds :returns: timeout in milliseconds (rounded) @@ -290,9 +290,7 @@ def tx_timeout_as_ms(timeout: float) -> int: except (TypeError, ValueError) as e: err_type = type(e) msg = "Timeout must be specified as a number of seconds" - raise err_type(msg) from None - if timeout < 0: - raise ValueError("Timeout must be a positive number or 0.") + raise err_type(msg) from e ms = int(round(1000 * timeout)) if ms == 0 and timeout > 0: # Special case for 0 < timeout < 0.5 ms. From ebcdc80f8c0e483bbc3b78add63da6c38558dd52 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Wed, 10 Apr 2024 17:59:38 +0200 Subject: [PATCH 3/4] Improve API docs --- neo4j/work/simple.py | 52 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/neo4j/work/simple.py b/neo4j/work/simple.py index d07aaba8d..6d9b89523 100644 --- a/neo4j/work/simple.py +++ b/neo4j/work/simple.py @@ -281,10 +281,19 @@ def begin_transaction(self, metadata=None, timeout=None): :param timeout: the transaction timeout in seconds. - Transactions that execute longer than the configured timeout will be terminated by the database. - This functionality allows to limit query/transaction execution time. - Specified timeout overrides the default timeout configured in the database using ``dbms.transaction.timeout`` setting. - Value should not represent a duration of zero or negative duration. + Transactions that execute longer than the configured timeout will + be terminated by the database. + This functionality allows user code to limit query/transaction + execution time. + The specified timeout overrides the default timeout configured in + the database using the ``db.transaction.timeout`` setting + (``dbms.transaction.timeout`` before Neo4j 5.0). + Values higher than ``db.transaction.timeout`` will be ignored and + will fall back to the default for server versions between 4.2 and + 5.2 (inclusive). + The value should not represent a negative duration. + A ``0`` duration will make the transaction execute indefinitely. + :data:`None` will use the default timeout configured on the server. :type timeout: int :returns: A new transaction instance. @@ -441,7 +450,21 @@ class Query: :type text: str :param metadata: metadata attached to the query. :type metadata: dict - :param timeout: seconds. + :param timeout: + the transaction timeout in seconds. + Transactions that execute longer than the configured timeout will + be terminated by the database. + This functionality allows user code to limit query/transaction + execution time. + The specified timeout overrides the default timeout configured in + the database using the ``db.transaction.timeout`` setting + (``dbms.transaction.timeout`` before Neo4j 5.0). + Values higher than ``db.transaction.timeout`` will be ignored and + will fall back to the default for server versions between 4.2 and + 5.2 (inclusive). + The value should not represent a negative duration. + A ``0`` duration will make the transaction execute indefinitely. + :data:`None` will use the default timeout configured on the server. :type timeout: float or :const:`None` """ def __init__(self, text, metadata=None, timeout=None): @@ -476,12 +499,19 @@ def count_people_tx(tx): :param timeout: the transaction timeout in seconds. - Transactions that execute longer than the configured timeout will be terminated by the database. - This functionality allows to limit query/transaction execution time. - Specified timeout overrides the default timeout configured in the database using ``dbms.transaction.timeout`` setting. - Values higher than ``dbms.transaction.timeout`` will be ignored and - will fall back to default (unless using Neo4j < 4.2). - Value should not represent a duration of zero or negative duration. + Transactions that execute longer than the configured timeout will + be terminated by the database. + This functionality allows user code to limit query/transaction + execution time. + The specified timeout overrides the default timeout configured in + the database using the ``db.transaction.timeout`` setting + (``dbms.transaction.timeout`` before Neo4j 5.0). + Values higher than ``db.transaction.timeout`` will be ignored and + will fall back to the default for server versions between 4.2 and + 5.2 (inclusive). + The value should not represent a negative duration. + A ``0`` duration will make the transaction execute indefinitely. + :data:`None` will use the default timeout configured on the server. :type timeout: float or :const:`None` """ From ba42c9027c1d677cab4cd87b78edc90d762435d7 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Thu, 11 Apr 2024 15:15:03 +0200 Subject: [PATCH 4/4] Adjust unit tests For better backwards compatibility, we're sending negative timeouts to the DBMS as the 4.4 driver did before (even though those values might not have the intuitive effect, we don't want to change the behavior of the driver too much in a patch release). --- tests/unit/io/test_class_bolt3.py | 4 ---- tests/unit/io/test_class_bolt4x0.py | 4 ---- tests/unit/io/test_class_bolt4x1.py | 4 ---- tests/unit/io/test_class_bolt4x2.py | 4 ---- tests/unit/io/test_class_bolt4x3.py | 4 ---- tests/unit/io/test_class_bolt4x4.py | 4 ---- 6 files changed, 24 deletions(-) diff --git a/tests/unit/io/test_class_bolt3.py b/tests/unit/io/test_class_bolt3.py index de145d306..0a5d49cdb 100644 --- a/tests/unit/io/test_class_bolt3.py +++ b/tests/unit/io/test_class_bolt3.py @@ -135,10 +135,6 @@ def test_hint_recv_timeout_seconds_gets_ignored(fake_socket_pair, recv_timeout): (3.0005, 3000), (3.456, 3456), (1, 1000), - ( - -1e-15, - ValueError("Timeout must be a positive number or 0") - ), ( "foo", ValueError("Timeout must be specified as a number of seconds") diff --git a/tests/unit/io/test_class_bolt4x0.py b/tests/unit/io/test_class_bolt4x0.py index 7db2d4bde..f797266d0 100644 --- a/tests/unit/io/test_class_bolt4x0.py +++ b/tests/unit/io/test_class_bolt4x0.py @@ -222,10 +222,6 @@ def test_hint_recv_timeout_seconds_gets_ignored(fake_socket_pair, recv_timeout): (3.0005, 3000), (3.456, 3456), (1, 1000), - ( - -1e-15, - ValueError("Timeout must be a positive number or 0") - ), ( "foo", ValueError("Timeout must be specified as a number of seconds") diff --git a/tests/unit/io/test_class_bolt4x1.py b/tests/unit/io/test_class_bolt4x1.py index 458e68089..34748750b 100644 --- a/tests/unit/io/test_class_bolt4x1.py +++ b/tests/unit/io/test_class_bolt4x1.py @@ -235,10 +235,6 @@ def test_hint_recv_timeout_seconds_gets_ignored(fake_socket_pair, recv_timeout): (3.0005, 3000), (3.456, 3456), (1, 1000), - ( - -1e-15, - ValueError("Timeout must be a positive number or 0") - ), ( "foo", ValueError("Timeout must be specified as a number of seconds") diff --git a/tests/unit/io/test_class_bolt4x2.py b/tests/unit/io/test_class_bolt4x2.py index 104db411b..80d53a4c5 100644 --- a/tests/unit/io/test_class_bolt4x2.py +++ b/tests/unit/io/test_class_bolt4x2.py @@ -236,10 +236,6 @@ def test_hint_recv_timeout_seconds_gets_ignored(fake_socket_pair, recv_timeout): (3.0005, 3000), (3.456, 3456), (1, 1000), - ( - -1e-15, - ValueError("Timeout must be a positive number or 0") - ), ( "foo", ValueError("Timeout must be specified as a number of seconds") diff --git a/tests/unit/io/test_class_bolt4x3.py b/tests/unit/io/test_class_bolt4x3.py index 75b52cef8..18273f569 100644 --- a/tests/unit/io/test_class_bolt4x3.py +++ b/tests/unit/io/test_class_bolt4x3.py @@ -262,10 +262,6 @@ def test_hint_recv_timeout_seconds(fake_socket_pair, hints, valid, (3.0005, 3000), (3.456, 3456), (1, 1000), - ( - -1e-15, - ValueError("Timeout must be a positive number or 0") - ), ( "foo", ValueError("Timeout must be specified as a number of seconds") diff --git a/tests/unit/io/test_class_bolt4x4.py b/tests/unit/io/test_class_bolt4x4.py index 9e1b2fe39..dfa19ad2a 100644 --- a/tests/unit/io/test_class_bolt4x4.py +++ b/tests/unit/io/test_class_bolt4x4.py @@ -274,10 +274,6 @@ def test_hint_recv_timeout_seconds(fake_socket_pair, hints, valid, (3.0005, 3000), (3.456, 3456), (1, 1000), - ( - -1e-15, - ValueError("Timeout must be a positive number or 0") - ), ( "foo", ValueError("Timeout must be specified as a number of seconds")