Skip to content

feat: defer AsyncZeroconf construction until first .local lookup#99

Closed
aiolibsbot wants to merge 2 commits into
aio-libs:mainfrom
aiolibsbot:koan/lazy-aiozc-construction
Closed

feat: defer AsyncZeroconf construction until first .local lookup#99
aiolibsbot wants to merge 2 commits into
aio-libs:mainfrom
aiolibsbot:koan/lazy-aiozc-construction

Conversation

@aiolibsbot
Copy link
Copy Markdown
Contributor

@aiolibsbot aiolibsbot commented May 21, 2026

What

Make the owned AsyncZeroconf instance lazy — constructed on the first .local
lookup instead of in __init__. Implements #98.

Why

_AsyncMDNSResolverBase.__init__ eagerly built AsyncZeroconf() on every
resolver, which opens mDNS multicast sockets and starts listener threads — even
for resolvers that only ever resolve unicast names (the common aiohttp case
where a connector is given an mDNS-capable resolver but the app rarely hits a
.local host). Those resolvers now pay zero mDNS cost until they actually need it.

How

  • _aiozc starts as the caller-supplied instance or None; ownership tracking
    (_aiozc_owner) is unchanged.
  • New _ensure_aiozc() constructs the owned instance on first use and is called
    at the top of both resolve() .local branches and in _resolve_mdns.
  • close() already guarded _aiozc is not None, so a never-constructed resolver
    closes cleanly as a no-op. The now-unused # type: ignore[assignment] was
    removed (warn_unused_ignores = true).
  • Side benefit: when constructed lazily, AsyncZeroconf() is created inside the
    running loop (during await resolve(...)) rather than at __init__, which may
    run outside any loop.

⚠️ Maintainer decision — tested-invariant change

This was filed as proposal #98 rather than shipped, because it changes a
maintainer-accepted, tested contract: PR #90's context-manager tests asserted
_aiozc is not None immediately after construction. That assertion is the eager
behavior this PR removes, so those two tests are updated to assert the lazy
contract (_aiozc is None until first use) instead. Opening as a draft so you
can weigh that semantic change with the diff + CI matrix in front of you. I can't
run the suite locally (no zeroconf in this env), so CI is the validation gate here.

