Avoid aiodns resolver memory leak (#5941)

* Avoid aiodns resolver memory leak

In certain cases, the aiodns resolver can leak memory. This also
leads to Fatal `Python error… ffi.from_handle()`. This addresses
the issue by ensuring that the resolver is properly closed
when it is no longer needed.

* Address coderabbitai feedback

* Fix pytest

* Fix pytest
This commit is contained in:
Stefan Agner 2025-06-12 11:32:53 +02:00 committed by GitHub
parent d5b5a328d7
commit bdbd09733a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 26 additions and 23 deletions

View File

@ -15,6 +15,24 @@ from ..const import DNS_CHECK_HOST, ContextType, IssueType
from .base import CheckBase
async def check_server(
loop: asyncio.AbstractEventLoop, server: str, qtype: str
) -> None:
"""Check a DNS server and report issues."""
ip_addr = server[6:] if server.startswith("dns://") else server
resolver = DNSResolver(loop=loop, nameservers=[ip_addr])
try:
await resolver.query(DNS_CHECK_HOST, qtype)
finally:
def _delete_resolver():
"""Close resolver to avoid memory leaks."""
nonlocal resolver
del resolver
loop.call_later(1, _delete_resolver)
def setup(coresys: CoreSys) -> CheckBase:
"""Check setup function."""
return CheckDNSServer(coresys)
@ -33,7 +51,7 @@ class CheckDNSServer(CheckBase):
"""Run check if not affected by issue."""
dns_servers = self.dns_servers
results = await asyncio.gather(
*[self._check_server(server) for server in dns_servers],
*[check_server(self.sys_loop, server, "A") for server in dns_servers],
return_exceptions=True,
)
for i in (r for r in range(len(results)) if isinstance(results[r], DNSError)):
@ -51,18 +69,12 @@ class CheckDNSServer(CheckBase):
return False
try:
await self._check_server(reference)
await check_server(self.sys_loop, reference, "A")
except DNSError:
return True
return False
async def _check_server(self, server: str):
"""Check a DNS server and report issues."""
ip_addr = server[6:] if server.startswith("dns://") else server
resolver = DNSResolver(nameservers=[ip_addr])
await resolver.query(DNS_CHECK_HOST, "A")
@property
def dns_servers(self) -> list[str]:
"""All user and system provided dns servers."""

View File

@ -3,15 +3,16 @@
import asyncio
from datetime import timedelta
from aiodns import DNSResolver
from aiodns.error import DNSError
from supervisor.resolution.checks.dns_server import check_server
from ...const import CoreState
from ...coresys import CoreSys
from ...jobs.const import JobCondition, JobExecutionLimit
from ...jobs.decorator import Job
from ...utils.sentry import async_capture_exception
from ..const import DNS_CHECK_HOST, DNS_ERROR_NO_DATA, ContextType, IssueType
from ..const import DNS_ERROR_NO_DATA, ContextType, IssueType
from .base import CheckBase
@ -33,7 +34,7 @@ class CheckDNSServerIPv6(CheckBase):
"""Run check if not affected by issue."""
dns_servers = self.dns_servers
results = await asyncio.gather(
*[self._check_server(server) for server in dns_servers],
*[check_server(self.sys_loop, server, "AAAA") for server in dns_servers],
return_exceptions=True,
)
for i in (
@ -58,19 +59,13 @@ class CheckDNSServerIPv6(CheckBase):
return False
try:
await self._check_server(reference)
await check_server(self.sys_loop, reference, "AAAA")
except DNSError as dns_error:
if dns_error.args[0] != DNS_ERROR_NO_DATA:
return True
return False
async def _check_server(self, server: str):
"""Check a DNS server and report issues."""
ip_addr = server[6:] if server.startswith("dns://") else server
resolver = DNSResolver(nameservers=[ip_addr])
await resolver.query(DNS_CHECK_HOST, "AAAA")
@property
def dns_servers(self) -> list[str]:
"""All user and system provided dns servers."""

View File

@ -21,10 +21,6 @@ def fixture_mock_dns_query():
"supervisor.resolution.checks.dns_server.DNSResolver.query",
new_callable=AsyncMock,
),
patch(
"supervisor.resolution.checks.dns_server_ipv6.DNSResolver.query",
new_callable=AsyncMock,
),
):
yield

View File

@ -15,7 +15,7 @@ from supervisor.resolution.const import ContextType, IssueType
async def fixture_dns_query() -> AsyncMock:
"""Mock aiodns query."""
with patch(
"supervisor.resolution.checks.dns_server_ipv6.DNSResolver.query",
"supervisor.resolution.checks.dns_server.DNSResolver.query",
new_callable=AsyncMock,
) as dns_query:
yield dns_query