stats: re-enable parallel processing for special-format inputs (#4462) - #4469
Merged
Conversation
…4462) `stats` refused to autoindex a special-format input (.gz/.zip/.parquet/.jsonl/...) and so always ran sequentially. That guard was the narrow fix in #4445 for a real crash: `parallel_stats` built a FRESH `Config` per worker, each of which resolved its OWN converted temp, found no index beside it, and panicked - silently, since a panicking pool worker only unwinds its own thread, leaving a headers-only stats file at exit 0. Adopt the pattern #4459 established across split/search/searchset/pragmastat and #4466 made safe: share ONE resolved `Config`. Clones share the `Arc<OnceLock>` holding the conversion, so every worker sees the same temp and the same sibling index. - `sequential_stats` and `parallel_stats` now take `&Config`; workers get a clone - `process_headers_with_weight_exclusion` takes it too, so exactly one `Config` is constructed per run - the invariant is now literally greppable - drop all three `is_special_format()` guards. The memcheck index fallback mattered most: it fires precisely when the input is too large to process sequentially, so excluding compressed inputs left them with no index escape hatch at all - autoindex cleanup now resolves the index beside the path that was actually indexed (the temp), not the compressed source Two hazards found while doing this: - stdin delimiter inference sets `args.flag_delimiter` AFTER `rconfig` is built, and relied on the compute paths rebuilding their own `Config` to pick it up. Sharing the pre-built one silently parsed a TSV as one comma-delimited column - wrong stats, exit 0, no warning. Re-applied explicitly. - `Config::resolve_converted` now logs one line per conversion, inside the `get_or_init` closure so it fires exactly once per Config family. This makes Config-rebuilding regressions countable rather than invisible; the compressed input went from 3 conversions to 1. Tests: `stats_autoindex_is_skipped_for_special_format_inputs` reworked into `stats_parallelizes_special_format_inputs`, keeping the ".zip stats == plain stats" half and dropping a vacuous assertion (no index was ever written beside the source, so it could not fail). Output cannot distinguish sequential from parallel - identical by design - so the discriminating evidence is the log: a `nchunks=` line and exactly one conversion. Covers both autoindex routes (--cache-threshold and QSV_AUTOINDEX_SIZE) plus the sequential path. New `stats_infers_stdin_delimiter` pins the delimiter hazard. All seven code changes mutation-verified: each was individually reverted and the corresponding assertion confirmed to fail. Both tests are also pinned against an ambient QSV_AUTOINDEX_SIZE / QSV_DEFAULT_DELIMITER, verified load-bearing. Closes #4462 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ments The handoff note this work came from stated that `moarstats` still rebuilds a Config per worker from the original path, and pointed at two line numbers. Both halves are wrong now: the lines are off by four, and #4464 already fixed the substance — `compute_outliers_and_kga` and `compute_all_bivariatestats` are both handed `read_input_path`, which is resolved by their caller, so the Configs they build are ordinary rather than special-format. That is a second valid shape (resolve once, pass the resolved PATH) alongside the one #4462 adopted (share the resolved CONFIG), and it is worth naming as such. Left as-is, the comment would have sent a future maintainer to fix a bug that does not exist. Also record why `resolved_path()` in the autoindex cleanup is always a cached read: it sits inside `if autoindex_set`, which is only set on the compute path after the OnceLock is populated. Verified empirically — a cache-hit run performs zero conversions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The comment said `autoindex_set` is set after `rconfig.indexed()` has populated the OnceLock. It is not — the flag is assigned at stats.rs:2037, immediately before the `indexed()` call at :2042. The conclusion is unchanged and still correct: `resolved_path()` in the cleanup block is a cached read, never a conversion. But what guarantees that is the cleanup block sitting far BELOW `indexed()` on the compute path, not the order of the flag assignment. Reworded to say so, and to name the flag's actual ordering so the next reader does not have to re-derive it. Comment-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
The docs-drift-check CI job failed on this branch: the doc claimed tests/test_stats.rs was ~8,338 lines against an actual 9,319 (10.5% drift, above the 10% tolerance). The staleness predates this branch — master was already at 9,159 lines, i.e. 9.85% drift, sitting just under the threshold. The tests added here tipped it over rather than caused it. Refreshed the sibling src/cmd/stats.rs count in the same table too: it claimed ~6,320 against an actual 6,910 (8.5%), which is under tolerance today but on the same trajectory and would have failed some unrelated PR next. Both numbers counted, not estimated. docs-drift-check now passes clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Closes #4462.
statsrefused to autoindex a special-format input (.gz/.zip/.parquet/.jsonl/…) and so always ran sequentially. That guard was the narrow fix in #4445 for a real crash:parallel_statsbuilt a freshConfigper worker, each of which resolved its own converted temp, found no index beside it, and panicked — silently, since a panicking pool worker only unwinds its own thread, leaving a headers-only stats file at exit 0.This adopts the pattern #4459 established across
split/search/searchset/pragmastatand #4466 made safe: share one resolvedConfig. Clones share theArc<OnceLock>holding the conversion, so every worker sees the same temp and the same sibling index.Changes
sequential_stats,parallel_statsandprocess_headers_with_weight_exclusionnow take&Config; parallel workers get a clone. Exactly oneConfigis constructed per run, so the invariant is greppable.is_special_format()guards removed. The memcheck index fallback mattered most — it fires precisely when the input is too large to process sequentially, so excluding compressed inputs left them with no index escape hatch at all.Could not remove index file.Config::resolve_convertedlogs one line per conversion, inside theget_or_initclosure so it fires exactly once perConfigfamily. This makes Config-rebuilding regressions countable instead of invisible.On the #4446 repro:
nchunks=17where master logged none, byte-identical to the uncompressed input, and conversions dropped 3 → 1.Two hazards found along the way
A silent wrong-results trap. stdin delimiter inference sets
args.flag_delimiterafterrconfigis built, and relied on the compute paths rebuilding their ownConfigto pick it up. Sharing the pre-built one parsed a TSV as a single comma-delimited column — wrong stats, exit 0, no warning. Re-applied explicitly;stats_infers_stdin_delimiterpins it.A stale claim in the comments. An earlier note asserted
moarstatsstill rebuilds aConfigper worker from the original path. #4464 already fixed that —compute_outliers_and_kgaandcompute_all_bivariatestatsare both handedread_input_path, already resolved by their caller. Left as written, the comment would have sent a maintainer to fix a bug that does not exist. Corrected, and the second valid shape (resolve once, pass the resolved path) is now named alongside the first (share the resolved Config).Tests
stats_autoindex_is_skipped_for_special_format_inputsis reworked intostats_parallelizes_special_format_inputs. It keeps the valuable half —.zipstats must equal the same data uncompressed — and drops one assertion that was vacuous: no index was ever written beside the compressed source, so it could not fail either way.Output cannot distinguish sequential from parallel (identical by design, which is the point), so the discriminating evidence is the log: a
nchunks=line plus exactly one conversion. Coverage spans both autoindex routes (--cache-thresholdandQSV_AUTOINDEX_SIZE) and the sequential path.All seven code changes were mutation-verified: each reverted individually, with the corresponding assertion confirmed to fail. Reverting the worker change reproduces the original crash. An early mutation pass passed when it should not have, which is what surfaced the uncovered
QSV_AUTOINDEX_SIZEroute.Both tests are also pinned against an ambient
QSV_AUTOINDEX_SIZE/QSV_DEFAULT_DELIMITER—Workdir::commandinherits the environment — and that pinning was itself verified load-bearing.Green locally:
all_features992 unit + 3748 integration,lite132 + 2251, clippy clean on both. The.zipfixture is deliberate so the new tests run under-F litetoo.Risk
The concurrency delta is small and one-directional. Workers now carry a non-zero
autoindex_sizewhere they previously inherited the env value, so a TOCTOU-deleted index self-heals instead of panicking. The staleness recheck cannot flip — the temp is written once and never touched, so its mtime cannot exceed the index's regardless of machine speed. That is reasoning rather than proof, and a green local suite is not evidence about CI, so macOS andqsvliterunners are the real check.🤖 Generated with Claude Code