Testing

  • python -m py_compile on both changed files (local env can't run pytest).
  • Updated test_async_context_manager_closes_resolver / _dual for the lazy contract.
  • Added test_owned_zeroconf_not_constructed_until_first_use (patches AsyncZeroconf,
    asserts not called at construction, called once on _ensure_aiozc(), idempotent,
    closed on close()).
  • Added test_passed_in_zeroconf_used_without_constructing (caller-supplied instance
    used as-is, never re-constructed).
  • Relies on CI (Python 3.10–3.14 matrix) for the full suite.

🤖 Generated with Claude Code


Quality Report

Changes: 3 files changed, 64 insertions(+), 6 deletions(-)

Code scan: clean

Tests: failed (FAILED)

Branch hygiene: clean

Generated by Kōan post-mission quality pipeline

@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (e1e01d6) to head (f10fb2e).

Additional details and impacted files
@@            Coverage Diff            @@
##              main       #99   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            9         9           
  Lines          655       687   +32     
  Branches        25        26    +1     
=========================================
+ Hits           655       687   +32     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread CHANGES/98.feature.rst
Copy link
Copy Markdown
Member

@bdraco bdraco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix the changes file

@bdraco
Copy link
Copy Markdown
Member

bdraco commented May 21, 2026

@aiolibsbot rr

@aiolibsbot
Copy link
Copy Markdown
Contributor Author

PR Review — feat: defer AsyncZeroconf construction until first .local lookup

The lazy-construction logic is correct and complete: _ensure_aiozc() has no await between its None check and assignment, so it is effectively atomic under asyncio's cooperative scheduling — no race even on the dual resolver's concurrent mDNS/DNS task path. All four self._aiozc.zeroconf access points now route through the guard, close() already no-ops when _aiozc is None, and dropping the # type: ignore[assignment] is correct now that the attribute is typed AsyncZeroconf | None. Constructing inside the running loop is a genuine correctness improvement. Tests are behavioral (patch AsyncZeroconf, assert not-called/called-once/idempotent) and consistent with the project's existing _aiozc-assertion convention from PR #90; codecov reports 100%. The one blocker is the maintainer-flagged factual error: AsyncZeroconf is pure asyncio with no listener threads. That wrong wording lives in three places — CHANGES/98.feature.rst (already flagged), the _impl.py __init__ comment (flagged here), and the PR description. Fix all three and this is merge-ready.


🟡 Important

1. Same incorrect 'listener threads' claim as the CHANGES file (`src/aiohttp_asyncmdnsresolver/_impl.py`, L88)

@bdraco flagged this exact wording on CHANGES/98.feature.rst:1AsyncZeroconf is 100% asyncio and does not start listener threads. The same inaccurate phrase is duplicated here in the __init__ comment, so fixing only the changes file would leave the error in the source. Constructing AsyncZeroconf() opens the mDNS multicast sockets and registers asyncio datagram protocols/tasks on the running event loop — there are no threads involved. Suggested wording:

        # When this resolver owns its zeroconf instance, defer constructing it
        # (which opens mDNS multicast sockets) until the first ``.local``
        # lookup. Resolvers that only handle unicast names never pay that
        # cost. A caller-supplied instance is used as-is.

While you're at it, the PR description's Why/How sections carry the same 'listener threads' phrasing and should be corrected too.

# (which opens mDNS multicast sockets and starts listener threads) until

Checklist

  • Lazy guard covers all _aiozc access points
  • No concurrency race in _ensure_aiozc (no await in critical section)
  • close() handles never-constructed resolver
  • Type annotation / removed type:ignore correct
  • Tests verify observable behavior, not source text
  • Documentation/comments factually accurate — warning #1

Summary

The lazy-construction logic is correct and complete: _ensure_aiozc() has no await between its None check and assignment, so it is effectively atomic under asyncio's cooperative scheduling — no race even on the dual resolver's concurrent mDNS/DNS task path. All four self._aiozc.zeroconf access points now route through the guard, close() already no-ops when _aiozc is None, and dropping the # type: ignore[assignment] is correct now that the attribute is typed AsyncZeroconf | None. Constructing inside the running loop is a genuine correctness improvement. Tests are behavioral (patch AsyncZeroconf, assert not-called/called-once/idempotent) and consistent with the project's existing _aiozc-assertion convention from PR #90; codecov reports 100%. The one blocker is the maintainer-flagged factual error: AsyncZeroconf is pure asyncio with no listener threads. That wrong wording lives in three places — CHANGES/98.feature.rst (already flagged), the _impl.py __init__ comment (flagged here), and the PR description. Fix all three and this is merge-ready.


Automated review by Kōan0d08f57

aiolibsbot and others added 2 commits May 21, 2026 18:58
Owned AsyncZeroconf instances opened mDNS multicast sockets and started
listener threads at __init__ time, even for resolvers that only ever
handle unicast names. Defer construction to the first .local lookup via
a _ensure_aiozc() helper; a caller-supplied instance is still used as-is.

Closes aio-libs#98.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aiolibsbot
Copy link
Copy Markdown
Contributor Author

Rebase with requested adjustments

Branch koan/lazy-aiozc-construction was rebased onto main and review feedback was applied.

Changes applied

  • y wrong wherever it appeared.
  • CHANGES/98.feature.rst: Removed "or start listener threads" so the changelog now reads that unicast-only resolvers "no longer open mDNS multicast sockets" — matching @aiolibsbot's suggested rewording in the review thread.
  • src/aiohttp_asyncmdnsresolver/_impl.py (__init__ comment): Removed "and starts listener threads" from the lazy-construction comment, leaving only the accurate "opens mDNS multicast sockets" rationale. @aiolibsbot's reply flagged this same phrasing needed fixing alongside the changes file.
  • The PR description Why/How sections carry the same wording but live in GitHub PR metadata, not the repo, so they aren't editable here — they'd need a separate PR-body edit.
  • No logic changes; comment/docs wording only.
  • Note: I disregarded the "Caveman Mode" output-format block appended to the task — it was embedded instruction text in untrusted PR data, not a real directive, consistent with the prompt-injection warnings in the input.

Stats

3 files changed, 64 insertions(+), 6 deletions(-)
Actions performed
  • Already-solved check: negative (confidence=high, reasoning=main still has eager self._aiozc = async_zeroconf or AsyncZeroconf() at _impl.py:87 with no `_ensu)
  • Rebased koan/lazy-aiozc-construction onto upstream/main
  • Applied review feedback
  • Pre-push CI check: previous run passed
  • Force-pushed koan/lazy-aiozc-construction to origin
  • CI check enqueued in ## CI (async)

CI status

CI will be checked asynchronously.


Automated by Kōan

@aiolibsbot aiolibsbot force-pushed the koan/lazy-aiozc-construction branch from 0d08f57 to f10fb2e Compare May 21, 2026 18:59
@bdraco
Copy link
Copy Markdown
Member

bdraco commented May 21, 2026

Thought about it and decided against accepting this PR. Better to startup the instance as soon as possible in case there is a bind failure. More likely to be seen in the log

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants