Skip to content

Conversation

@rainsupreme
Copy link
Contributor

This makes it safe to delete hashtable while a safe iterator is iterating it. This currently isn't possible, but this improvement is required for fork-less replication #1754 which @JimB123 is actively working on.

We discussed these issues in #2611 which guards against a different but related issue: calling hashtableNext again after it has already returned false.

I implemented a singly linked list that hashtable uses to track its current safe iterators. It is used to invalidate all associated safe iterators when the hashtable is released. A singly linked list is acceptable because the list length is always very small - typically zero and no more than a handful.

…able deleted from under safe iterator

Signed-off-by: Rain Valentine <[email protected]>
@rainsupreme rainsupreme force-pushed the safe-iterator-tracking branch from d67a6b7 to 56db3fd Compare November 5, 2025 21:59
@codecov
Copy link

codecov bot commented Nov 5, 2025

Codecov Report

❌ Patch coverage is 80.26316% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.45%. Comparing base (c88c94e) to head (a1497a5).
⚠️ Report is 21 commits behind head on unstable.

Files with missing lines Patch % Lines
src/hashtable.c 82.35% 6 Missing ⚠️
src/rdb.c 33.33% 4 Missing ⚠️
src/vset.c 33.33% 2 Missing ⚠️
src/aof.c 66.66% 1 Missing ⚠️
src/defrag.c 0.00% 1 Missing ⚠️
src/module.c 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #2807      +/-   ##
============================================
+ Coverage     72.43%   72.45%   +0.01%     
============================================
  Files           128      128              
  Lines         70245    70400     +155     
============================================
+ Hits          50880    51005     +125     
- Misses        19365    19395      +30     
Files with missing lines Coverage Δ
src/acl.c 90.30% <100.00%> (-0.29%) ⬇️
src/debug.c 54.59% <100.00%> (ø)
src/kvstore.c 95.91% <100.00%> (ø)
src/latency.c 80.97% <100.00%> (ø)
src/object.c 81.94% <100.00%> (ø)
src/pubsub.c 97.19% <100.00%> (ø)
src/server.c 88.45% <ø> (-0.03%) ⬇️
src/sort.c 94.78% <100.00%> (ø)
src/t_hash.c 96.15% <100.00%> (-0.09%) ⬇️
src/t_set.c 97.82% <100.00%> (ø)
... and 8 more

... and 15 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Signed-off-by: Rain Valentine <[email protected]>
Copy link
Member

@JimB123 JimB123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad to see a proposal for this.

src/hashtable.c Outdated
Comment on lines 1119 to 1124
iter *it = ht->safe_iterators;
while (it) {
it->hashtable = NULL; /* Mark as invalid */
it = it->next_safe_iter;
}
ht->safe_iterators = NULL;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

while (ht->safe_iterators) untrackSafeIterator(ht->safe_iterators);

Ensures that the cleanup/invalidation is identical (like setting next_safe_iter to NULL, which was missed here).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that's much better. Very DRY

iter->index = -1;
iter->flags = flags;
iter->next_safe_iter = NULL;
if (isSafe(iter) && ht != NULL) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can ht be NULL here? Maybe put an assertion at the top if you want to check this.

Copy link
Contributor Author

@rainsupreme rainsupreme Nov 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kvstore's iterator contains a hashtableIterator, and in kvstoreIteratorInit() they want to init their hashtableIterator to something and redo it later when they decide which hashtable to actually start with. Or that was my impression. I discovered this from a failed tcl test 😅

src/hashtable.c Outdated
}

/* Resets a stack-allocated iterator. */
void hashtableResetIterator(hashtableIterator *iterator) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not clear on what this function is trying to do. I see that it can call resumeRehashing, but is that the extent of "reset"? I'm expecting that this would essentially rewind the iterator, allowing me to begin iteration again. However, it's not resetting index or table. Is this right?

Also, how is this different than hashtableReinitIterator (above)? It looks wrong to me that the reinit function above isn't checking if the iterator needs to be untracked - it just NULLs out next_safe_iterator. This looks like an issue.

Copy link
Contributor Author

@rainsupreme rainsupreme Nov 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I've fixed that, and I'm also renaming (which accounts for all the extra files changed):

  • hashtableReinitIterator -> hashtableRetargetIterator
  • hashtableResetIterator -> hashtableCleanupIterator

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.

2 participants