Skip to content

Fix handling of sub-ms transaction timeouts #940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/neo4j/_async/io/_bolt.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,3 +947,32 @@ def is_idle_for(self, timeout):


AsyncBoltSocket.Bolt = AsyncBolt # type: ignore


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
15 changes: 3 additions & 12 deletions src/neo4j/_async/io/_bolt3.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from ._bolt import (
AsyncBolt,
ServerStateManagerBase,
tx_timeout_as_ms,
)
from ._common import (
check_supported_server_product,
Expand Down Expand Up @@ -262,12 +263,7 @@ def run(self, query, parameters=None, mode=None, bookmarks=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be specified as a number of seconds")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a positive number or 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)))
self._append(b"\x10", fields,
Expand Down Expand Up @@ -327,12 +323,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be specified as a number of seconds")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a positive number or 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", hydration_hooks, **handlers),
Expand Down
29 changes: 5 additions & 24 deletions src/neo4j/_async/io/_bolt4.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from ._bolt import (
AsyncBolt,
ServerStateManagerBase,
tx_timeout_as_ms,
)
from ._bolt3 import (
ServerStateManager,
Expand Down Expand Up @@ -212,12 +213,7 @@ def run(self, query, parameters=None, mode=None, bookmarks=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be specified as a number of seconds")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a positive number or 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)))
self._append(b"\x10", fields,
Expand Down Expand Up @@ -276,12 +272,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be specified as a number of seconds")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a positive number or 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", hydration_hooks, **handlers),
Expand Down Expand Up @@ -555,12 +546,7 @@ def run(self, query, parameters=None, mode=None, bookmarks=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be specified as a number of seconds")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a positive number or 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)))
Expand Down Expand Up @@ -596,12 +582,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be specified as a number of seconds")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a positive number or 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", hydration_hooks, **handlers),
Expand Down
29 changes: 5 additions & 24 deletions src/neo4j/_async/io/_bolt5.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from ._bolt import (
AsyncBolt,
ServerStateManagerBase,
tx_timeout_as_ms,
)
from ._bolt3 import (
ServerStateManager,
Expand Down Expand Up @@ -209,12 +210,7 @@ def run(self, query, parameters=None, mode=None, bookmarks=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be a number (in seconds)")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a number >= 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)))
Expand Down Expand Up @@ -270,12 +266,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be a number (in seconds)")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a number >= 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", hydration_hooks, **handlers),
Expand Down Expand Up @@ -567,12 +558,7 @@ def run(self, query, parameters=None, mode=None, bookmarks=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be a number (in seconds)")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a number >= 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)))
Expand Down Expand Up @@ -603,12 +589,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be a number (in seconds)")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a number >= 0")
extra["tx_timeout"] = tx_timeout_as_ms(timeout)
if notifications_min_severity is not None:
extra["notifications_minimum_severity"] = \
notifications_min_severity
Expand Down
29 changes: 29 additions & 0 deletions src/neo4j/_sync/io/_bolt.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,3 +947,32 @@ def is_idle_for(self, timeout):


BoltSocket.Bolt = Bolt # type: ignore


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
15 changes: 3 additions & 12 deletions src/neo4j/_sync/io/_bolt3.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from ._bolt import (
Bolt,
ServerStateManagerBase,
tx_timeout_as_ms,
)
from ._common import (
check_supported_server_product,
Expand Down Expand Up @@ -262,12 +263,7 @@ def run(self, query, parameters=None, mode=None, bookmarks=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be specified as a number of seconds")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a positive number or 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)))
self._append(b"\x10", fields,
Expand Down Expand Up @@ -327,12 +323,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be specified as a number of seconds")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a positive number or 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", hydration_hooks, **handlers),
Expand Down
29 changes: 5 additions & 24 deletions src/neo4j/_sync/io/_bolt4.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from ._bolt import (
Bolt,
ServerStateManagerBase,
tx_timeout_as_ms,
)
from ._bolt3 import (
ServerStateManager,
Expand Down Expand Up @@ -212,12 +213,7 @@ def run(self, query, parameters=None, mode=None, bookmarks=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be specified as a number of seconds")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a positive number or 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)))
self._append(b"\x10", fields,
Expand Down Expand Up @@ -276,12 +272,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be specified as a number of seconds")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a positive number or 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", hydration_hooks, **handlers),
Expand Down Expand Up @@ -555,12 +546,7 @@ def run(self, query, parameters=None, mode=None, bookmarks=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be specified as a number of seconds")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a positive number or 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)))
Expand Down Expand Up @@ -596,12 +582,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be specified as a number of seconds")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a positive number or 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", hydration_hooks, **handlers),
Expand Down
29 changes: 5 additions & 24 deletions src/neo4j/_sync/io/_bolt5.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from ._bolt import (
Bolt,
ServerStateManagerBase,
tx_timeout_as_ms,
)
from ._bolt3 import (
ServerStateManager,
Expand Down Expand Up @@ -209,12 +210,7 @@ def run(self, query, parameters=None, mode=None, bookmarks=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be a number (in seconds)")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a number >= 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)))
Expand Down Expand Up @@ -270,12 +266,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be a number (in seconds)")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a number >= 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", hydration_hooks, **handlers),
Expand Down Expand Up @@ -567,12 +558,7 @@ def run(self, query, parameters=None, mode=None, bookmarks=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be a number (in seconds)")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a number >= 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)))
Expand Down Expand Up @@ -603,12 +589,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
except TypeError:
raise TypeError("Metadata must be coercible to a dict")
if timeout is not None:
try:
extra["tx_timeout"] = int(1000 * float(timeout))
except TypeError:
raise TypeError("Timeout must be a number (in seconds)")
if extra["tx_timeout"] < 0:
raise ValueError("Timeout must be a number >= 0")
extra["tx_timeout"] = tx_timeout_as_ms(timeout)
if notifications_min_severity is not None:
extra["notifications_minimum_severity"] = \
notifications_min_severity
Expand Down
Loading