Skip to content

Blocking I/O inside an async function #8

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

main_with_args synchronously reads bootstrap configuration from inside its async execution path. File::open and read_to_end can block the executor thread. A byte ceiling limits consumed bytes but does not bound time spent opening or reading a special file.

Evidence and reproduction

  • tools/cli/src/entry.rs:107: synchronous configuration(...) call before awaiting the application builder.
  • tools/cli/src/entry.rs:171-182: direct synchronous file opening and reading.
  • During review, supplying a FIFO with no writer as --bootstrap blocked the existing CLI executable until a 2-second watchdog terminated the child.
  • Unix reproduction: create a FIFO using mkfifo bootstrap.json, then run the built CLI with --bootstrap bootstrap.json emit profile-search --id abc123 under an external watchdog. Do not leave the hung process running.

Acceptance criteria

  • Keep blocking file acquisition off async executor threads, or complete it at an explicit synchronous boundary before async execution.
  • Define and enforce accepted bootstrap file types; reject unsupported special files without waiting for a writer.
  • Preserve byte/encoding/JSON validation, exit policy, and the guarantee that invalid bootstrap data does not invoke the application builder.
  • Verify executor progress and special-file handling with bounded watchdog-based checks. Moving work to a blocking worker alone does not make a blocked OS read cancellable.

Code examples and suggested direction

One possible private bridge, after adopting the regular-file policy from #9:

async fn configuration_async(
    path: Option<std::path::PathBuf>,
) -> stackpress_lib::Result<stackpress_server::ConfigLayers> {
    tokio::task::spawn_blocking(move || configuration(path.as_deref()))
        .await
        .map_err(stackpress_lib::Error::from_source)?
}

Call this helper with .await inside the existing error/exit-code match. It returns the inner configuration result and preserves a blocking-worker join failure as a source.

This moves blocking work off the executor; it does not make a stalled OS operation abortable.

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 main_with → main_with_args → configuration. Preserve one argv parse, invalid-input short circuiting, one builder invocation, and current exit/diagnostic disclosure policy.
    2. Resolve special-file handling with Inconsistent validation across file-reading APIs #9 before treating a worker handoff as complete. A byte limit and spawn_blocking do not fix a FIFO blocked in open.
    3. Evaluate the private owned-path bridge in the description versus a truly synchronous outer preparation phase. Preserve the reusable embedded entry point; do not add process reads to core Server.
    4. Map worker panic/join errors deliberately. Do not hold sink locks across the await, retry the builder, or add an uncapped background reader.
    5. Verify executor progress using a deterministic test seam: a blocking test operation waits on a standard channel, while another task on a current-thread runtime sends the release signal. Run the probe in a separate process with an external watchdog so a regression cannot hang the test runner.
    6. Cover missing file, malformed/oversized input, invalid UTF-8, and FIFO rejection; check the builder was not called on failure.

    Relevant test invocation:

    cargo test -p stackpress-cli --test entry_contracts --locked
    cargo test -p stackpress-cli --no-default-features --test entry_contracts --locked

    Coordinate with #9 but keep the responsibilities distinct: this issue owns async scheduling; #9 owns file-acquisition policy. See Tokio's blocking-task cancellation limits.

    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).

    • CLI bootstrap configuration now accepts regular files only and does the blocking read off the async executor; a FIFO is rejected without waiting for a writer.
    • Exit policy and the "invalid bootstrap never runs the app builder" guarantee are unchanged.
    • Regression failed first (a sibling future could not run: bootstrap blocked executor progress), now passes under a kill/reap watchdog.

    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