Skip to content

TCP endpoints lack a graceful close path that drains queued output #175

Description

@kentbull

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

  1. Drain egress. Continue normal send service until accepted output is transmitted or transmission fails.
  2. Shut down local writes. Call shutdown(SHUT_WR) exactly once. The peer will observe EOF only after previously accepted kernel output.
  3. Wait for peer write shutdown. Keep local receive available until it observes EOF or a terminal transport failure.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions