Skip to content

Commit 23b2a80

Browse files
authored
Fix connection_acquisition_timeout setting (#675)
The timeout was not respected should the server successfully exchange the version handshake with the driver but then exceed the timeout by taking too long to reply to `HELLO`.
1 parent bca8e9c commit 23b2a80

File tree

11 files changed

+312
-161
lines changed

11 files changed

+312
-161
lines changed

neo4j/_async/io/_bolt.py

Lines changed: 97 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@
2323
from time import perf_counter
2424

2525
from ..._async_compat.network import AsyncBoltSocket
26-
from ..._exceptions import BoltHandshakeError
26+
from ..._async_compat.util import AsyncUtil
27+
from ..._exceptions import (
28+
BoltError,
29+
BoltHandshakeError,
30+
SocketDeadlineExceeded,
31+
)
2732
from ...addressing import Address
2833
from ...api import (
2934
ServerInfo,
@@ -32,6 +37,7 @@
3237
from ...conf import PoolConfig
3338
from ...exceptions import (
3439
AuthError,
40+
DriverError,
3541
IncompleteCommit,
3642
ServiceUnavailable,
3743
SessionExpired,
@@ -76,6 +82,7 @@ class AsyncBolt:
7682
idle_since = float("-inf")
7783

7884
# The socket
85+
_closing = False
7986
_closed = False
8087

8188
# The socket
@@ -260,24 +267,42 @@ async def ping(cls, address, *, timeout=None, **config):
260267

261268
@classmethod
262269
async def open(
263-
cls, address, *, auth=None, timeout=None, routing_context=None, **pool_config
270+
cls, address, *, auth=None, timeout=None, routing_context=None,
271+
**pool_config
264272
):
265-
""" Open a new Bolt connection to a given server address.
273+
"""Open a new Bolt connection to a given server address.
266274
267275
:param address:
268276
:param auth:
269277
:param timeout: the connection timeout in seconds
270278
:param routing_context: dict containing routing context
271279
:param pool_config:
272-
:return:
273-
:raise BoltHandshakeError: raised if the Bolt Protocol can not negotiate a protocol version.
280+
281+
:return: connected AsyncBolt instance
282+
283+
:raise BoltHandshakeError:
284+
raised if the Bolt Protocol can not negotiate a protocol version.
274285
:raise ServiceUnavailable: raised if there was a connection issue.
275286
"""
287+
def time_remaining():
288+
if timeout is None:
289+
return None
290+
t = timeout - (perf_counter() - t0)
291+
return t if t > 0 else 0
292+
293+
t0 = perf_counter()
276294
pool_config = PoolConfig.consume(pool_config)
295+
296+
socket_connection_timeout = pool_config.connection_timeout
297+
if socket_connection_timeout is None:
298+
socket_connection_timeout = time_remaining()
299+
elif timeout is not None:
300+
socket_connection_timeout = min(pool_config.connection_timeout,
301+
time_remaining())
277302
s, pool_config.protocol_version, handshake, data = \
278303
await AsyncBoltSocket.connect(
279304
address,
280-
timeout=timeout,
305+
timeout=socket_connection_timeout,
281306
custom_resolver=pool_config.resolver,
282307
ssl_context=pool_config.get_ssl_context(),
283308
keep_alive=pool_config.keep_alive,
@@ -308,17 +333,31 @@ async def open(
308333
AsyncBoltSocket.close_socket(s)
309334

310335
supported_versions = cls.protocol_handlers().keys()
311-
raise BoltHandshakeError("The Neo4J server does not support communication with this driver. This driver have support for Bolt Protocols {}".format(supported_versions), address=address, request_data=handshake, response_data=data)
336+
raise BoltHandshakeError(
337+
"The Neo4J server does not support communication with this "
338+
"driver. This driver have support for Bolt Protocols {}"
339+
"".format(supported_versions),
340+
address=address, request_data=handshake, response_data=data
341+
)
312342

313343
connection = bolt_cls(
314344
address, s, pool_config.max_connection_lifetime, auth=auth,
315345
user_agent=pool_config.user_agent, routing_context=routing_context
316346
)
317347

318348
try:
319-
await connection.hello()
349+
connection.socket.set_deadline(time_remaining())
350+
try:
351+
await connection.hello()
352+
except SocketDeadlineExceeded as e:
353+
# connection._defunct = True
354+
raise ServiceUnavailable(
355+
"Timeout during initial handshake occurred"
356+
) from e
357+
finally:
358+
connection.socket.set_deadline(None)
320359
except Exception:
321-
await connection.close()
360+
await connection.close_non_blocking()
322361
raise
323362

324363
return connection
@@ -440,6 +479,11 @@ async def reset(self):
440479
"""
441480
pass
442481

482+
@abc.abstractmethod
483+
def goodbye(self):
484+
"""Append a GOODBYE message to the outgoing queued."""
485+
pass
486+
443487
def _append(self, signature, fields=(), response=None):
444488
""" Appends a message to the outgoing queue.
445489
@@ -481,7 +525,8 @@ async def send_all(self):
481525
await self._send_all()
482526

483527
@abc.abstractmethod
484-
async def _fetch_message(self):
528+
async def _process_message(self, details, summary_signature,
529+
summary_metadata):
485530
""" Receive at most one message from the server, if available.
486531
487532
:return: 2-tuple of number of detail messages and number of summary
@@ -505,7 +550,12 @@ async def fetch_message(self):
505550
if not self.responses:
506551
return 0, 0
507552

508-
res = await self._fetch_message()
553+
# Receive exactly one message
554+
details, summary_signature, summary_metadata = \
555+
await AsyncUtil.next(self.inbox)
556+
res = await self._process_message(
557+
details, summary_signature, summary_metadata
558+
)
509559
self.idle_since = perf_counter()
510560
return res
511561

@@ -548,9 +598,13 @@ async def _set_defunct(self, message, error=None, silent=False):
548598
# connection from the client side, and remove the address
549599
# from the connection pool.
550600
self._defunct = True
551-
await self.close()
552-
if self.pool:
553-
await self.pool.deactivate(address=self.unresolved_address)
601+
if not self._closing:
602+
# If we fail while closing the connection, there is no need to
603+
# remove the connection from the pool, nor to try to close the
604+
# connection again.
605+
await self.close()
606+
if self.pool:
607+
await self.pool.deactivate(address=self.unresolved_address)
554608
# Iterate through the outstanding responses, and if any correspond
555609
# to COMMIT requests then raise an error to signal that we are
556610
# unable to confirm that the COMMIT completed successfully.
@@ -584,11 +638,37 @@ def stale(self):
584638
def set_stale(self):
585639
self._stale = True
586640

587-
@abc.abstractmethod
588641
async def close(self):
589-
""" Close the connection.
642+
"""Close the connection."""
643+
if self._closed or self._closing:
644+
return
645+
self._closing = True
646+
if not self._defunct:
647+
self.goodbye()
648+
try:
649+
await self._send_all()
650+
except (OSError, BoltError, DriverError):
651+
pass
652+
log.debug("[#%04X] C: <CLOSE>", self.local_port)
653+
try:
654+
self.socket.close()
655+
except OSError:
656+
pass
657+
finally:
658+
self._closed = True
659+
660+
async def close_non_blocking(self):
661+
"""Set the socket to non-blocking and close it.
662+
663+
This will try to send the `GOODBYE` message (given the socket is not
664+
marked as defunct). However, should the write operation require
665+
blocking (e.g., a full network buffer), then the socket will be closed
666+
immediately (without `GOODBYE` message).
590667
"""
591-
pass
668+
if self._closed or self._closing:
669+
return
670+
self.socket.settimeout(0)
671+
await self.close()
592672

593673
@abc.abstractmethod
594674
def closed(self):

neo4j/_async/io/_bolt3.py

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -314,16 +314,17 @@ def fail(metadata):
314314
await self.send_all()
315315
await self.fetch_all()
316316

317-
async def _fetch_message(self):
318-
""" Receive at most one message from the server, if available.
317+
def goodbye(self):
318+
log.debug("[#%04X] C: GOODBYE", self.local_port)
319+
self._append(b"\x02", ())
320+
321+
async def _process_message(self, details, summary_signature,
322+
summary_metadata):
323+
""" Process at most one message from the server, if available.
319324
320325
:return: 2-tuple of number of detail messages and number of summary
321326
messages fetched
322327
"""
323-
# Receive exactly one message
324-
details, summary_signature, summary_metadata = \
325-
await AsyncUtil.next(self.inbox)
326-
327328
if details:
328329
log.debug("[#%04X] S: RECORD * %d", self.local_port, len(details)) # Do not log any data
329330
await self.responses[0].on_records(details)
@@ -363,25 +364,6 @@ async def _fetch_message(self):
363364

364365
return len(details), 1
365366

366-
async def close(self):
367-
""" Close the connection.
368-
"""
369-
if not self._closed:
370-
if not self._defunct:
371-
log.debug("[#%04X] C: GOODBYE", self.local_port)
372-
self._append(b"\x02", ())
373-
try:
374-
await self._send_all()
375-
except (OSError, BoltError, DriverError):
376-
pass
377-
log.debug("[#%04X] C: <CLOSE>", self.local_port)
378-
try:
379-
self.socket.close()
380-
except OSError:
381-
pass
382-
finally:
383-
self._closed = True
384-
385367
def closed(self):
386368
return self._closed
387369

neo4j/_async/io/_bolt4.py

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@
2020
from ssl import SSLSocket
2121

2222
from ..._async_compat.util import AsyncUtil
23-
from ..._exceptions import (
24-
BoltError,
25-
BoltProtocolError,
26-
)
23+
from ..._exceptions import BoltProtocolError
2724
from ...api import (
2825
READ_ACCESS,
2926
SYSTEM_DATABASE,
@@ -32,7 +29,6 @@
3229
from ...exceptions import (
3330
ConfigurationError,
3431
DatabaseUnavailable,
35-
DriverError,
3632
ForbiddenOnReadOnlyDatabase,
3733
Neo4jError,
3834
NotALeader,
@@ -265,16 +261,17 @@ def fail(metadata):
265261
await self.send_all()
266262
await self.fetch_all()
267263

268-
async def _fetch_message(self):
269-
""" Receive at most one message from the server, if available.
264+
def goodbye(self):
265+
log.debug("[#%04X] C: GOODBYE", self.local_port)
266+
self._append(b"\x02", ())
267+
268+
async def _process_message(self, details, summary_signature,
269+
summary_metadata):
270+
""" Process at most one message from the server, if available.
270271
271272
:return: 2-tuple of number of detail messages and number of summary
272273
messages fetched
273274
"""
274-
# Receive exactly one message
275-
details, summary_signature, summary_metadata = \
276-
await AsyncUtil.next(self.inbox)
277-
278275
if details:
279276
log.debug("[#%04X] S: RECORD * %d", self.local_port, len(details)) # Do not log any data
280277
await self.responses[0].on_records(details)
@@ -315,25 +312,6 @@ async def _fetch_message(self):
315312

316313
return len(details), 1
317314

318-
async def close(self):
319-
""" Close the connection.
320-
"""
321-
if not self._closed:
322-
if not self._defunct:
323-
log.debug("[#%04X] C: GOODBYE", self.local_port)
324-
self._append(b"\x02", ())
325-
try:
326-
await self._send_all()
327-
except (OSError, BoltError, DriverError):
328-
pass
329-
log.debug("[#%04X] C: <CLOSE>", self.local_port)
330-
try:
331-
self.socket.close()
332-
except OSError:
333-
pass
334-
finally:
335-
self._closed = True
336-
337315
def closed(self):
338316
return self._closed
339317

neo4j/_async/io/_pool.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,14 @@ async def _acquire_from_pool(self, address, health_check):
112112
return connection
113113
return None
114114

115-
async def _acquire_new(self, address, timeout, health_check):
115+
async def _acquire_new(self, address, timeout):
116116
connections = self.connections[address]
117117
max_pool_size = self.pool_config.max_connection_pool_size
118118
infinite_pool_size = (max_pool_size < 0
119119
or max_pool_size == float("inf"))
120120
can_create_new_connection = (infinite_pool_size
121121
or len(connections) < max_pool_size)
122122
if can_create_new_connection:
123-
timeout = min(self.pool_config.connection_timeout, timeout)
124123
try:
125124
connection = await self.opener(address, timeout)
126125
except ServiceUnavailable:
@@ -170,9 +169,7 @@ async def health_check(connection_):
170169
if connection:
171170
return connection
172171
# all connections in pool are in-use
173-
connection = await self._acquire_new(
174-
address, time_remaining(), health_check
175-
)
172+
connection = await self._acquire_new(address, time_remaining())
176173
if connection:
177174
return connection
178175

0 commit comments

Comments
 (0)