-
Notifications
You must be signed in to change notification settings - Fork 2
fix(run): bound child output capture and reaper threads #340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
58 changes: 58 additions & 0 deletions
58
docs/vrs/.decisions/0007-child-output-capture-is-bounded-and-tail-preserving.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # Child process output capture is bounded and tail-preserving | ||
|
|
||
| Status: accepted | ||
|
|
||
| Decision made by Johannes on 2026-08-25 (issue #339, independently reproduced | ||
| measurements in the session record; option A of three, cap size 256 KiB). | ||
|
|
||
| ## Context | ||
|
|
||
| The supervisor shell-out helpers (`src/run.rs` | ||
| `output_with_input_timeout_observed`, `src/ding/mod.rs` `output_with_timeout`) | ||
| redirect child stdout/stderr to tempfiles — sound, because files keep an | ||
| escaped descendant from blocking cleanup and make bounded read-back | ||
| deadlock-free — then unconditionally rewind both streams and `read_to_end` | ||
| them into fresh heap buffers. Measured peak RSS scales 1:1 with child output | ||
| (16 MiB child → +16.2 MiB RSS; 8 concurrent × 16 MiB → +103 MiB) despite doc | ||
| comments claiming "bounded output capture". No spec or invariant defined a | ||
| bound; the comment described aspiration, not behavior. | ||
|
|
||
| Consumer inventory: only `pty list --json` (parsed JSON, naturally bounded by | ||
| catalog size) and `pty peek` (terminal screen text, consumed for composer | ||
| matching where the tail is what matters) need complete stdout. Every other | ||
| call site uses stderr only trimmed inside error strings. The same audit found | ||
| two sibling hazards: detached reaper threads accumulate without bound under | ||
| timeout storms, and `src/eval_run.rs` holds whole step output plus full | ||
| scrollback in memory before writing log files, with an undrained pipe pair in | ||
| the bash judge that can deadlock. | ||
|
|
||
| ## Decision | ||
|
|
||
| 1. **Diagnostics capture is capped at 256 KiB per stream and tail-preserving.** | ||
| When a stream exceeds the cap, the last `CAPTURE_CAP_BYTES` bytes are kept; | ||
| the head is dropped. Truncation emits one diagnostic line naming the | ||
| command, stream, kept/total bytes, and cap. | ||
| 2. **Payload capture stays complete and explicit.** Callers that parse | ||
| structured data (`pty list --json`) use a distinctly named | ||
| full-stdout variant whose doc comment states that stdout is intentionally | ||
| uncapped and why. Bounded-tail remains the default so an uncapped read is | ||
| always a visible, deliberate choice at the call site. | ||
| 3. **One shared reaper thread** drains killed children over a channel, | ||
| replacing per-timeout detached threads. | ||
| 4. **Eval run steps stream to their log files** instead of buffering, and the | ||
| bash judge uses null stdio (only its exit status is consumed). | ||
|
|
||
| Rejected alternatives: disk spill references for oversized diagnostics | ||
| (spill-file lifecycle for no demonstrated consumer) and streaming every | ||
| shell-out to log files exec-backend style (changes every error path; revisit | ||
| only if a consumer needs full oversized diagnostics). | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Peak supervisor RSS no longer scales with child output volume; worst case | ||
| is bounded by calls × 512 KiB regardless of child behavior. | ||
| - Diagnostics for oversized children lose their head. Error messages built | ||
| from stderr keep their tail, which is where failure text lives. | ||
| - The tempfile redirection is now load-bearing for more than cleanup: it is | ||
| what makes the bounded read-back deadlock-free. Any future move back to | ||
| pipes must preserve a bound on buffered bytes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
st2 evalis launched from an interactive terminal and a run step reads stdin, replacingCommand::output()withCommand::status()makes the shell inherit st2's stdin instead of receiving immediate EOF. Such a step can now consume the operator's input or hang the evaluation indefinitely, since run steps have no timeout. Set stdin explicitly toStdio::null()while retaining the new file-backed stdout/stderr capture.Useful? React with 👍 / 👎.