Skip to content

Commit e81fbd2

Browse files
authored
Support configuration hint connection.recv_timeout_seconds (#557)
* Add deprecation to API docs as well * Add support for configuration hint connection.recv_timeout_seconds
1 parent 9d77e8d commit e81fbd2

10 files changed

+142
-3
lines changed

neo4j/api.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ def version_info(self):
219219
220220
:return: Server Version or None
221221
:rtype: tuple
222+
223+
.. deprecated:: 4.3
224+
`version_info` will be removed in version 5.0. Use
225+
:meth:`~ServerInfo.agent`, :meth:`~ServerInfo.protocol_version`,
226+
or call the `dbms.components` procedure instead.
222227
"""
223228
if not self.agent:
224229
return None

neo4j/io/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ def __init__(self, unresolved_address, sock, max_connection_lifetime, *, auth=No
141141
self.unresolved_address = unresolved_address
142142
self.socket = sock
143143
self.server_info = ServerInfo(Address(sock.getpeername()), self.PROTOCOL_VERSION)
144+
# so far `connection.recv_timeout_seconds` is the only available
145+
# configuration hint that exists. Therefore, all hints can be stored at
146+
# connection level. This might change in the future.
147+
self.configuration_hints = {}
144148
self.outbox = Outbox()
145149
self.inbox = Inbox(self.socket, on_error=self._set_defunct_read)
146150
self.packer = Packer(self.outbox)

neo4j/io/_bolt4.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,30 @@ def fail(md):
376376
self.send_all()
377377
self.fetch_all()
378378
return [metadata.get("rt")]
379+
380+
def hello(self):
381+
def on_success(metadata):
382+
self.configuration_hints.update(metadata.pop("hints", {}))
383+
self.server_info.update(metadata)
384+
recv_timeout = self.configuration_hints.get(
385+
"connection.recv_timeout_seconds"
386+
)
387+
if isinstance(recv_timeout, int) and recv_timeout > 0:
388+
self.socket.settimeout(recv_timeout)
389+
else:
390+
log.info("[#%04X] Server supplied an invalid value for "
391+
"connection.recv_timeout_seconds (%r). Make sure the "
392+
"server and network is set up correctly.",
393+
self.local_port, recv_timeout)
394+
395+
headers = self.get_base_headers()
396+
headers.update(self.auth_dict)
397+
logged_headers = dict(headers)
398+
if "credentials" in logged_headers:
399+
logged_headers["credentials"] = "*******"
400+
log.debug("[#%04X] C: HELLO %r", self.local_port, logged_headers)
401+
self._append(b"\x01", (headers,),
402+
response=InitResponse(self, on_success=on_success))
403+
self.send_all()
404+
self.fetch_all()
405+
check_supported_server_product(self.server_info.agent)

neo4j/io/_common.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
# limitations under the License.
2020

2121

22+
import socket
2223
from struct import pack as struct_pack
2324

2425
from neo4j.exceptions import (
@@ -67,7 +68,7 @@ def _yield_messages(self, sock):
6768
# Reset for new message
6869
unpacker.reset()
6970

70-
except OSError as error:
71+
except (OSError, socket.timeout) as error:
7172
self.on_error(error)
7273

7374
def pop(self):

testkitbackend/test_config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"Optimization:PullPipelining": true,
3636
"Temporary:ResultKeys": true,
3737
"Temporary:FullSummary": true,
38-
"Temporary:CypherPathAndRelationship": true
38+
"Temporary:CypherPathAndRelationship": true,
39+
"ConfHint:connection.recv_timeout_seconds": true
3940
}
4041
}

tests/unit/io/test_class_bolt3.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# See the License for the specific language governing permissions and
1919
# limitations under the License.
2020

21+
from unittest.mock import MagicMock
2122

2223
import pytest
2324

@@ -94,3 +95,18 @@ def test_simple_pull(fake_socket):
9495
tag, fields = socket.pop_message()
9596
assert tag == b"\x3F"
9697
assert len(fields) == 0
98+
99+
100+
@pytest.mark.parametrize("recv_timeout", (1, -1))
101+
def test_hint_recv_timeout_seconds_gets_ignored(fake_socket_pair, recv_timeout):
102+
address = ("127.0.0.1", 7687)
103+
sockets = fake_socket_pair(address)
104+
sockets.client.settimeout = MagicMock()
105+
sockets.server.send_message(0x70, {
106+
"server": "Neo4j/3.5.0",
107+
"hints": {"connection.recv_timeout_seconds": recv_timeout},
108+
})
109+
connection = Bolt3(address, sockets.client,
110+
PoolConfig.max_connection_lifetime)
111+
connection.hello()
112+
sockets.client.settimeout.assert_not_called()

tests/unit/io/test_class_bolt4x0.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# See the License for the specific language governing permissions and
1919
# limitations under the License.
2020

21+
from unittest.mock import MagicMock
2122

2223
import pytest
2324

@@ -181,3 +182,18 @@ def test_n_and_qid_extras_in_pull(fake_socket):
181182
assert tag == b"\x3F"
182183
assert len(fields) == 1
183184
assert fields[0] == {"n": 666, "qid": 777}
185+
186+
187+
@pytest.mark.parametrize("recv_timeout", (1, -1))
188+
def test_hint_recv_timeout_seconds_gets_ignored(fake_socket_pair, recv_timeout):
189+
address = ("127.0.0.1", 7687)
190+
sockets = fake_socket_pair(address)
191+
sockets.client.settimeout = MagicMock()
192+
sockets.server.send_message(0x70, {
193+
"server": "Neo4j/4.0.0",
194+
"hints": {"connection.recv_timeout_seconds": recv_timeout},
195+
})
196+
connection = Bolt4x0(address, sockets.client,
197+
PoolConfig.max_connection_lifetime)
198+
connection.hello()
199+
sockets.client.settimeout.assert_not_called()

tests/unit/io/test_class_bolt4x1.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# See the License for the specific language governing permissions and
1919
# limitations under the License.
2020

21+
from unittest.mock import MagicMock
2122

2223
import pytest
2324

@@ -194,3 +195,18 @@ def test_hello_passes_routing_metadata(fake_socket_pair):
194195
assert tag == 0x01
195196
assert len(fields) == 1
196197
assert fields[0]["routing"] == {"foo": "bar"}
198+
199+
200+
@pytest.mark.parametrize("recv_timeout", (1, -1))
201+
def test_hint_recv_timeout_seconds_gets_ignored(fake_socket_pair, recv_timeout):
202+
address = ("127.0.0.1", 7687)
203+
sockets = fake_socket_pair(address)
204+
sockets.client.settimeout = MagicMock()
205+
sockets.server.send_message(0x70, {
206+
"server": "Neo4j/4.1.0",
207+
"hints": {"connection.recv_timeout_seconds": recv_timeout},
208+
})
209+
connection = Bolt4x1(address, sockets.client,
210+
PoolConfig.max_connection_lifetime)
211+
connection.hello()
212+
sockets.client.settimeout.assert_not_called()

tests/unit/io/test_class_bolt4x2.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
# limitations under the License.
2020

2121

22+
from unittest.mock import MagicMock
23+
2224
import pytest
2325

2426
from neo4j.io._bolt4 import Bolt4x2
@@ -194,3 +196,18 @@ def test_hello_passes_routing_metadata(fake_socket_pair):
194196
assert tag == 0x01
195197
assert len(fields) == 1
196198
assert fields[0]["routing"] == {"foo": "bar"}
199+
200+
201+
@pytest.mark.parametrize("recv_timeout", (1, -1))
202+
def test_hint_recv_timeout_seconds_gets_ignored(fake_socket_pair, recv_timeout):
203+
address = ("127.0.0.1", 7687)
204+
sockets = fake_socket_pair(address)
205+
sockets.client.settimeout = MagicMock()
206+
sockets.server.send_message(0x70, {
207+
"server": "Neo4j/4.2.0",
208+
"hints": {"connection.recv_timeout_seconds": recv_timeout},
209+
})
210+
connection = Bolt4x2(address, sockets.client,
211+
PoolConfig.max_connection_lifetime)
212+
connection.hello()
213+
sockets.client.settimeout.assert_not_called()

tests/unit/io/test_class_bolt4x3.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
# See the License for the specific language governing permissions and
1919
# limitations under the License.
2020

21+
import logging
22+
from unittest.mock import MagicMock
2123

2224
import pytest
2325

@@ -186,7 +188,7 @@ def test_n_and_qid_extras_in_pull(fake_socket):
186188
def test_hello_passes_routing_metadata(fake_socket_pair):
187189
address = ("127.0.0.1", 7687)
188190
sockets = fake_socket_pair(address)
189-
sockets.server.send_message(0x70, {"server": "Neo4j/4.2.0"})
191+
sockets.server.send_message(0x70, {"server": "Neo4j/4.3.0"})
190192
connection = Bolt4x3(address, sockets.client,
191193
PoolConfig.max_connection_lifetime,
192194
routing_context={"foo": "bar"})
@@ -195,3 +197,37 @@ def test_hello_passes_routing_metadata(fake_socket_pair):
195197
assert tag == 0x01
196198
assert len(fields) == 1
197199
assert fields[0]["routing"] == {"foo": "bar"}
200+
201+
202+
@pytest.mark.parametrize(("recv_timeout", "valid"), (
203+
(1, True),
204+
(42, True),
205+
(-1, False),
206+
(0, False),
207+
(2.5, False),
208+
(None, False),
209+
("1", False),
210+
))
211+
def test_hint_recv_timeout_seconds(fake_socket_pair, recv_timeout, valid,
212+
caplog):
213+
address = ("127.0.0.1", 7687)
214+
sockets = fake_socket_pair(address)
215+
sockets.client.settimeout = MagicMock()
216+
sockets.server.send_message(0x70, {
217+
"server": "Neo4j/4.2.0",
218+
"hints": {"connection.recv_timeout_seconds": recv_timeout},
219+
})
220+
connection = Bolt4x3(address, sockets.client,
221+
PoolConfig.max_connection_lifetime)
222+
with caplog.at_level(logging.INFO):
223+
connection.hello()
224+
invalid_value_logged = any(repr(recv_timeout) in msg
225+
and "recv_timeout_seconds" in msg
226+
and "invalid" in msg
227+
for msg in caplog.messages)
228+
if valid:
229+
sockets.client.settimeout.assert_called_once_with(recv_timeout)
230+
assert not invalid_value_logged
231+
else:
232+
sockets.client.settimeout.assert_not_called()
233+
assert invalid_value_logged

0 commit comments

Comments
 (0)