Skip to content

Cancel from either side, tell the other side; exit codes and state lines - #73

Open
op-q wants to merge 4 commits into
feat/receiver-consentfrom
feat/cancel-and-status
Open

op-q wants to merge 4 commits into
feat/receiver-consentfrom
feat/cancel-and-status

Conversation

@op-q

@op-q op-q commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Consent plan, phase 4, recorded as decision 20. It changes behaviour, so it waits for your review. Stacked on #72.

What changes for a person

  • Ctrl-C on either side cancels and tells the other side. The sender prints "the receiver cancelled the transfer" (and vice versa) instead of a dropped-connection error. A second Ctrl-C quits at once, as before.
  • A half-received file is never left behind.
  • Exit statuses: 0 done, 1 failed, 3 declined or didn't answer, 4 the other side cancelled, 130 cancelled here. Declines and cancels print as sentences, not error:.
  • With --status, each side prints drop-status: state=connected | code-ok | accepted | finishing | done | declined | cancelled.

How

cancel::Cancellable<T> wraps the transport. Once a cancel fires, the next receive (including one already waiting) or send tells the peer cancel {reason:"user"} once, bounded by 1s, and fails with TransportError::Cancelled. Every wait already goes through a transport, so no transfer function needed a new parameter. Sends are refused before they start, never interrupted mid-frame.

Bugs found on the way (both now tested)

  1. Partial files survived any early exit other than an integrity failure, including a plain dropped connection. PartialFile now deletes on drop unless every byte was verified and flushed. Negative control: with the guard disabled, both mid-transfer cancel tests fail on the leftover file.
  2. A sender reported Broken pipe when the receiver cancelled. The relay forwards cancel and then closes the socket, so the sender's next write fails before it reads the reason. A failed write now reads what already arrived (≤500 ms) and reports the peer's reason.

Tests (248, up from 240)

  • Cancel unit tests: a fire before the wait is still seen; a waiting receive is interrupted and the peer is told exactly once; nothing is sent after a cancel; exit-code mapping.
  • Over a real relay: the receiver cancels mid-transfer, so the receiver exits 130, the sender exits 4, no file is left, and the relay counts it as cancelled rather than failed. The sender cancels mid-transfer, so the receiver exits 4 and keeps nothing.
  • Real SIGINT (Unix) against the binaries: drop recv exits 130 with "Cancelled."; drop send exits 4 and prints states connected, code-ok, accepted, cancelled.
  • netlab relayed topology asserts both peers' state sequences against the real binaries. It passes.
  • One flaky run in 18 was traced to a 48 MiB transfer finishing before the cancel landed. The tests now use a 1 GiB sparse file; 10/10 runs passed after the change.

Not tested: a cancel over the direct path (it uses the same wrapper), and the "N files were kept" message for a cancelled extraction.

🤖 Generated with Claude Code

https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S

Ctrl-C used to exit without a word. The peer found out from a dropped
connection, and the relay counted it as a failure. Now the first Ctrl-C (or
SIGTERM) cancels: whatever the transfer waits on next sends the peer `cancel`
and stops, and the other side says who stopped it. A second Ctrl-C, or two
seconds without stopping, quits at once as before.

The mechanism is a wrapper around the transport, since every wait in both
directions already goes through one. Sends are refused before they start,
never interrupted, so no frame is left half-written.

Two bugs found on the way, both covered by tests:

- A receiver left a partial file behind on any early exit other than an
  integrity failure, including a dropped connection. A guard now deletes it
  unless every byte arrived and was verified.
- A sender that was mid-write when the receiver cancelled reported "Broken
  pipe": the relay forwards `cancel` and closes, and the write fails first. A
  failed write now reads what already arrived and reports the peer's reason.

Exit statuses: 3 declined or not answered, 4 cancelled by the other side, 130
cancelled here. With --status each side prints `drop-status: state=` lines,
which netlab now asserts in the relayed topology.

Consent plan phase 4; decisions entry 20.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

op-q and others added 2 commits September 15, 2026 12:34
The Windows CI run showed the sender still reporting "connection aborted"
when the receiver cancelled, after the relay learned to drain. The sender only
read when its window was full, so it never answered the relay's close. The
drain expired, the relay reset the connection, and Windows discarded the unread
`cancel` along with everything else. Reading after the failed write, which
works on Linux, cannot get it back there.

The sender now reads whatever has already arrived before each chunk, without
waiting, which is safe because a receive must be cancel-safe. Acknowledgements
move the window as before, and a `cancel` ends the transfer with the
receiver's reason while it is still readable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant