Skip to content

fix: Range/RangeBackwards no longer skip items reordered mid-iteration#213

Open
amitmishra11 wants to merge 1 commit into
jellydator:v3from
amitmishra11:fix/range-skips-reordered-items
Open

fix: Range/RangeBackwards no longer skip items reordered mid-iteration#213
amitmishra11 wants to merge 1 commit into
jellydator:v3from
amitmishra11:fix/range-skips-reordered-items

Conversation

@amitmishra11

Copy link
Copy Markdown

This PR was written primarily by Claude Code; I reviewed the change and ran the test suite before submitting.

Problem

As described in #205, Range/RangeBackwards walk the live LRU list while releasing the read lock between steps. A concurrent Set/Touch call in that window can call MoveToFront, moving an item the iterator hasn't visited yet ahead of the current position, so it gets skipped entirely.

Approach

@swithek noted in the issue thread that fully cloning the cache to avoid this would double its memory footprint, and @d1lmr0d suggested snapshotting keys instead. This PR takes that approach: instead of walking the live list, it takes a snapshot of keys (in current iteration order) up front under the read lock, then looks each key up again as it goes, skipping any that have since been evicted/deleted. This is the same number of lock acquisitions as before, but the iteration order is now fixed by the key snapshot rather than by list links that can move underneath it, so a concurrent reorder can no longer cause skips. The extra cost is one []K slice (just keys, not full items), not a clone of the cache.

I'm not certain this is the direction you'd want to take it, given the issue is still open with no agreed approach. Happy to adjust if you'd prefer something else (e.g. documenting the existing semantics instead of changing them, or a different consistency guarantee).

Changes

  • cache.go: extracted snapshotKeysUnsafe (collects keys in list order under the read lock) and rangeKeys (re-looks-up each key, skips ones no longer present, calls fn outside the lock); Range and RangeBackwards now use these instead of walking the live list directly.
  • cache_test.go: added Test_Cache_Range_VisitsItemsReorderedDuringIteration, reproducing the exact repro from Range() returns inconsistent results due to race condition with Set/Touch #205 (Set on an unvisited key from within the Range callback). Verified it fails against the current code and passes with this fix.

Testing

go test ./... passes (a few timer-based tests — Test_Cache_Get, Test_Cache_OnInsertion, Test_Cache_OnUpdate — are pre-existing flakes unrelated to this change; confirmed by running the full suite against unmodified v3 as well). Could not run with -race in my sandbox (only a 32-bit MinGW gcc is available, which can't build cgo's race detector), but the bug itself is deterministic single-threaded reentrancy (calling Set from within the Range callback), not a true data race, so the new test exercises it directly without needing -race.

Fixes #205

Range and RangeBackwards walked the live LRU list while releasing the
read lock between steps, so a concurrent Set/Touch could call
MoveToFront and move an unvisited item ahead of the iterator,
skipping it. Snapshot the keys in iteration order up front, then
re-look-up each key (skipping ones since evicted) instead of walking
the live list.

Fixes jellydator#205

This PR was written primarily by Claude Code; I reviewed the change
and ran the test suite before submitting.
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 28315180578

Coverage increased (+0.001%) to 99.739%

Details

  • Coverage increased (+0.001%) from the base build.
  • Patch coverage: 27 of 27 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 766
Covered Lines: 764
Line Coverage: 99.74%
Coverage Strength: 26.09 hits per line

💛 - Coveralls

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.

Range() returns inconsistent results due to race condition with Set/Touch

2 participants