On macOS, `socket.close()` after `shutdown(SHUT_RDWR)` does not reliably interrupt a blocked `recv()` with a timeout
Personne n'a encore pris cette issue.
- Langage dominant
- Python
- Étoiles
- 77.2k
- Forks
- 35.9k
- Métriques de merge des PR
- Métriques de PR en attente
Description
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
Guide de contribution
Ouvrir le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Piste de recherche
Commencez par exécuter la reproduction fournie sur macOS et suivre les points d’entrée socket.recv(), socket.shutdown() et socket.close() dans CPython ; l’issue ne mentionne aucun fichier source ni test de régression. C’est terminé lorsque le recv() bloqué est interrompu rapidement et de manière fiable après shutdown() suivi de close(), avec une couverture de tests pour le comportement signalé.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- macos, python
- Domaine
- networking, operating-systems
- Type d'issue
- Bug
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Activité
- Active
- Clarté
- Plutôt claire
- Accessibilité débutants
- 52/100