Repository: aio-libs/aiodns
Pull request #151: Fallback to `sock_state_cb` if `event_thread` creation fails
Author: login: bdraco
Created: 2025-05-08T17:01:28Z | Merged: 2025-05-08T19:25:43Z

<!-- Thank you for your contribution! -->

## What do these changes do?

On some systems (see #150), the creation of the `event_thread` can fail when the system runs out of available inotify watches; often because other processes have consumed them all.  
This change adds a fallback to `sock_state_cb` in such scenarios, allowing socket state handling to continue gracefully even when inotify resources are exhausted.

## Are there changes in behavior for the user?

These changes should be mostly transparent to the user. On Windows, the fallback is unlikely to be triggered since we we can't run out of inotify handles.

The main downside is that updates to the resolver configuration won’t be picked up automatically.

## Related issue number

close #150

## Checklist

- [x] I think the code is well written
- [ ] Unit tests for the changes exist
- [ ] Documentation reflects the changes


Linked issue #150: Resolver cannot be created if inotify is broken or out of file descriptors
Looks like inotify runs out of file descriptors and c-ares failed to init the channel
```
94498 epoll_pwait(7,  <unfinished ...>
94497 <... inotify_init1 resumed>)      = -1 EMFILE (No file descriptors available)
94497 write(9, "1", 1)             
```

```
homeassistant:/config# strace -f -s4096 -o cares.log python3 aiodns_test.py 
Traceback (most recent call last):
  File "/config/aiodns_test.py", line 9, in <module>
    asyncio.run(test_cares())
    ~~~~~~~~~~~^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/asyncio/runners.py", line 195, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "/usr/local/lib/python3.13/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/usr/local/lib/python3.13/asyncio/base_events.py", line 719, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "/config/aiodns_test.py", line 6, in test_cares
    resolver = aiodns.DNSResolver()
  File "/usr/local/lib/python3.13/site-packages/aiodns/__init__.py", line 67, in __init__
    self._channel = pycares.Channel(event_thread=True,
                    ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
                                    timeout=timeout,
                                    ^^^^^^^^^^^^^^^^
                                    **kwargs)
                                    ^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/pycares/__init__.py", line 422, in __init__
    raise AresError('Failed to initialize c-ares channel')
pycares.AresError: Failed to initialize c-ares channel
```

originally reported in https://github.com/home-assistant/core/issues/144457

Diff:
diff --git a/aiodns/__init__.py b/aiodns/__init__.py
index e084a18..8d7fca5 100644
--- a/aiodns/__init__.py
+++ b/aiodns/__init__.py
@@ -1,5 +1,7 @@
 import asyncio
 import functools
+import logging
+import pycares
 import socket
 import sys
 from collections.abc import Iterable, Sequence
@@ -30,6 +32,7 @@
     "https://github.com/aio-libs/aiodns#note-for-windows-users"
 )
 
+_LOGGER = logging.getLogger(__name__)
 
 READ = 1
 WRITE = 2
