Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/zeroconf/_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ async def _async_create_endpoints(self) -> None:
if s in self._respond_sockets:
self._respond_sockets.remove(s)

def _async_remove_listener(self, listener: AsyncListener) -> None:
"""Drop a listener and its wrapped transports from the engine lists."""
wrapped = listener.transport
transport = wrapped.transport if wrapped is not None else None
if listener in self.protocols:
self.protocols.remove(listener)
if transport is not None:
self.readers = [w for w in self.readers if w.transport is not transport]
self.senders = [w for w in self.senders if w.transport is not transport]

def _async_cache_cleanup(self) -> None:
"""Periodic cache cleanup."""
now = current_time_millis()
Expand Down
3 changes: 2 additions & 1 deletion src/zeroconf/_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,4 +356,5 @@ def connection_made(self, transport: asyncio.BaseTransport) -> None:
self.sock_description = f"{wrapped_transport.fileno} ({wrapped_transport.sock_name})"

def connection_lost(self, exc: Exception | None) -> None:
"""Handle connection lost."""
"""Prune this transport from the engine so a dead socket is not reused."""
self.zc.engine._async_remove_listener(self)
44 changes: 43 additions & 1 deletion tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import pytest

import zeroconf as r
from zeroconf import _engine, const
from zeroconf import _engine, _listener, const
from zeroconf.asyncio import AsyncZeroconf

log = logging.getLogger("zeroconf")
Expand Down Expand Up @@ -84,6 +84,48 @@ async def test_setup_releases_socket_ownership(aiozc_loopback: AsyncZeroconf) ->
assert engine.senders


@pytest.mark.asyncio
async def test_connection_lost_prunes_transport(aiozc_loopback: AsyncZeroconf) -> None:
"""A lost transport is removed from the engine reader/sender/protocol lists."""
await aiozc_loopback.zeroconf.async_wait_for_start()
engine = aiozc_loopback.zeroconf.engine
assert engine.senders
reader_count = len(engine.readers)
sender_count = len(engine.senders)
protocol_count = len(engine.protocols)

dead_transport = engine.senders[0].transport
protocol = next(
p for p in engine.protocols if p.transport is not None and p.transport.transport is dead_transport
)
protocol.connection_lost(None)

assert len(engine.senders) == sender_count - 1
assert len(engine.readers) == reader_count - 1
assert len(engine.protocols) == protocol_count - 1
assert all(w.transport is not dead_transport for w in engine.senders)
assert all(w.transport is not dead_transport for w in engine.readers)
assert protocol not in engine.protocols


@pytest.mark.asyncio
async def test_connection_lost_without_transport_is_noop(aiozc_loopback: AsyncZeroconf) -> None:
"""connection_lost on a listener that never bound a transport leaves the lists intact."""
await aiozc_loopback.zeroconf.async_wait_for_start()
engine = aiozc_loopback.zeroconf.engine
reader_count = len(engine.readers)
sender_count = len(engine.senders)
protocol_count = len(engine.protocols)

orphan = _listener.AsyncListener(aiozc_loopback.zeroconf)
assert orphan.transport is None
orphan.connection_lost(None)

assert len(engine.readers) == reader_count
assert len(engine.senders) == sender_count
assert len(engine.protocols) == protocol_count


@pytest.mark.asyncio
async def test_async_close_propagates_outer_cancellation(aiozc_loopback: AsyncZeroconf) -> None:
"""Outer-task cancellation while awaiting setup propagates to the caller."""
Expand Down
Loading