Skip to content

Blocking cleanup hidden inside Drop #12

Open

Description

@cblanquera

Timing: Follow-up after trial/3 merges into main; defer implementation until then.

Review baseline: Local trial/3 at a094418baf6388e3eeca23821df481a7f0a8751c. This branch is not yet on the remote. Source locations below refer to that snapshot and should be rechecked after merge.

Problem

TtyInput::drop synchronously joins its native input worker. Dropping a pending async prompt during cancellation or unwind can therefore block the executor thread performing the drop.

This is a documented compromise, not a demonstrated deadlock: the normal worker loop uses a 20-ms polling/backpressure interval, but competing Crossterm calls and OS stalls are explicitly outside that bound. The existing code deliberately avoids detaching a live stdin reader and attempts to restore terminal ownership.

Evidence

  • tools/cli/src/tty.rs:247-254: explicit async close awaits completion before joining.
  • tools/cli/src/tty.rs:273-281: synchronous worker join.
  • tools/cli/src/tty.rs:289-296: Drop signals stop and then joins synchronously.

Acceptance criteria

  • Evaluate a cleanup design that minimizes executor blocking on cancellation/unwind while preserving exclusive stdin ownership and raw-mode restoration.
  • Keep normal explicit close asynchronous and idempotent.
  • Do not silently detach a competing terminal reader or claim that arbitrary OS stalls have a strict time bound.
  • Verify idle, backpressured, cancellation, repeated-close, worker-failure, and restoration-failure behavior; document any unavoidable blocking fallback.

Code examples and suggested direction

A possible ownership direction is a prestarted cleanup/reaping owner:

// Design sketch only: cleanup is a proposed new owned component.
impl Drop for TtyInput {
    fn drop(&mut self) {
        self.signal_stop();
        if let Some(worker) = self.worker.take() {
            self.cleanup.retire(worker);
        }
    }
}

retire would have to transfer ownership without waiting for worker completion, guarantee eventual joining, and define queue/shutdown failure handling. This snippet is not a complete fix. The terminal lease must remain held until the worker actually restores raw mode.

Activity

  1. cblanquera commented on Oct 26, 2026

    @cblanquera
    MemberAuthor

    Suggested investigation and fix notes

    Deferred until trial/3 is merged into main. Recheck the merged source and tests first. The snippets are examples/proposals, not an approved patch or a change to frozen contracts.

    1. Trace the guard, worker, completion channel, close(), and Drop across success, cancellation, panic, and restoration failure. Use tty_contracts, tty_prompt_contracts, and their PTY probe helpers as the baseline.
    2. Evaluate a prestarted reaper/cleanup owner versus a documented bounded fallback. Preserve ownership of the exact worker and its TtySession; dropping the join handle alone detaches the reader and is not sufficient.
    3. Define behavior when the cleanup queue is full/disconnected, the runtime is shutting down, or the owner is itself dropped. Do not assume spawn_blocking from Drop is always available or abortable.
    4. Preserve explicit async close and report its errors; prevent a second stdin acquisition until restoration is complete. Do not release the global terminal lease just because work was queued for cleanup.
    5. Add a controlled worker seam so a separate-process test can hold worker completion while confirming unrelated executor work still progresses. Use an external watchdog to avoid deadlocking CI.
    6. Reuse PTY probes for real raw-mode restoration, including pre-existing raw mode, full queues, cancellation, repeated close, worker panic, and restoration failure. If a nonblocking fallback cannot preserve the required ownership guarantees, record that tradeoff rather than claiming a solved hard bound.

    Suggested feature checks:

    cargo test -p stackpress-cli --test tty_contracts --test tty_prompt_contracts --locked
    cargo check -p stackpress-cli --no-default-features --locked

    Implementation workflow: After the merge, follow .agents/workflows/rust-programming.md: prove the affected behavior, complete the three test-verified review rounds, and record relevant formatting/lints/docs, feature/platform scope, and applicable development coverage. Preserve unrelated changes.

  2. cblanquera commented on Oct 26, 2026

    @cblanquera
    MemberAuthor

    Fixed on trial/3 in 407b4d6 + df443b7 (merged as ff7a251, task 00010 lane 2; not yet on main, so leaving open).

    • TtyInput drop no longer blocks the executor on a synchronous worker join in the normal path; explicit close stays async and idempotent, and a competing stdin reader is never detached.
    • Remaining unavoidable blocking fallbacks are documented. Real PTY tests cover idle/backpressured workers, cancellation/unwind, prior raw mode, repeated close and termios restoration; controlled tests cover cancelled close, worker panic and restoration failure.

    Coordinator-verified on the combined code: fmt, Clippy -D warnings, Rustdoc -D warnings, cargo +1.96.0 check, 466 workspace tests and 857 doctests pass (467 tests on merged trial/3). Coverage: server 91.61%, CLI 93.15%, transformer 93.62%.

  3. added a commit that references this issue on Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions