Skip to content

Commit eddc877

Browse files
committed
Fix socket timeouts
## Fix connection hint timeout The driver may receive a configuration hint from the server during connection setup. The hint is a timeout: if the driver is waiting for a response from the server but has not heard back within the timeout window, the driver shall assume the connection is dead. The server can send `NOOP` ping messages to reset the timer ticking on the client-side. The driver applied this hint to both reads and writes. This PR changes it so that the hint, as intended, only applies to read operations on connections. ## New write timeout configuraiton option To avoid the driver hanging indefinitely on write operations, a new driver-level configuration option `connection_write_timeout`, which defaults to `30 seconds` is introduced. As the name suggests, this timeout is applied to all write operations on connections.
1 parent 134be3e commit eddc877

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+587
-237
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ See also https://github.com/neo4j/neo4j-python-driver/wiki for a full changelog.
190190
- On failed liveness check (s. `liveness_check_timeout` configuration option), the driver will no longer remove the
191191
remote from the cached routing tables, but only close the connection under test.
192192
This aligns the driver with the other official Neo4j drivers.
193+
- The driver incorrectly applied a timeout hint received from the server to both read and write I/O operations.
194+
It is now only applied to read I/O operations.
195+
In turn, a new configuration option `connection_write_timeout` with a default value of `30 seconds` is introduced.
193196

194197

195198
## Version 5.28

docs/source/api.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ Additional configuration can be provided via the :class:`neo4j.Driver` construct
399399

400400
+ :ref:`connection-acquisition-timeout-ref`
401401
+ :ref:`connection-timeout-ref`
402+
+ :ref:`connection-write-timeout-ref`
402403
+ :ref:`encrypted-ref`
403404
+ :ref:`keep-alive-ref`
404405
+ :ref:`max-connection-lifetime-ref`
@@ -430,7 +431,7 @@ it should be chosen larger than :ref:`connection-timeout-ref`.
430431
:Type: ``float``
431432
:Default: ``60.0``
432433

433-
.. versionadded:: 6.0
434+
.. versionchanged:: 6.0
434435
The setting now entails *anything* required to acquire a connection.
435436
This includes potential fetching of routing tables which in itself requires acquiring a connection.
436437
Previously, the timeout would be restarted for such auxiliary connection acquisitions.
@@ -450,6 +451,16 @@ connection can be used to perform database related work.
450451
:Default: ``30.0``
451452

452453

454+
.. _connection-write-timeout-ref:
455+
456+
``connection_write_timeout``
457+
----------------------------
458+
The maximum amount of time in seconds to wait for TCP write operations to complete.
459+
460+
:Type: ``float``
461+
:Default: ``30.0``
462+
463+
453464
.. _encrypted-ref:
454465

455466
``encrypted``

src/neo4j/_async/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ class AsyncPoolConfig(Config):
5454
# The maximum amount of time to wait for a TCP connection to be
5555
# established.
5656

57+
#: Connection Write Timeout
58+
connection_write_timeout = 30.0 # seconds
59+
# The maximum amount of time to wait for I/O write operations to complete.
60+
5761
#: Custom Resolver
5862
resolver = None
5963
# Custom resolver function, returning list of resolved addresses.