@@ -64,29 +67,50 @@ def __init__(self, nameservers: Optional[Sequence[str]] = None,
         kwargs.pop('sock_state_cb', None)
         timeout = kwargs.pop('timeout', None)
         self._timeout = timeout
-        self._event_thread = hasattr(pycares,"ares_threadsafety") and pycares.ares_threadsafety()
-        if self._event_thread:
-            # pycares is thread safe
-            self._channel = pycares.Channel(event_thread=True,
-                                            timeout=timeout,
-                                            **kwargs)
-        else:
-            if sys.platform == 'win32' and not isinstance(self.loop, asyncio.SelectorEventLoop):
-                try:
-                    import winloop
-                    if not isinstance(self.loop , winloop.Loop):
-                        raise RuntimeError(WINDOWS_SELECTOR_ERR_MSG)
-                except ModuleNotFoundError:
-                    raise RuntimeError(WINDOWS_SELECTOR_ERR_MSG)
-            self._channel = pycares.Channel(sock_state_cb=self._sock_state_cb,
-                                            timeout=timeout,
-                                            **kwargs)
+        self._event_thread, self._channel = self._make_channel(**kwargs)
         if nameservers:
             self.nameservers = nameservers
         self._read_fds: set[int] = set()
         self._write_fds: set[int] = set()
         self._timer: Optional[asyncio.TimerHandle] = None
 
+    def _make_channel(self, **kwargs: Any) -> Tuple[bool, pycares.Channel]:
+        if hasattr(pycares, "ares_threadsafety") and pycares.ares_threadsafety():
+            # pycares is thread safe
+            try:
+                return True, pycares.Channel(
+                    event_thread=True, timeout=self._timeout, **kwargs
+                )
+            except pycares.AresError as e:
+                if sys.platform == "linux":
+                    _LOGGER.warning(
+                        "Failed to create a DNS resolver channel with automatic monitoring of "
+                        "resolver configuration changes, this usually means the system ran "
+                        "out of inotify watches. Falling back to socket state callback. "
+                        "Consider increasing the system inotify watch limit: %s",
+                        e,
+                    )
+                else:
+                    _LOGGER.warning(
+                        "Failed to create a DNS resolver channel with automatic monitoring "
+                        "of resolver configuration changes. Falling back to socket state "
+                        "callback: %s",
+                        e,
+                    )
+        if sys.platform == "win32" and not isinstance(
+            self.loop, asyncio.SelectorEventLoop
+        ):
+            try:
+                import winloop
+
+                if not isinstance(self.loop, winloop.Loop):
+                    raise RuntimeError(WINDOWS_SELECTOR_ERR_MSG)
+            except ModuleNotFoundError as ex:
+                raise RuntimeError(WINDOWS_SELECTOR_ERR_MSG) from ex
+        return False, pycares.Channel(
+            sock_state_cb=self._sock_state_cb, timeout=self._timeout, **kwargs
+        )
+
     @property
     def nameservers(self) -> Sequence[str]:
         return self._channel.servers
diff --git a/pytest.ini b/pytest.ini
index 6ae899f..8594278 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -10,6 +10,8 @@ addopts =
     --showlocals
     # coverage reports
     --cov=aiodns/ --cov=tests/ --cov-report term
+asyncio_default_fixture_loop_scope = function
+asyncio_mode = auto
 filterwarnings =
     error
 testpaths = tests/
diff --git a/requirements.txt b/requirements.txt
index c74661b..36299d4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,7 @@
 -e .
 pycares==4.8.0
 pytest==8.3.5
+pytest-asyncio==0.26.0
 pytest-cov==6.1.1
 uvloop==0.21.0; platform_system != "Windows" and implementation_name == "cpython"
 winloop==0.1.8; platform_system == "Windows"
diff --git a/tests/test_aiodns.py b/tests/test_aiodns.py
index 5ceec95..8cd4886 100755
--- a/tests/test_aiodns.py
+++ b/tests/test_aiodns.py
@@ -2,16 +2,19 @@
 
 import asyncio
 import ipaddress
+import logging
 import unittest
 import pytest
 import socket
 import sys
 import time
 import unittest.mock
+from typing import Any
 
 import pycares
 
 import aiodns
+import pycares
 
 try:
     if sys.platform == "win32":
@@ -228,12 +231,201 @@ def setUp(self) -> None:
 @unittest.skipIf(sys.platform != 'win32', 'Only run on Windows')
 def test_win32_no_selector_event_loop() -> None:
     """Test DNSResolver with Windows without SelectorEventLoop."""
-    asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy())
+    # Create a non-SelectorEventLoop to trigger the error
+    mock_loop = unittest.mock.MagicMock(spec=asyncio.AbstractEventLoop)
+    mock_loop.__class__ = (
+        asyncio.AbstractEventLoop  # type: ignore[assignment]
+    )
+
+    with (
+        pytest.raises(
+            RuntimeError, match="aiodns needs a SelectorEventLoop on Windows"
+        ),
+        unittest.mock.patch("aiodns.pycares.ares_threadsafety", return_value=False),
+        unittest.mock.patch("sys.platform", "win32"),
+    ):
+        aiodns.DNSResolver(loop=mock_loop, timeout=5.0)
+
+
+@pytest.mark.parametrize(
+    ("platform", "expected_msg_parts", "unexpected_msg_parts"),
+    [
+        (
+            "linux",
+            [
+                "automatic monitoring of",
+                "resolver configuration changes",
+                "system ran out of inotify watches",
+                "Falling back to socket state callback",
+                "Consider increasing the system inotify watch limit",
+            ],
+            [],
+        ),
+        (
+            "darwin",
+            [
+                "automatic monitoring",
+                "resolver configuration changes",
+                "Falling back to socket state callback",
+            ],
+            [
+                "system ran out of inotify watches",
+                "Consider increasing the system inotify watch limit",
+            ],
+        ),
+        (
+            "win32",
+            [
+                "automatic monitoring",
+                "resolver configuration changes",
+                "Falling back to socket state callback",
+            ],
+            [
+                "system ran out of inotify watches",
+                "Consider increasing the system inotify watch limit",
+            ],
+        ),
+    ],
+)
+async def test_make_channel_ares_error(
+    platform: str,
+    expected_msg_parts: list[str],
+    unexpected_msg_parts: list[str],
+    caplog: pytest.LogCaptureFixture,
+) -> None:
+    """Test exception handling in _make_channel on different platforms."""
+    # Set log level to capture warnings
+    caplog.set_level(logging.WARNING)
+
+    # Create a mock loop that is a SelectorEventLoop to avoid Windows-specific errors
+    mock_loop = unittest.mock.MagicMock(spec=asyncio.SelectorEventLoop)
+    mock_channel = unittest.mock.MagicMock()
+
+    with (
+        unittest.mock.patch("sys.platform", platform),
+        # Configure first Channel call to raise AresError, second call to return our mock
+        unittest.mock.patch(
+            "aiodns.pycares.Channel",
+            side_effect=[
+                pycares.AresError("Mock error"),
+                mock_channel,
+            ],
+        ),
+        unittest.mock.patch("aiodns.pycares.ares_threadsafety", return_value=True),
+        # Also patch asyncio.get_event_loop to return our mock loop
+        unittest.mock.patch("asyncio.get_event_loop", return_value=mock_loop),
+    ):
+        # Create resolver which will call _make_channel
+        resolver = aiodns.DNSResolver(loop=mock_loop)
+
+        # Check that event_thread is False due to exception
+        assert resolver._event_thread is False
+
+        # Check expected message parts in the captured log
+        for part in expected_msg_parts:
+            assert part in caplog.text
+
+        # Check unexpected message parts aren't in the captured log
+        for part in unexpected_msg_parts:
+            assert part not in caplog.text
+
+
+def test_win32_import_winloop_error() -> None:
+    """Test handling of ModuleNotFoundError when importing winloop on Windows."""
+    # Create a mock event loop that is not a SelectorEventLoop
+    mock_loop = unittest.mock.MagicMock(spec=asyncio.AbstractEventLoop)
+
+    # Setup patching for this test
+    original_import = __import__
+
+    def mock_import(name: str, *args: Any, **kwargs: Any) -> Any:
+        if name == "winloop":
+            raise ModuleNotFoundError("No module named 'winloop'")
+        return original_import(name, *args, **kwargs)
+
+    # Patch the Channel class to avoid creating real network resources
+    mock_channel = unittest.mock.MagicMock()
+
+    with (
+        unittest.mock.patch("sys.platform", "win32"),
+        unittest.mock.patch("aiodns.pycares.ares_threadsafety", return_value=False),
+        unittest.mock.patch("builtins.__import__", side_effect=mock_import),
+        unittest.mock.patch("importlib.import_module", side_effect=mock_import),
+        # Also patch Channel creation to avoid socket resource leak
+        unittest.mock.patch("aiodns.pycares.Channel", return_value=mock_channel),
+        pytest.raises(RuntimeError, match=aiodns.WINDOWS_SELECTOR_ERR_MSG),
+    ):
+        aiodns.DNSResolver(loop=mock_loop)
+
+
+def test_win32_winloop_not_loop_instance() -> None:
+    """Test handling of a loop that is not a winloop.Loop instance on Windows."""
+    # Create a mock event loop that is not a SelectorEventLoop
+    mock_loop = unittest.mock.MagicMock(spec=asyncio.AbstractEventLoop)
+
+    original_import = __import__
+
+    # Create a mock winloop module with a Loop class that's an actual type
+    class MockLoop:
+        pass
+
+    mock_winloop_module = unittest.mock.MagicMock()
+    mock_winloop_module.Loop = MockLoop
+
+    def mock_import(name: str, *args: Any, **kwargs: Any) -> Any:
+        if name == "winloop":
+            return mock_winloop_module
+        return original_import(name, *args, **kwargs)
+
+    # Patch the Channel class to avoid creating real network resources
+    mock_channel = unittest.mock.MagicMock()
+
+    with (
+        unittest.mock.patch("sys.platform", "win32"),
+        unittest.mock.patch("aiodns.pycares.ares_threadsafety", return_value=False),
+        unittest.mock.patch("builtins.__import__", side_effect=mock_import),
+        unittest.mock.patch("importlib.import_module", side_effect=mock_import),
+        # Also patch Channel creation to avoid socket resource leak
+        unittest.mock.patch("aiodns.pycares.Channel", return_value=mock_channel),
+        pytest.raises(RuntimeError, match=aiodns.WINDOWS_SELECTOR_ERR_MSG),
+    ):
+        aiodns.DNSResolver(loop=mock_loop)
+
+
+def test_win32_winloop_loop_instance() -> None:
+    """Test handling of a loop that IS a winloop.Loop instance on Windows."""
+
+    # Create a mock winloop module with a Loop class
+    class MockLoop:
+        pass
+
+    # Create a mock event loop that IS a winloop.Loop instance
+    mock_loop = unittest.mock.MagicMock(spec=asyncio.AbstractEventLoop)
+    # Make isinstance check pass
+    mock_loop.__class__ = MockLoop  # type: ignore[assignment]
+
+    mock_winloop_module = unittest.mock.MagicMock()
+    mock_winloop_module.Loop = MockLoop
+
+    original_import = __import__
+
+    def mock_import(name: str, *args: Any, **kwargs: Any) -> Any:
+        if name == "winloop":
+            return mock_winloop_module
+        return original_import(name, *args, **kwargs)
+
+    # Mock channel creation to avoid actual DNS resolution
+    mock_channel = unittest.mock.MagicMock()
+
     with (
-        pytest.raises(RuntimeError, match="aiodns needs a SelectorEventLoop on Windows"),
-        unittest.mock.patch('aiodns.pycares.ares_threadsafety', return_value=False)
+        unittest.mock.patch("sys.platform", "win32"),
+        unittest.mock.patch("aiodns.pycares.ares_threadsafety", return_value=False),
+        unittest.mock.patch("builtins.__import__", side_effect=mock_import),
+        unittest.mock.patch("importlib.import_module", side_effect=mock_import),
+        unittest.mock.patch("aiodns.pycares.Channel", return_value=mock_channel),
     ):
