Skip to content

Commit adcd4f7

Browse files
robsdedudeMaxAake
andauthored
Make DNS error retryable (#1211)
Configuring the driver with a URL that cannot be DNS resolved will raise a (retryable) `ServiceUnavailable` error instead of a `ValueError`. Co-authored-by: MaxAake <61233757+MaxAake@users.noreply.github.com>
1 parent d6668df commit adcd4f7

File tree

10 files changed

+227
-2
lines changed

10 files changed

+227
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ See also https://github.com/neo4j/neo4j-python-driver/wiki for a full changelog.
165165
- `neo4j.graph.Node`, `neo4j.graph.Relationship`, `neo4j.graph.Path`
166166
- `neo4j.time.Date`, `neo4j.time.Time`, `neo4j.time.DateTime`
167167
- `neo4j.spatial.Point` (and subclasses)
168+
- Configuring the driver with a URL that cannot be DNS resolved will raise a (retryable) `ServiceUnavailable` error
169+
instead of a `ValueError`.
168170
- Separate out log entries that are session-related (including transaction retries)
169171
form sub-logger `neo4j.pool` to a new sub-logger `neo4j.session`.
170172
- Notifications:

src/neo4j/_async_compat/network/_util.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
Address,
2323
ResolvedAddress,
2424
)
25+
from ...exceptions import ServiceUnavailable
2526
from ..util import AsyncUtil
2627

2728

@@ -40,6 +41,14 @@ def _resolved_addresses_from_info(info, host_name):
4041
yield ResolvedAddress(addr, host_name=host_name)
4142

4243

44+
_RETRYABLE_DNS_ERRNOS = {
45+
socket.EAI_ADDRFAMILY,
46+
socket.EAI_AGAIN,
47+
socket.EAI_MEMORY,
48+
socket.EAI_NODATA,
49+
}
50+
51+
4352
class AsyncNetworkUtil:
4453
@staticmethod
4554
async def get_address_info(
@@ -69,7 +78,16 @@ async def _dns_resolver(address, family=0):
6978
type=socket.SOCK_STREAM,
7079
)
7180
except OSError as e:
72-
raise ValueError(f"Cannot resolve address {address}") from e
81+
if e.errno in _RETRYABLE_DNS_ERRNOS or (
82+
e.errno == socket.EAI_NONAME
83+
and (address.host is not None or address.port is not None)
84+
):
85+
raise ServiceUnavailable(
86+
f"Failed to DNS resolve address {address}: {e}"
87+
) from e
88+
raise ValueError(
89+
f"Failed to DNS resolve address {address}: {e}"
90+
) from e
7391
return list(_resolved_addresses_from_info(info, address._host_name))
7492

7593
@staticmethod
@@ -151,7 +169,16 @@ def _dns_resolver(address, family=0):
151169
type=socket.SOCK_STREAM,
152170
)
153171
except OSError as e:
154-
raise ValueError(f"Cannot resolve address {address}") from e
172+
if e.errno in _RETRYABLE_DNS_ERRNOS or (
173+
e.errno == socket.EAI_NONAME
174+
and (address.host is not None or address.port is not None)
175+
):
176+
raise ServiceUnavailable(
177+
f"Failed to DNS resolve address {address}: {e}"
178+
) from e
179+
raise ValueError(
180+
f"Failed to DNS resolve address {address}: {e}"
181+
) from e
155182
return _resolved_addresses_from_info(info, address._host_name)
156183

157184
@staticmethod
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright (c) "Neo4j"
2+
# Neo4j Sweden AB [https://neo4j.com]
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright (c) "Neo4j"
2+
# Neo4j Sweden AB [https://neo4j.com]
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright (c) "Neo4j"
2+
# Neo4j Sweden AB [https://neo4j.com]
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
17+
from __future__ import annotations
18+
19+
import socket
20+
21+
import pytest
22+
23+
from neo4j._addressing import (
24+
ResolvedAddress,
25+
ResolvedIPv4Address,
26+
ResolvedIPv6Address,
27+
)
28+
from neo4j._async_compat.network import AsyncNetworkUtil
29+
from neo4j.addressing import Address
30+
from neo4j.exceptions import ServiceUnavailable
31+
32+
from ....._async_compat import mark_async_test
33+
34+
35+
@mark_async_test
36+
async def test_resolve_address():
37+
resolved = [
38+
addr
39+
async for addr in AsyncNetworkUtil.resolve_address(
40+
Address(("localhost", 1234)),
41+
)
42+
]
43+
assert all(isinstance(addr, ResolvedAddress) for addr in resolved)
44+
for addr in resolved:
45+
if isinstance(addr, ResolvedIPv4Address):
46+
assert len(addr) == 2
47+
assert addr[0].startswith("127.0.0.")
48+
assert addr[1] == 1234
49+
elif isinstance(addr, ResolvedIPv6Address):
50+
assert len(addr) == 4
51+
assert addr[:2] == ("::1", 1234)
52+
53+
54+
@mark_async_test
55+
async def test_resolve_invalid_address():
56+
with pytest.raises(ServiceUnavailable) as exc:
57+
await anext(
58+
AsyncNetworkUtil.resolve_address(
59+
Address(("example.invalid", 1234)),
60+
)
61+
)
62+
cause = exc.value.__cause__
63+
assert isinstance(cause, socket.gaierror)
64+
assert cause.errno, socket.EAI_NONAME

tests/integration/sync/async_compat/__init__.py

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/sync/async_compat/network/__init__.py

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/sync/async_compat/network/test_util.py

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/unit/async_/test_addressing.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
)
2525
from neo4j._async_compat.network import AsyncNetworkUtil
2626
from neo4j._async_compat.util import AsyncUtil
27+
from neo4j.exceptions import ServiceUnavailable
2728

2829
from ..._async_compat import mark_async_test
2930

@@ -53,14 +54,19 @@ async def test_address_resolve_with_custom_resolver_none() -> None:
5354
@pytest.mark.parametrize(
5455
("test_input", "expected"),
5556
[
57+
(Address(("example.invalid", "7687")), ServiceUnavailable),
58+
(Address(("example.invalid", 7687)), ServiceUnavailable),
5659
(Address(("127.0.0.1", "abcd")), ValueError),
5760
(Address((None, None)), ValueError),
61+
(Address((1234, "7687")), TypeError),
5862
],
5963
)
6064
@mark_async_test
6165
async def test_address_resolve_with_unresolvable_address(
6266
test_input, expected
6367
) -> None:
68+
# import contextlib
69+
# with contextlib.suppress(Exception):
6470
with pytest.raises(expected):
6571
await AsyncUtil.list(
6672
AsyncNetworkUtil.resolve_address(test_input, resolver=None)

tests/unit/sync/test_addressing.py

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)