src/neo4j/_async/driver.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ def driver(
127127
liveness_check_timeout: float | None = ...,
128128
max_connection_pool_size: int = ...,
129129
connection_timeout: float = ...,
130+
connection_write_timeout: float = ...,
130131
resolver: (
131132
t.Callable[[Address], t.Iterable[Address]]
132133
| t.Callable[[Address], t.Awaitable[t.Iterable[Address]]]

src/neo4j/_async/io/_bolt.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,11 +418,13 @@ async def open(
418418
)
419419

420420
try:
421-
connection.socket.set_deadline(deadline)
421+
connection.socket.set_read_deadline(deadline)
422+
connection.socket.set_write_deadline(deadline)
422423
try:
423424
await connection.hello()
424425
finally:
425-
connection.socket.set_deadline(None)
426+
connection.socket.set_read_deadline(None)
427+
connection.socket.set_write_deadline(None)
426428
except (
427429
Exception,
428430
# Python 3.8+: CancelledError is a subclass of BaseException

src/neo4j/_async/io/_bolt4.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,7 @@ def on_success(metadata):
626626
"connection.recv_timeout_seconds"
627627
]
628628
if isinstance(recv_timeout, int) and recv_timeout > 0:
629-
self.socket.settimeout(recv_timeout)
629+
self.socket.set_read_timeout(recv_timeout)
630630
else:
631631
log.info(
632632
"[#%04X] _: <CONNECTION> Server supplied an "

src/neo4j/_async/io/_bolt5.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def on_success(metadata):
149149
"connection.recv_timeout_seconds"
150150
]
151151
if isinstance(recv_timeout, int) and recv_timeout > 0:
152-
self.socket.settimeout(recv_timeout)
152+
self.socket.set_read_timeout(recv_timeout)
153153
else:
154154
log.info(
155155
"[#%04X] _: <CONNECTION> Server supplied an "
@@ -622,7 +622,7 @@ def on_success(metadata):
622622
"connection.recv_timeout_seconds"
623623
]
624624
if isinstance(recv_timeout, int) and recv_timeout > 0:
625-
self.socket.settimeout(recv_timeout)
625+
self.socket.set_read_timeout(recv_timeout)
626626
else:
627627
log.info(
628628
"[#%04X] _: <CONNECTION> Server supplied an "
@@ -708,7 +708,7 @@ def on_success(metadata):
708708
"connection.recv_timeout_seconds"
709709
]
710710
if isinstance(recv_timeout, int) and recv_timeout > 0:
711-
self.socket.settimeout(recv_timeout)
711+
self.socket.set_read_timeout(recv_timeout)
712712
else:
713713
log.info(
714714
"[#%04X] _: <CONNECTION> Server supplied an "

src/neo4j/_async/io/_bolt6.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def on_success(metadata):
158158
"connection.recv_timeout_seconds"
159159
]
160160
if isinstance(recv_timeout, int) and recv_timeout > 0:
161-
self.socket.settimeout(recv_timeout)
161+
self.socket.set_read_timeout(recv_timeout)
162162
else:
163163
log.info(
164164
"[#%04X] _: <CONNECTION> Server supplied an "

src/neo4j/_async/io/_bolt_socket.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@
3232
BoltError,
3333
BoltProtocolError,
3434
)
35-
from ..._io import BoltProtocolVersion
35+
from ..._io import (
36+
BoltProtocolVersion,
37+
min_timeout,
38+
)
3639
from ...exceptions import (
3740
DriverError,
3841
ServiceUnavailable,
@@ -157,8 +160,8 @@ def _encode_varint(n: int) -> bytearray:
157160
return res
158161

159162
async def _handshake_read(self, ctx: HandshakeCtx, n: int) -> bytes:
160-
original_timeout = self.gettimeout()
161-
self.settimeout(ctx.deadline.to_timeout())
163+
original_timeout = self.get_read_timeout()
164+
self.set_read_timeout(ctx.deadline.to_timeout())
162165
try:
163166
response = await self.recv(n)
164167
ctx.full_response.extend(response)
@@ -168,7 +171,7 @@ async def _handshake_read(self, ctx: HandshakeCtx, n: int) -> bytes:
168171
f"{ctx.resolved_address!r} (deadline {ctx.deadline})"
169172
) from exc
170173
finally:
171-
self.settimeout(original_timeout)
174+
self.set_read_timeout(original_timeout)
172175
data_size = len(response)
173176
if data_size == 0:
174177
# If no data is returned after a successful select
@@ -192,9 +195,11 @@ async def _handshake_read(self, ctx: HandshakeCtx, n: int) -> bytes:
192195

193196
return response
194197

195-
async def _handshake_send(self, ctx, data):
196-
original_timeout = self.gettimeout()
197-
self.settimeout(ctx.deadline.to_timeout())
198+
async def _handshake_send(self, ctx, data, write_timeout=None):
199+
original_timeout = self.get_write_timeout()
200+
self.set_write_timeout(
201+
min_timeout(ctx.deadline.to_timeout(), write_timeout)
202+
)
198203
try:
199204
await self.sendall(data)
200205
except OSError as exc:
@@ -203,7 +208,7 @@ async def _handshake_send(self, ctx, data):
203208
f"{ctx.resolved_address!r} (deadline {ctx.deadline})"
204209
) from exc
205210
finally:
206-
self.settimeout(original_timeout)
211+
self.set_write_timeout(original_timeout)
207212

208213
async def _handshake(
209214
self,

0 commit comments

Comments
 (0)