Problem
Client.close() and Remoter.close() immediately shut down both socket directions and dispose of the socket:
def close(self):
if self.cs:
self.shutdown()
self.cs.close()
self.cs = None
Bytes in txbs have only been accepted by HIO; they have not necessarily been accepted by the kernel. Immediate close can therefore discard a response that the application believes was queued.
The current API also lacks a recurrent path for orderly TCP shutdown. A nonblocking owner needs to drain output over multiple service cycles, issue local write shutdown once, continue receiving final peer input, and dispose of the socket only after local receive observes peer write EOF.
Required graceful-close sequence
- Drain egress. Continue normal send service until accepted output is transmitted or transmission fails.
- Shut down local writes. Call
shutdown(SHUT_WR) exactly once. The peer will observe EOF only after previously accepted kernel output.
- Wait for peer write shutdown. Keep local receive available until it observes EOF or a terminal transport failure.
- Dispose. Close the socket and make repeated close-service calls harmless.
Starting graceful close must latch closing state and reject newly submitted output. Otherwise a caller can append bytes after local write shutdown has become inevitable.
Force-close remains necessary when graceful settlement is impossible: incomplete setup, terminal connection-wide failure, an owner-imposed deadline, or administrative abort. The distinction must be explicit. Graceful close settles accepted work; force-close abandons the remaining transport.
Reproduction
- Connect a HIO endpoint to a real peer.
- Queue output and request closure before send service drains it.
- Observe that current
close() disposes of the socket immediately.
- Under the expected recurrent path, verify that the peer receives all queued bytes before it observes EOF.
- Send final peer input before
SHUT_WR on the peer and verify that HIO retains it before final disposal.
Equivalent tests should cover Client and Remoter, output that drains over several recurrences, write-shutdown failure, and repeated calls after completion.
Proposed direction
Provide a recurrent close service whose state follows the directional endpoint contract:
def serviceClose(self):
if not self.cs:
return True
if self.txbs and not self.txCutoff:
return False
if not self.txCutoff:
self.cs.shutdown(socket.SHUT_WR)
self.txCutoff = True
if not self.cutoff:
return False
self.close()
return self.cs is None
The real implementation must retain a shutdown(SHUT_WR) failure as the terminal transmit cause and preserve queued bytes. It must not translate local write shutdown into receive closure or use local receive EOF to suppress already accepted output.
Failure semantics
A local SHUT_WR failure is terminal for transmission and must be retained even if local receive can still drain. A connection-wide failure closes both directions. Neither path may delete unsent txbs. Repeated close service must not retry a write shutdown that has already succeeded or failed.
Ownership and deadlines
The endpoint supplies the recurrent primitive; its owner decides when to invoke it. The owner remains responsible for:
- establishing application or protocol completion before starting close;
- continuing send and receive service while close is pending;
- imposing a graceful-shutdown deadline; and
- choosing force-close when that deadline expires.
This issue does not define HTTP framing completion, application success, remote durability, or scheduler policy. It only makes the raw TCP transport capable of settling accepted output without blocking.
TLS endpoints must not reuse this raw SHUT_WR sequence while an active TLS session remains. Their protocol shutdown is tracked separately by #176.
Related work
Acceptance criteria
- Client and Remoter expose an idempotent recurrent graceful-close operation.
- Accepted output drains before local write shutdown.
- Local write shutdown occurs exactly once.
- Local receive remains available until peer write EOF.
- Final peer input remains observable.
- New output is rejected after close begins.
- Shutdown failures retain their cause and queued bytes.
- Owners can force-close independently when graceful settlement is no longer possible.
Problem
Client.close()andRemoter.close()immediately shut down both socket directions and dispose of the socket:Bytes in
txbshave only been accepted by HIO; they have not necessarily been accepted by the kernel. Immediate close can therefore discard a response that the application believes was queued.The current API also lacks a recurrent path for orderly TCP shutdown. A nonblocking owner needs to drain output over multiple service cycles, issue local write shutdown once, continue receiving final peer input, and dispose of the socket only after local receive observes peer write EOF.
Required graceful-close sequence
shutdown(SHUT_WR)exactly once. The peer will observe EOF only after previously accepted kernel output.Starting graceful close must latch closing state and reject newly submitted output. Otherwise a caller can append bytes after local write shutdown has become inevitable.
Force-close remains necessary when graceful settlement is impossible: incomplete setup, terminal connection-wide failure, an owner-imposed deadline, or administrative abort. The distinction must be explicit. Graceful close settles accepted work; force-close abandons the remaining transport.
Reproduction
close()disposes of the socket immediately.SHUT_WRon the peer and verify that HIO retains it before final disposal.Equivalent tests should cover
ClientandRemoter, output that drains over several recurrences, write-shutdown failure, and repeated calls after completion.Proposed direction
Provide a recurrent close service whose state follows the directional endpoint contract:
The real implementation must retain a
shutdown(SHUT_WR)failure as the terminal transmit cause and preserve queued bytes. It must not translate local write shutdown into receive closure or use local receive EOF to suppress already accepted output.Failure semantics
A local
SHUT_WRfailure is terminal for transmission and must be retained even if local receive can still drain. A connection-wide failure closes both directions. Neither path may delete unsenttxbs. Repeated close service must not retry a write shutdown that has already succeeded or failed.Ownership and deadlines
The endpoint supplies the recurrent primitive; its owner decides when to invoke it. The owner remains responsible for:
This issue does not define HTTP framing completion, application success, remote durability, or scheduler policy. It only makes the raw TCP transport capable of settling accepted output without blocking.
TLS endpoints must not reuse this raw
SHUT_WRsequence while an active TLS session remains. Their protocol shutdown is tracked separately by #176.Related work
SSLSocket.unwrap().Acceptance criteria