Skip to content
Merged
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
17 changes: 17 additions & 0 deletions clients/python/tests/fake.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ def __init__(
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._sock.bind(("127.0.0.1", 0))
self._sock.listen(8)
# **A timeout, so `close` can actually stop it.** The accept loop below blocks in
# `accept()`; closing the socket from another thread does not reliably wake a thread
# already parked there, so without this the listener can outlive `close()` and accept a
# connection on its way out - which is a client getting a reset where it was promised a
# refusal.
self._sock.settimeout(0.05)
self.port = self._sock.getsockname()[1]
self._stop = threading.Event()
self._thread = threading.Thread(target=self._serve, daemon=True)
Expand All @@ -54,6 +60,9 @@ def _serve(self) -> None:
return
try:
conn, _ = self._sock.accept()
except TimeoutError:
# Nothing arrived in this slice. Round again and re-read `_stop`.
continue
except OSError:
return
self.connections += 1
Expand Down Expand Up @@ -83,7 +92,15 @@ def _handle(self, conn: socket.socket) -> None:
return

def close(self) -> None:
"""Stops listening, and does not return until that is true.

The join is the point. `close` used to set the flag and shut the socket, leaving the
accept thread parked in `accept()`; a client connecting in that window was accepted by
a server that was supposed to be gone, read in full, and dropped - which reads to the
client as `Unknown` rather than the `NotSent` a closed port owes it.
"""
self._stop.set()
self._thread.join(timeout=2.0)
with contextlib.suppress(OSError):
self._sock.close()

Expand Down
14 changes: 10 additions & 4 deletions clients/python/tests/test_transport_conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from __future__ import annotations

import asyncio
import socket
import time

import pytest
Expand Down Expand Up @@ -179,10 +180,15 @@ def test_a_connection_dropped_before_the_status_line_is_an_unknown_outcome(kind)

@BOTH
def test_a_refused_connection_provably_never_arrived(kind):
# Bound then closed, so the port is almost certainly nobody's.
server = FakeServer([])
addr = Address.parse(server.addr)
server.close()
# **A socket that never listened**, rather than a `FakeServer` that was closed. A listener
# on its way out can still accept - which is a connection that reaches somebody, reads the
# request in full and drops it, and that is `Unknown` rather than the `NotSent` this test
# is about. Nothing can ever be accepted on a socket that was never listening.
probe = socket.socket()
probe.bind(("127.0.0.1", 0))
port = probe.getsockname()[1]
probe.close()
addr = Address.parse(f"127.0.0.1:{port}")
t = drive(kind, addr)
with pytest.raises(NotSent):
t.roundtrip("GET", "/health")
Expand Down
Loading