TcpConnection has no way to close, or half-close, the connection it represents.
That is fine for the transport itself — it owns the descriptor and frees it when the refcount drops in DecRef. But a protocol layer built on ioxide routinely needs to end a connection before that, and has no API to say so. HTTP is the obvious case: Connection: close means send the response, then FIN.
Both HTTP layers on ioxide solve it the same way
Two independent consumers reach around the abstraction for the descriptor and call shutdown(2) through P/Invoke:
- GenHTTP.Engine.Ioxide — its assembly references
libc and shutdown.
- HttpArena's
ioxide entry — it did not, which is why it never closed a connection at all: the response went out carrying Connection: close and the socket stayed open, so any client that reads to EOF hung until its own timeout and the process leaked a descriptor per request. Fixed by doing the same P/Invoke.
When two separate consumers independently invent the same workaround, it is usually a missing API rather than two mistakes.
What the workaround costs
Every consumer has to:
- reach for
ClientFd, a descriptor it does not own, and hope the lifetime lines up
- hardcode
SHUT_WR = 1 and get the platform constant right
- pick
shutdown(2) over close(2) and understand why — closing frees a number the reactor still holds, so the next accept could hand the same integer to someone else. That reasoning is not obvious, and getting it wrong produces a bug that only shows up under load.
Proposal
public void ShutdownWrite(); // FIN, descriptor lifetime unchanged
Half-close is the operation that is actually wanted: the protocol layer is done sending, the transport still owns the descriptor and still closes it on DecRef. A full Close() would need to define what happens to the refcount, which ShutdownWrite() avoids entirely.
Checked on 0.4.169 and 0.7.210: conn.Close() does not compile on either, and conn.Dispose() compiles but does not close the socket.
Reproducing
Any HTTP layer on ioxide that honours Connection: close:
$ curl -sv http://localhost:8080/ -H 'Connection: close'
< HTTP/1.1 200 OK
< Connection: close
... socket still open 20s later, curl waits for its own timeout
Context: MDA2AV/HttpArena#1338.
TcpConnectionhas no way to close, or half-close, the connection it represents.That is fine for the transport itself — it owns the descriptor and frees it when the refcount drops in
DecRef. But a protocol layer built on ioxide routinely needs to end a connection before that, and has no API to say so. HTTP is the obvious case:Connection: closemeans send the response, then FIN.Both HTTP layers on ioxide solve it the same way
Two independent consumers reach around the abstraction for the descriptor and call
shutdown(2)through P/Invoke:libcandshutdown.ioxideentry — it did not, which is why it never closed a connection at all: the response went out carryingConnection: closeand the socket stayed open, so any client that reads to EOF hung until its own timeout and the process leaked a descriptor per request. Fixed by doing the same P/Invoke.When two separate consumers independently invent the same workaround, it is usually a missing API rather than two mistakes.
What the workaround costs
Every consumer has to:
ClientFd, a descriptor it does not own, and hope the lifetime lines upSHUT_WR = 1and get the platform constant rightshutdown(2)overclose(2)and understand why — closing frees a number the reactor still holds, so the nextacceptcould hand the same integer to someone else. That reasoning is not obvious, and getting it wrong produces a bug that only shows up under load.Proposal
Half-close is the operation that is actually wanted: the protocol layer is done sending, the transport still owns the descriptor and still closes it on
DecRef. A fullClose()would need to define what happens to the refcount, whichShutdownWrite()avoids entirely.Checked on 0.4.169 and 0.7.210:
conn.Close()does not compile on either, andconn.Dispose()compiles but does not close the socket.Reproducing
Any HTTP layer on ioxide that honours
Connection: close:Context: MDA2AV/HttpArena#1338.