Skip to content

fix(search): cap raw retention so a large search cannot OOM the caller - #3579

Open
thebigjc wants to merge 1 commit into
rtk-ai:developfrom
thebigjc:fix/search-raw-cap
Open

thebigjc wants to merge 1 commit into
rtk-ai:developfrom
thebigjc:fix/search-raw-cap

Conversation

@thebigjc

Copy link
Copy Markdown

Summary

  • rtk grep/rtk rg over a path buffers the engine's entire stdout via .output(), then copies it again with from_utf8_lossy. On a 30 GB log tree the search emitted 4.6 GB of matches, rtk peaked at ~9 GB, and the calling agent was OOM-killed (Out of memory: Killed process (.rtk-wrapped) total-vm:8987044kB).
  • | head -220 is no escape hatch on this path: nothing is written until the child exits, so the consumer never gets to close the pipe.
  • Fix routes the capture through run_streaming's CaptureOnly, which already caps retention at RAW_CAP (10 MiB) and warns when it truncates. One-line change plus a regression test — the streaming machinery was already there, the path-based branch just wasn't using it.

Output is unchanged for any search under the cap, so the byte-identity contract in grep_faithful_format_test still holds. Above the cap the grouped form only ever renders grep_max_results matches anyway; the N matches in M files header becomes a floor, and stderr says so.

Measured on a 64 MiB file of matches: peak allocation dropped from >90 MiB to the cap, and the search went from 2m25s to 25s (debug) / 10s (release), because the parse pass now runs over 10 MiB instead of everything.

Test plan

  • cargo fmt --all && cargo clippy --all-targets && cargo test
    • fmt clean, clippy clean, all integration suites green.
    • Note: core::tracking::tests::* flake on this machine (a different one each run, on develop unmodified too) — they share the real tracking DB.
  • New tests/search_memory_test.rs: 64 MiB of matches searched under ulimit -v 400000. Fails on develop with memory allocation of 92274688 bytes failed, passes here.
  • Manual: rtk grep <pat> <64MiB file> output byte-identical to the released binary on normal searches; the capped case prints the grouped form plus the truncation warning.

🤖 Generated with Claude Code

The path-based search buffered the engine's entire stdout via `.output()`,
then copied it again with `from_utf8_lossy`. `rtk rg <pattern> logs/` over a
30 GB tree emitted 4.6 GB of matches, so rtk peaked at ~9 GB and the calling
agent was OOM-killed. `| head -220` did not help: nothing is written until
the child exits, so the consumer never gets to close the pipe.

Route the capture through `run_streaming`'s `CaptureOnly`, which already caps
retention at `RAW_CAP` (10 MiB) and warns when it truncates. The grouped form
only ever renders `grep_max_results` matches, so output is unchanged for any
search under the cap (the byte-identity contract in
`grep_faithful_format_test` still holds); above it, the "N matches in M
files" header becomes a floor.

Measured on a 64 MiB file of matches: peak allocation dropped from >90 MiB to
the cap, and the search went from 2m25s to 25s (the parse pass now runs over
10 MiB, not everything).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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