-
Notifications
You must be signed in to change notification settings - Fork 955
Fix leak when shrinking a hashtable without entries #2288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Signed-off-by: yzc-yzc <[email protected]>
enjoy-binbin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see we do the same condition in the dict.c, so it should be fine. I did not dive into the details
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## unstable #2288 +/- ##
============================================
+ Coverage 71.46% 71.49% +0.02%
============================================
Files 123 123
Lines 66927 66941 +14
============================================
+ Hits 47831 47857 +26
+ Misses 19096 19084 -12
🚀 New features to boost your workflow:
|
zuiderkwast
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great finding!
I have a minor suggestion. If I'm wrong, we can merge it as is.
I tested it locally and noticed it is slow. It takes 15 seconds. I have modified it to remove the sleeps and some loops. Now it takes 160ms. You can add it if you want: start_server {tags {expire} overrides {hz 100}} {
test {Active expiration triggers hashtable shrink} {
set persistent_keys 5
set volatile_keys 100
set total_keys [expr $persistent_keys + $volatile_keys]
for {set i 0} {$i < $persistent_keys} {incr i} {
r set "key_$i" "value_$i"
}
for {set i 0} {$i < $volatile_keys} {incr i} {
r psetex "expire_key_${i}" 100 "expire_value_${i}"
}
set table_size_before_expire [main_hash_table_size]
# Verify keys are set
assert_equal $total_keys [r dbsize]
# Wait for active expiration
wait_for_condition 100 50 {
[r dbsize] eq $persistent_keys
} else {
fail "Keys not expired"
}
# Wait for the table to shrink and active rehashing finish
wait_for_condition 100 50 {
[main_hash_table_size] < $table_size_before_expire
} else {
puts [r debug htstats 9]
fail "Table didn't shrink"
}
# Verify server is still responsive
assert_equal [r ping] {PONG}
}
} |
Co-authored-by: Viktor Söderqvist <[email protected]> Signed-off-by: yzc-yzc <[email protected]>
@zuiderkwast The above two questions are bothering me, could you help me out? Thanks! |
@yzc-yzc That's right. It is used internally in hashtable.c for safety reasons but in the public API it should not be required for safety.
For safety of scan, I think it's OK to resize but not rehash during the scan. For example if the scan callback deletes entries. Resize only allocates a new table but no entries are moved, so it doesn't affect the scan algorithm. However, if new entries are inserted while rehashing is paused (for example the scan callback inserts new entries, or in another situation where the rehashing is paused) they are inserted in the new table. If a lot of new entries are inserted, more than what can fit in the old table, I think it's good that resize is enabled even if rehashing is paused. Makes sense? |
|
I also went through a similar debugging process that @yzc-yzc described, and I want to suggest an alternative way to handle this edge case which might be more efficient since you won't have to do any extra rehash steps. The chained buckets are not released since According to the comment, in that case the compaction should be handles by the scan or iterator. For some reason we only call |
@gusakovy Why is this useful? If rehashing is not paused, the compaction happens immediately when an entry is deleted. Am I missing something? There is another scenario that can lead to holes and empty buckets: If the scan callback deletes some other entry which is not in the same bucket that was just scanned. Also if rehashing was paused and entries were deleted (without scan or iterator) we can get empty buckets. Therefore, I think @yzc-yzc's fix is still needed. |
During scan we pause rehashing so In
If that is possible then yes definitely the proposed fix is still needed. |
|
@gusakovy Gotcha. Rehashing paused when rehashing is not ongoing still means that compaction doesn't happen automatically. Your fix is good. It's not required for fixing the leak but it's needed to clean up empty buckets during scan. For example if many entries are expired, it can cause a lot of empty buckets. @yzc-yzc Can you include @gusakovy's patch in #2288 (comment) above? |
got it, thanks!
sure |
I ran this test on my PC and found that the probability of triggering leak is very low.(Triggered once after 257 times) |
This is not for testing the leak. The main point is for testing that Active expiration triggers hashtable shrink. It's what was actually implemented in #2257. The original test case was too slow IMO. I think we don't really need a special test case to trigger the leak. If you think we need one, we should try to write a unit test in |
…nction. Written by Yakov Gusakov. Co-authored-by: Yakov Gusakov <[email protected]> Signed-off-by: yzc-yzc <[email protected]>
Written by Viktor Söderqvist Co-authored-by: Viktor Söderqvist <[email protected]> Signed-off-by: yzc-yzc <[email protected]>
Signed-off-by: yzc-yzc <[email protected]>
Fixes valkey-io#2271 When we shrink a hash table and it is empty, we do it without iterating over it to rehash the entries. However, there may still be empty child buckets (`used[0]==0 && child_buckets[0]!=0`). These were leaked in this case. This fix is to check for child buckets and don't skip the incremental rehashing if any child buckets exist. The incremental rehashing pass will free them. An additional fix is to compact bucket chains in scan when the scan callback has deleted some entries. This was already implemented for the case when rehashing is ongoing but it was missing in the case rehashing is not ongoing. Additionally, a test case for valkey-io#2257 was added. --------- Signed-off-by: yzc-yzc <[email protected]> Co-authored-by: Viktor Söderqvist <[email protected]> Co-authored-by: Yakov Gusakov <[email protected]>
Fixes #2271 When we shrink a hash table and it is empty, we do it without iterating over it to rehash the entries. However, there may still be empty child buckets (`used[0]==0 && child_buckets[0]!=0`). These were leaked in this case. This fix is to check for child buckets and don't skip the incremental rehashing if any child buckets exist. The incremental rehashing pass will free them. An additional fix is to compact bucket chains in scan when the scan callback has deleted some entries. This was already implemented for the case when rehashing is ongoing but it was missing in the case rehashing is not ongoing. Additionally, a test case for #2257 was added. --------- Signed-off-by: yzc-yzc <[email protected]> Co-authored-by: Viktor Söderqvist <[email protected]> Co-authored-by: Yakov Gusakov <[email protected]>
Fixes #2271
When we shrink a hash table and it is empty, we do it without iterating over it to rehash the entries. However, there may still be empty child buckets (
used[0]==0 && child_buckets[0]!=0). These were leaked in this case.This fix is to check for child buckets and don't skip the incremental rehashing if any child buckets exist. The incremental rehashing pass will free them.
An additional fix is to compact bucket chains in scan when the scan callback has deleted some entries. This was already implemented for the case when rehashing is ongoing but it was missing in the case rehashing is not ongoing.
Additionally, a test case for #2257 was added.