-        aiodns.DNSResolver(loop=asyncio.new_event_loop(), timeout=5.0)
+        # This should not raise an exception since loop is a winloop.Loop instance
+        aiodns.DNSResolver(loop=mock_loop)
 
 
 if __name__ == "__main__":  # pragma: no cover


Reviews:
user: login: bdraco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T17:10:01Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds a fallback mechanism in the DNSResolver to use socket state callbacks when creating an event thread fails due to exhausted inotify watches, while also ensuring Windows-specific requirements are met.  
- Fallback logic is added in _make_channel() to gracefully handle pycares.AresError exceptions on Linux.  
- New tests are introduced to verify behavior across platforms and various event loop scenarios.  
- pytest.ini is updated to adjust asyncio default settings.

### Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

| File                     | Description                                                                   |
| ------------------------ | ----------------------------------------------------------------------------- |
| tests/test_aiodns.py     | Added test cases to verify fallback behavior and proper warnings for errors.   |
| pytest.ini               | Updated asyncio configuration options for the test suite.                     |
| aiodns/__init__.py       | Refactored _make_channel() to add fallback logic and integrated logging support.|


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**aiodns/__init__.py:107**
* [nitpick] The timeout adjustment logic in _start_timer() is a bit opaque; adding inline comments to explain the conditions and rationale for adjusting the timeout value would improve code clarity.
```
def _start_timer(self):
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T19:01:45Z
user: login: bdraco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T19:02:51Z

Review comments:
diff_hunk: @@ -61,29 +63,43 @@ def __init__(self, nameservers: Optional[Sequence[str]] = None,
         kwargs.pop('sock_state_cb', None)
         timeout = kwargs.pop('timeout', None)
         self._timeout = timeout
-        self._event_thread = hasattr(pycares,"ares_threadsafety") and pycares.ares_threadsafety()
-        if self._event_thread:
-            # pycares is thread safe
-            self._channel = pycares.Channel(event_thread=True,
-                                            timeout=timeout,
-                                            **kwargs)
-        else:
-            if sys.platform == 'win32' and not isinstance(self.loop, asyncio.SelectorEventLoop):
-                try:
-                    import winloop
-                    if not isinstance(self.loop , winloop.Loop):
-                        raise RuntimeError(WINDOWS_SELECTOR_ERR_MSG)
-                except ModuleNotFoundError:
-                    raise RuntimeError(WINDOWS_SELECTOR_ERR_MSG)
-            self._channel = pycares.Channel(sock_state_cb=self._sock_state_cb,
-                                            timeout=timeout,
-                                            **kwargs)
+        self._event_thread, self._channel = self._make_channel(**kwargs)
         if nameservers:
             self.nameservers = nameservers
         self._read_fds = set() # type: Set[int]
         self._write_fds = set() # type: Set[int]
         self._timer = None  # type: Optional[asyncio.TimerHandle]
 
+    def _make_channel(self, **kwargs: Any) -> Tuple[bool, pycares.Channel]:
+        if hasattr(pycares, "ares_threadsafety") and pycares.ares_threadsafety():
+            # pycares is thread safe
+            try:
+                return True, pycares.Channel(
+                    event_thread=True, timeout=self._timeout, **kwargs
+                )
+            except pycares.AresError as e:
+                if sys.platform == "linux":
+                    _LOGGER.debug(
+                        "Failed to create a DNS resolver channel, this usually means "
+                        "that the system ran out of inotify watches: %s",
+                        e,
+                    )
+                else:
+                    _LOGGER.debug("Failed to create a DNS resolver channel: %s", e)
path: aiodns/__init__.py
user: login: bdraco
body: This should probably warn since they get degraded experience with no automatic monitoring of resolver
created_at: 2025-05-08T17:10:00Z
author_association: MEMBER
diff_hunk: @@ -61,29 +63,50 @@ def __init__(self, nameservers: Optional[Sequence[str]] = None,
         kwargs.pop('sock_state_cb', None)
         timeout = kwargs.pop('timeout', None)
         self._timeout = timeout
-        self._event_thread = hasattr(pycares,"ares_threadsafety") and pycares.ares_threadsafety()
-        if self._event_thread:
-            # pycares is thread safe
-            self._channel = pycares.Channel(event_thread=True,
-                                            timeout=timeout,
-                                            **kwargs)
-        else:
-            if sys.platform == 'win32' and not isinstance(self.loop, asyncio.SelectorEventLoop):
-                try:
-                    import winloop
-                    if not isinstance(self.loop , winloop.Loop):
-                        raise RuntimeError(WINDOWS_SELECTOR_ERR_MSG)
-                except ModuleNotFoundError:
-                    raise RuntimeError(WINDOWS_SELECTOR_ERR_MSG)
-            self._channel = pycares.Channel(sock_state_cb=self._sock_state_cb,
-                                            timeout=timeout,
-                                            **kwargs)
+        self._event_thread, self._channel = self._make_channel(**kwargs)
         if nameservers:
             self.nameservers = nameservers
         self._read_fds = set() # type: Set[int]
         self._write_fds = set() # type: Set[int]
         self._timer = None  # type: Optional[asyncio.TimerHandle]
 
+    def _make_channel(self, **kwargs: Any) -> Tuple[bool, pycares.Channel]:
+        if hasattr(pycares, "ares_threadsafety") and pycares.ares_threadsafety():
+            # pycares is thread safe
path: aiodns/__init__.py
user: login: Copilot
body: [nitpick] Consider refactoring _make_channel() to separate the creation of the thread-safe channel and the fallback logic into smaller helper functions to improve clarity and maintainability.
```suggestion
        thread_safe_channel = self._create_thread_safe_channel(**kwargs)
        if thread_safe_channel:
            return thread_safe_channel
        self._handle_fallback_logic()
        return False, pycares.Channel(
            sock_state_cb=self._sock_state_cb, timeout=self._timeout, **kwargs
        )

    def _create_thread_safe_channel(self, **kwargs: Any) -> Optional[Tuple[bool, pycares.Channel]]:
        if hasattr(pycares, "ares_threadsafety") and pycares.ares_threadsafety():
