Fix hash_table setOldestVersion infinite loop on empty map #79

Merged
andrew merged 1 commits from weaselbot/conflict-set:weaselbot/issue-78 into main 2026-08-17 00:25:32 +00:00
Member

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

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
weaselbot added 1 commit 2026-08-16 17:03:05 +00:00
Fix hash_table setOldestVersion infinite loop on empty map
CI / pre-commit (pull_request) Successful in 2m2s
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
CI / release (arm64, , ubuntu-latest-arm64) (pull_request) Successful in 3m19s
CI / test (amd64, -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 21, , 64-bit-versions, ubuntu-latest-amd64) (pull_request) Successful in 3m16s
CI / test (amd64, -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, 21, , gcc, ubuntu-latest-amd64) (pull_request) Successful in 3m9s
CI / test (amd64, -DUSE_SIMD_FALLBACK=ON, 21, , simd-fallback, ubuntu-latest-amd64) (pull_request) Successful in 3m15s
CI / release (amd64, -DMSAN_TOOLCHAIN_PATH=/opt/msan, ubuntu-latest-amd64) (pull_request) Successful in 5m30s
CI / coverage (pull_request) Successful in 3m40s
6db080a34c
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 main 2026-08-17 00:25:32 +00:00
andrew deleted branch weaselbot/issue-78 2026-08-17 00:25:33 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: weaselab/conflict-set#79