docs(guidelines): record the HashMap iteration-order defect class - #1290
Merged
Conversation
One review sweep between 2026-08-20 and 2026-08-22 turned up seven independent instances of a single defect class across five modules, and nothing in the toolchain flagged any of them: types are correct, `cargo check` is clean, clippy is clean, and the tests pass most of the time. The class is a `HashMap` iteration result becoming ordered state, or feeding an order-sensitive consumer. It was rediscovered from scratch each time, and one instance (#1276) was documented nowhere except a closed PR body. Adds a "HashMap Iteration Order" section to `docs/code-guidelines.md` as a sibling to "JIT Kernel Cache Keys", in the same shape (rule with example, why it matters, enforcement). It covers the per-instance rather than per-process `RandomState` behavior with a re-derived measurement, what counts as an order-sensitive consumer, the stable-sort subtlety, the regression-testing rule, the `BTreeMap` tradeoff, and the seven instances as evidence with their issue and PR numbers. The stable-sort subtlety gets the most space because it is the least obvious part and the reason three of the seven survived review: `slice::sort_by_key` is stable, so a sort does not pin a `HashMap`-derived list unless the key is a total order over the elements, and `sort_unstable_by` substitutes a different arbitrary tie order rather than fixing it. The section gives three correct total-order remedies drawn from the actual fixes. The testing rule is stated because every fix in this family needed it: a fresh map per iteration, at least 32 iterations, and a deliberately constructed tie, since distinct keys already give a total order and `Instant::now()` never collides on its own at nanosecond resolution. PR #1281's pre-fix run failed on 27 of 64 freshly built maps, which is why a single-shot test would have passed and shipped nothing. Part 2 of #1287, a static check, is declined, and the numbers behind that are recorded in the Enforcement block. Two candidates were prototyped in the shape of `scripts/ci/check_kernel_dtype_keys.py` and run over all 1,248 `.rs` files under `src/`, against the current tree and against the reconstructed pre-fix tree of each of the five fixes. A `min_by_key` / `max_by_key` rule scoped to an unordered-map receiver has 4 flags and 0 false positives on the current tree but catches 0 of the 7 known instances, and all 4 of its hits are lint true positives with no live defect behind them, since every key is an `Instant` stamped once per call. Gating on it would mean suppressing 4 of 4 findings on day one, and the suppression would not re-arm on the change that would make the pattern real. A `Vec`-from-a-map-view rule has 22 flags and cannot distinguish a total sort key from a partial one, so it flags all three #1286 sites after PR #1288 correctly fixed them. Validated with `make verify-fmt` (exit 0) and the issue's `grep` checks. No Rust or build files are touched and no script was added, so no other gate applies. Refs #1287
Bilingual report for the HashMap iteration order guideline, recorded before the merge because this repository carries the `TECHNICAL_REPORTS/.keep-reports` marker. Force-added since `TECHNICAL_REPORTS/` is gitignored. Refs #1287
This was referenced Aug 22, 2026
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.
Summary
One review sweep between 2026-08-20 and 2026-08-22 turned up seven independent instances of a single defect class across five modules, and no part of the toolchain flagged any of them. The class is a
HashMapiteration result becoming ordered state, or feeding a consumer that is sensitive to order. This writes the class down indocs/code-guidelines.md, as a sibling to "JIT Kernel Cache Keys" and in the same shape, and records a measured decision to decline the static check that #1287 left open.What changed
docs/code-guidelines.md: new "HashMap Iteration Order" section, additive, with the existing three sections untouched. It states the rule with a wrong/right example drawn from the actual code, then a**Why this matters:**block, three subsections, and an**Enforcement:**block.docs/README.md: entry 20 extended so the docs index names the new section, matching how it already enumerates the file's other sections.The section covers:
RandomStateseeds each map instance, so a fixed binary on a fixed machine does not see a fixed order. Re-derived here withrustc -O1.97.1: tenHashMap<&str, u32>built from the same five keys inside one process, over 200 processes, gave 10 distinct orders out of 10 in 127 processes, 9 in 62, and 8 in 11. Not one process in 200 had the ten maps agree. (The issue's figure was 133/52/15; the split moves between measurement runs because the experiment is itself random, and the invariant that the ten never agree does not.)min_by_key/max_by_key(both return the FIRST extremum, so they tie-break on input order), a prefix viatakeor an earlybreakorreturn, a loop-carried accumulator such as a seed advanced once per element, and anything documented as a priority order.slice::sort_by_keyandslice::sort_byare stable, so a sort does not make aHashMap-derived list deterministic unless the key is a total order over the elements.sort_unstable_byis not a remedy either; it substitutes a different arbitrary tie order. Three of the seven instances (fix(distributed): eviction candidate sorts are stable, so HashMap order still picks which tied sequence is evicted #1286, PR fix(distributed): break eviction sort ties on the unique id #1288) sorted and were still nondeterministic. Three correct total-order remedies are given, taken from the real fixes.Instant::now()never collides on its own at nanosecond resolution. PR fix(rt-detr-v2): make needs_sanitize independent of HashMap order #1281's pre-fix run failed on 27 of 64 freshly built maps, which is exactly why a single-shot test would have passed and shipped nothing.BTreeMaptradeoff, and why fix(distributed): registry accessors return HashMap order, so node routing and failover re-routing are not reproducible #1277 kept itsHashMapand sorted in the accessors instead.Part 2: the static check is declined, with the numbers
Two candidates were prototyped in the shape of
scripts/ci/check_kernel_dtype_keys.pyand run over all 1,248.rsfiles undersrc/, against the current tree and against the reconstructed pre-fix tree (git archive <fix>^ src) of each of the five fixes.min_by_key/max_by_keyon an unordered-map receiver. 4 flags on the current tree, 0 false positives:src/server/prompt_cache/store.rs:315and:336,src/server/responses_store.rs:243,src/server/conversation_store.rs:151. Correctly excludessrc/distributed/pipeline/partition_balance.rs:126and the other slice receivers. But its catch rate against the seven known instances is 0 of 7: all five pre-fix trees produced those same four flags and nothing else, because none of the seven usedmin_by_keyormax_by_key. Its four hits are also lint true positives and not live defects: every key is astd::time::Instantstamped once per call, so at nanosecond resolution the tie is not reachable. Gating on it would mean suppressing 4 of 4 findings on the day it landed, and a suppression comment does not re-arm on the change that would make the pattern real (the key becoming coarser), which is the only scenario in which it pays.A
Vecbuilt from an unordered-map view and consumed order-sensitively. 22 flags on the current tree. It flags all three #1286 sites after PR #1288 fixed them, because the fix keeps the shape and changes only whether the sort key is total, which is a distinction no regex can draw. Its 3-of-7 catch rate held only against pre-fix trees; on a tree where the class has been fixed those three become permanent false positives, so it can never be a gate. It reaches none of the four separately filed instances, each for a different structural reason.A tree-wide scan for
type X = HashMap<...>does remove the type-alias blind spot the issue names as the biggest recall limit (it resolvesWeightMap,MemoandDetachedMap), so that one limit is fixable. The other three are not at this level of analysis, and the general form is undecidable from source alone because the consumer is often in another module. Both numbers and the reasoning are recorded in the Enforcement block so the next person does not re-derive them.Test plan
make verify-fmtexits 0 (docs-only change; no Rust or build file is touched, and no script was added, so no other gate applies).grep -n '^## ' docs/code-guidelines.mdshows a fourth section,HashMap Iteration Orderat line 109, with the existing three unmoved.grep -n 'code-guidelines' docs/README.md CONTRIBUTING.md .github/PULL_REQUEST_TEMPLATE.mdexits 0; the index entry atdocs/README.md:36now names the new section.../scripts/ci/check_kernel_dtype_keys.pyresolves, matching the JIT section's existing link.Makefile:604-616line range and the 27-of-64 figure cited in the section was checked against the tree or the PR body rather than copied from the issue.Note on the issue body
The issue describes #1286 as open and its three instances as "still unfixed at
main". They were fixed by PR #1288, merged after the body's last edit, andmainat9f4f6516is that merge. The section is written against the fixed state, and that is what makes the second candidate rule's limitation visible.Closes #1287