Skip to content

fix(aich): true linear-scan fallback when LoadHashSet cache offset is stale - #191

Merged
got3nks merged 1 commit into
amule-org:masterfrom
got3nks:fix/aich-loadhashset-defensive-rewind
Jun 17, 2026
Merged

fix(aich): true linear-scan fallback when LoadHashSet cache offset is stale#191
got3nks merged 1 commit into
amule-org:masterfrom
got3nks:fix/aich-loadhashset-defensive-rewind

Conversation

@got3nks

@got3nks got3nks commented Jun 17, 2026

Copy link
Copy Markdown

Summary

Follow-up to #186, addressing point 1 of @danim7's post-merge review.

PR #186 added an offset cache so LoadHashSet seeks straight to the matching entry in known2.met instead of walking it linearly. The previous inline comment claimed that on a first-read mismatch we'd "fall through to the linear scan from the start as defensive recovery". @danim7 pointed out — correctly — that the loop actually keeps reading from the cached offset onward and just skips nHashCount * HASHSIZE bytes on each mismatch. If the cache is stale (known2.met modified externally between cache load and request), the offset could point into the middle of a hash blob; we'd misread CurrentHash, misread nHashCount, and the loop would walk off into garbage and ultimately return false even though the entry still exists somewhere in the file.

What this changes

In LoadHashSet:

  • On the first iteration after seeking to the cached offset, if CurrentHash doesn't match m_pHashTree.m_Hash, treat the cache as stale, rewind to byte 1 (just past the version header), and continue with a true linear scan from the top. Guarded by cacheFallbackTriggered so the rewind can fire at most once per call.
  • If the cached offset is >= file.GetLength() (file was truncated externally), skip the seek entirely and fall straight to the linear scan.

In the happy path (cache correct), behaviour is unchanged: the first read matches and the existing match handler runs.

Verification

  • Local Mac build clean.
  • PR AICH: rate-limit OP_AICHREQUEST + O(1) LoadHashSet via offset cache #186's existing verify-pr186-aich.py still passes (single-file happy path).
  • New verify-loadhashset-multi-file.py (in scripts/, not part of this PR): boots amuled with two shared files of different sizes, waits for both AICH trees to land in known2_64.met, then sends two OP_AICHREQUESTs (one per file) on a single connection within the rate-limit budget. Both files resolve to non-empty OP_AICHANSWER payloads (152 B for file A, 306 B for file B), confirming the cache correctly returns distinct offsets per entry.

The stale-cache rewind path itself is not directly exercised by these tests — reaching it requires external modification of known2.met between cache load and request, which isn't reachable from a black-box harness without an instrumentation hook. The code path is small (~20 lines) and reviewable on inspection.

Test plan

  • CI to verify the other targets.

… stale

PR amule-project#186 added an offset cache so LoadHashSet seeks straight to the
matching entry instead of linearly walking known2.met. The previous
comment claimed that on a first-read mismatch we'd "fall through to
the linear scan from the start as defensive recovery" — but the loop
actually keeps reading from the cached offset onward, treating
whatever bytes are at that position as a root hash + hashCount. If
the cache is stale (known2.met modified externally between cache
load and the request) the offset can land in the middle of a hash
blob; we'd misread CurrentHash, misread nHashCount, and the loop
would misalign and ultimately return false even though the entry
still exists somewhere in the file.

Reported by @danim7 in amule-org#186.

Add a real defensive rewind: on the first iteration after seeking to
the cached offset, if CurrentHash doesn't match our root hash, treat
the cache as stale, jump back to just past the version header
(byte 1), and continue with a true linear scan from the top. Cap the
rewind to one attempt with cacheFallbackTriggered so we can't loop.
Also handle the corner case where the cached offset is past EOF (file
was truncated externally): skip the seek entirely and fall straight
through to the linear scan.

In the happy path (cache offset correct), behavior is unchanged: the
first read matches and we return true after the existing match
handler.
@got3nks
got3nks merged commit 618544d into amule-org:master Jun 17, 2026
10 checks passed
@got3nks
got3nks deleted the fix/aich-loadhashset-defensive-rewind branch June 17, 2026 13:26
got3nks added a commit that referenced this pull request Jun 17, 2026
PR #191 added a defensive rewind so LoadHashSet finds the right entry
even when the cached offset is stale (known2.met modified externally
between cache load and the request). The rewind worked but didn't
update the cache: every subsequent LoadHashSet for the same stale
root hash kept paying the seek-miss + full linear scan, instead of
the O(1) cache hit the cache exists to provide. The cache only
self-cleared on daemon restart, on CEOFException-driven truncation,
or after an orphan-prune rewrite in CAICHSyncTask -- none of which
fire just because a single entry's offset went stale.

When the rewind succeeds, the linear scan has just found the right
position for the root hash. Stamp it back into s_rootHashCache so
future lookups for the same hash go straight there.

Capture the entry's start position pre-read (entryStartPos) so we
have the exact offset of the root hash, matching the value
LoadRootHashCacheLocked and SaveHashSet write into the cache.
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