On macOS, `socket.close()` after `shutdown(SHUT_RDWR)` does not reliably interrupt a blocked `recv()` with a timeout
Dieses Issue hat noch niemand übernommen.
- Vorherrschende Sprache
- Python
- Sterne
- 77.2k
- Forks
- 35.9k
- PR-Merge-Kennzahlen
- PR-Kennzahlen ausstehend
Beschreibung
Bug description:
Thread A is blocked in sock.recv() with a timeout set (sock.settimeout(...)).
Thread B closes the socket by calling sock.shutdown(socket.SHUT_RDWR) immediately followed by sock.close()
In this situation, sometimes thread A's recv() call is interrupted promptly, and sometimes it is not interrupted at all: it blocks for the full timeout duration and only then raises TimeoutError, as if shutdown()/close() had no effect on the already-blocked call.
The flakiness is easy to reproduce with the script below, if you have a Mac.
I have not observed this issue when:
recv()has no timeout set- The socket's peer closes its end — but the whole point is I want to close the socket quickly, without depending on the peer's behavior.
- A small delay i.e.
time.sleep(0.001)is inserted betweensock.shutdown(socket.SHUT_RDWR)andsock.close()(buttime.sleep(0)is not enough)
Discovered while investigating https://github.com/python-websockets/websockets/issues/1596.
Reproduction
import socket
import threading
import time
RECV_TIMEOUT = 1 # seconds
TRIALS = 50
def run_trial():
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind(("127.0.0.1", 0))
listener.listen(1)
a = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
a.connect(listener.getsockname())
b, _ = listener.accept()
listener.close()
a.settimeout(RECV_TIMEOUT)
result = {}
def blocked_recv():
t0 = time.monotonic()
try:
data = a.recv(4096)
result["outcome"] = f"returned {data!r}"
except OSError as exc:
result["outcome"] = f"raised {exc!r}"
result["elapsed"] = time.monotonic() - t0
thread = threading.Thread(target=blocked_recv)
thread.start()
time.sleep(0.15) # let the thread actually enter the recv() syscall
a.shutdown(socket.SHUT_RDWR)
a.close()
thread.join()
b.close()
return result["elapsed"], result["outcome"]
if __name__ == "__main__":
hangs = 0
for i in range(TRIALS):
elapsed, outcome = run_trial()
hung = elapsed >= RECV_TIMEOUT * 0.5
hangs += hung
print(f"trial {i}: {outcome} after {elapsed:.3f}s {'HUNG' if hung else ''}")
print(f"\n{hangs}/{TRIALS} trials failed to interrupt recv() promptly")
Expected result
recv() is interrupted within a few milliseconds in every trial, since the socket has been explicitly shut down and closed before the timeout can elapse.
Actual result
A representative run:
27/50 trials failed to interrupt recv() promptly
Individual "HUNG" trials show recv() raising TimeoutError('timed out') after the full RECV_TIMEOUT (1.0s), rather than being interrupted immediately after shutdown()/close() run (~0.15s in, as seen in the non-hung trials).
CPython versions tested on:
3.14
Specifically Python 3.14.4 (main, Apr 18 2026, 07:57:29) [Clang 17.0.0]
Operating systems tested on:
macOS
Specifically macOS 26.5.2 (Darwin 25.5.0), arm64
Beitragsleitfaden
Erste Schritte
- Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
- Forke das Repository und arbeite in einem Branch.
- Öffne einen Pull Request, der die Issue-Nummer nennt.
Rechercherichtung
Beginne damit, die bereitgestellte Reproduktion unter macOS auszuführen und die Einstiegspunkte socket.recv(), socket.shutdown() und socket.close() in CPython nachzuverfolgen; im Issue werden weder eine Quelldatei noch ein Regressionstest genannt. Als erledigt gilt die Aufgabe, wenn der blockierte recv() nach shutdown() gefolgt von close() zeitnah und zuverlässig unterbrochen wird und der gemeldete Fehlerfall durch Tests abgedeckt ist.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Bewertung
- Tech-Stack
- macos, python
- Bereich
- networking, operating-systems
- Issue-Typ
- Bug
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Aktivitätsstatus
- Aktiv
- Klarheit
- Größtenteils klar
- Anfängerfreundlichkeit
- 52/100