v0.3.4
Stability release: a systematic leak/liveness audit of the stack (driven by live measurement through pymobiledevice3's userspace RSD tunnel), fixing every confirmed way a session, port, waiter task, or timer could be stranded.
Highlights
- TCP: blocked
recv()waiters are now woken on every session-death transition. RST in FIN_WAIT_1/FIN_WAIT_2/CLOSING/CLOSE_WAIT, the peer's FIN entering TIME_WAIT/CLOSING, the FIN_WAIT_2 orphan reap, and the 2MSL expiry all left a parkedrecv()sleeping forever while the socket was unregistered under it (measured live: 24/30 abandoned tunnel relays leaked their reader tasks permanently)._change_statenow owns a catch-all wake, and a reset connection raisesConnectionResetError(POSIX parity) instead of reading as a clean EOF — data received ahead of the failure is still delivered first. - NUD:
FAILEDneighbours re-resolve on next TX (Linux parity). A neighbour that missed one solicit burst (~3 s of unresponsiveness) was black-holed until process restart on small caches — GC never runs below 128 entries, so a point-to-point tunnel never recovered. FAILED entries now restart resolution on demand, drop their undeliverable queued frames, and are always GC-evictable. - TCP: CLOSING (simultaneous close) can no longer dead-end. The state had no timer handler, no FIN retransmission, and no give-up path: one lost ACK pinned the session and its port forever, with a 1 kHz pump spin on top. It now retransmits the FIN on the RTO ladder and collapses through the R2 budget like its siblings.
- Datagram sockets:
close()andstack.stop()wake blocked receivers withEBADF. UDP/RAW/PACKETrecv()waiters were woken by nothing (stop() even skipped AF_PACKET sockets entirely); the default blocking mode parked those tasks for the loop's lifetime. - Zero-idle-CPU restored. Gracefully closed sessions spun the 1 ms pump through their entire TIME_WAIT; persist-timer deactivation, RACK reorder ticks, and TLP declines all left expired deadlines that re-armed the coalesced service at its 1 ms floor forever. Also fixes the RFC 8985 §6.2 boundary (
>=) the RACK spin was masking.
No API breaks; recv()-family calls can now raise ConnectionResetError/ConnectionAbortedError/TimeoutError/OSError(EBADF) where they previously blocked forever or returned misleading b"".
Verified per-commit on Python 3.9 with the CI suites plus the full test suite (4145 tests), and end-to-end against a physical iOS 27.0 device over the userspace tunnel: connect/close, relay-abandonment, connection-refused, and cancelled-connect churn now return to baseline with zero stranded tasks, at unchanged throughput and latency.