Fixes an infinite loop in ConflictSet::Impl::setOldestVersion (hash_table)
that occurs when the garbage-collection pass erases every entry in the map.
Root cause
Each write charges keyUpdates += 2, and GC runs once keyUpdates >= 100.
The inner for loop decrements keyUpdates by exactly 1 per entry visited.
When every entry has version <= oldestVersion, all entries are erased, but keyUpdates was charged at 2x the entry count, so it remains > 0 after the
map is emptied. On the next while (keyUpdates > 0) iteration, iter is reset
to map.begin(), which is end() for an empty map; the for loop body never
executes and keyUpdates is never decremented again, so the loop spins forever.
Fix
Break out of the while loop when the map is exhausted (i.e. when map.begin() == map.end()), instead of unconditionally re-entering the for
loop on an empty map. This matches the suggested fix in the issue.
Test
Added test_hash_table_setOldestVersion_empties_map, which performs 50 point
writes at version 1 (so keyUpdates = 100, enabling GC) and then calls setOldestVersion(1), which erases all entries. The test asserts the call
returns and verifies subsequent read results. Without the fix this test hangs
(confirmed by temporarily reverting the fix); with the fix it passes, and the
full CTest suite (8600 tests) passes.
Fixes an infinite loop in `ConflictSet::Impl::setOldestVersion` (hash_table)
that occurs when the garbage-collection pass erases every entry in the map.
## Root cause
Each write charges `keyUpdates += 2`, and GC runs once `keyUpdates >= 100`.
The inner `for` loop decrements `keyUpdates` by exactly 1 per entry visited.
When every entry has `version <= oldestVersion`, all entries are erased, but
`keyUpdates` was charged at 2x the entry count, so it remains `> 0` after the
map is emptied. On the next `while (keyUpdates > 0)` iteration, `iter` is reset
to `map.begin()`, which is `end()` for an empty map; the `for` loop body never
executes and `keyUpdates` is never decremented again, so the loop spins forever.
## Fix
Break out of the `while` loop when the map is exhausted (i.e. when
`map.begin() == map.end()`), instead of unconditionally re-entering the `for`
loop on an empty map. This matches the suggested fix in the issue.
## Test
Added `test_hash_table_setOldestVersion_empties_map`, which performs 50 point
writes at version 1 (so `keyUpdates = 100`, enabling GC) and then calls
`setOldestVersion(1)`, which erases all entries. The test asserts the call
returns and verifies subsequent read results. Without the fix this test hangs
(confirmed by temporarily reverting the fix); with the fix it passes, and the
full CTest suite (8600 tests) passes.
Closes #78
CI / test (arm64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 22, https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst, debug-arm64, ubuntu-latest-arm64) (pull_request) Successful in 3m46s
CI / test (amd64, -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan, 21, https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst, debug, ubuntu-latest-amd64) (pull_request) Successful in 3m44s
setOldestVersion's GC pass charges keyUpdates at 2x the entry count, so
when every entry has version <= oldestVersion and is erased, keyUpdates
remains > 0 while the map is empty. The outer while(keyUpdates > 0) loop
then reset iter to map.begin() (== end() for an empty map) and the inner
for loop never executed, spinning forever.
Break out of the while loop when the map is exhausted, so the call
returns normally.
Closes#78
andrew
merged commit af76c4d623 into main2026-08-17 00:25:32 +00:00
andrew
deleted branch weaselbot/issue-782026-08-17 00:25:33 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes an infinite loop in
ConflictSet::Impl::setOldestVersion(hash_table)that occurs when the garbage-collection pass erases every entry in the map.
Root cause
Each write charges
keyUpdates += 2, and GC runs oncekeyUpdates >= 100.The inner
forloop decrementskeyUpdatesby exactly 1 per entry visited.When every entry has
version <= oldestVersion, all entries are erased, butkeyUpdateswas charged at 2x the entry count, so it remains> 0after themap is emptied. On the next
while (keyUpdates > 0)iteration,iteris resetto
map.begin(), which isend()for an empty map; theforloop body neverexecutes and
keyUpdatesis never decremented again, so the loop spins forever.Fix
Break out of the
whileloop when the map is exhausted (i.e. whenmap.begin() == map.end()), instead of unconditionally re-entering theforloop on an empty map. This matches the suggested fix in the issue.
Test
Added
test_hash_table_setOldestVersion_empties_map, which performs 50 pointwrites at version 1 (so
keyUpdates = 100, enabling GC) and then callssetOldestVersion(1), which erases all entries. The test asserts the callreturns and verifies subsequent read results. Without the fix this test hangs
(confirmed by temporarily reverting the fix); with the fix it passes, and the
full CTest suite (8600 tests) passes.
Closes #78