```
created_at: 2025-05-08T19:01:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,29 +63,50 @@ def __init__(self, nameservers: Optional[Sequence[str]] = None,
         kwargs.pop('sock_state_cb', None)
         timeout = kwargs.pop('timeout', None)
         self._timeout = timeout
-        self._event_thread = hasattr(pycares,"ares_threadsafety") and pycares.ares_threadsafety()
-        if self._event_thread:
-            # pycares is thread safe
-            self._channel = pycares.Channel(event_thread=True,
-                                            timeout=timeout,
-                                            **kwargs)
-        else:
-            if sys.platform == 'win32' and not isinstance(self.loop, asyncio.SelectorEventLoop):
-                try:
-                    import winloop
-                    if not isinstance(self.loop , winloop.Loop):
-                        raise RuntimeError(WINDOWS_SELECTOR_ERR_MSG)
-                except ModuleNotFoundError:
-                    raise RuntimeError(WINDOWS_SELECTOR_ERR_MSG)
-            self._channel = pycares.Channel(sock_state_cb=self._sock_state_cb,
-                                            timeout=timeout,
-                                            **kwargs)
+        self._event_thread, self._channel = self._make_channel(**kwargs)
         if nameservers:
             self.nameservers = nameservers
         self._read_fds = set() # type: Set[int]
         self._write_fds = set() # type: Set[int]
         self._timer = None  # type: Optional[asyncio.TimerHandle]
 
+    def _make_channel(self, **kwargs: Any) -> Tuple[bool, pycares.Channel]:
+        if hasattr(pycares, "ares_threadsafety") and pycares.ares_threadsafety():
+            # pycares is thread safe
path: aiodns/__init__.py
user: login: bdraco
body: thats overkill, this PR already breaks it out into its own function
created_at: 2025-05-08T19:02:51Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-05-08T17:04:02Z
body: ## [Codecov](https://app.codecov.io/gh/aio-libs/aiodns/pull/151?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.25%. Comparing base [(`5c4b29c`)](https://app.codecov.io/gh/aio-libs/aiodns/commit/5c4b29c4847e532cb48976913fe31d0d8a51063a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) to head [(`2457e40`)](https://app.codecov.io/gh/aio-libs/aiodns/commit/2457e40c776f3275df997a1d3c8829e501ac015b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs).
> Report is 1 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #151      +/-   ##
==========================================
+ Coverage   92.55%   94.25%   +1.69%     
==========================================
  Files           3        3              
  Lines         336      400      +64     
  Branches       23       29       +6     
==========================================
+ Hits          311      377      +66     
+ Misses         15       14       -1     
+ Partials       10        9       -1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aio-libs/aiodns/pull/151?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: saghul
created_at: 2025-05-08T17:24:29Z
body: Neat idea!
author_association: CONTRIBUTOR
user: login: bdraco
created_at: 2025-05-08T19:22:04Z
body: github is being flakey, close reopen to restart ci
author_association: MEMBER
user: login: bdraco
created_at: 2025-05-08T19:24:59Z
body: I'm going to format and clean up the CI a bit after this and than do a release since #150 seems rather urgent to get a fix out
author_association: MEMBER