Skip to content
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

Fix memory leak in get_with and friend methods of future::Cache #330

Merged
merged 5 commits into from
Oct 3, 2023

Conversation

tatsuya6502
Copy link
Member

@tatsuya6502 tatsuya6502 commented Oct 2, 2023

This PR fixes #329, a bug introduced to v0.12.0 causing memory leak in get_with(), entry().or_insert_with() and friend methods in future::Cache.

It also bumps the crate version to v0.12.1.

Details of the bug

The bug was introduced by PR #294 for v0.12.0 (src/future/value_initializer.rs diff L316-L330). So this PR basically reverts the change.

Here are some backgrounds. An instance of future::Cache has a few internal concurrent hash tables (cht), and the following tables are related to this bug:

  1. The main cht called cache map, which stores cached entries.
  2. A cht called waiter map, which stores per-key reader-writer locks used by get_with and friends methods.

The key of cache map is Arc<K>, and the key of waiter map is (Arc<K>, TypeId). These internal maps take a hash function at creation, and both cache map and waiter map are given the same hash function.

Also, these maps take hash value for the key when inserting, getting, or removing an entry (a key-value pair). The hash value is supposed to be calculated from the key by the hash function. However, the bug made get_with etc. to use a hash value for cache map for accessing waiter map. Since their keys are different, the hash value are also different.

Using a wrong hash value still works as long as waiter map is using the same memory region where the entry was inserted to. However, once the memory region gets half full with live entries and deleted keys, waiter map will allocate a new memory region and move live entries to it, using the correct hash values for the keys. Once this happens, get_with etc. (who use wrong hash) cannot find entries in waiter map anymore, leaving them in the map forever. (memory leak)

friends methods are used

- Use the correct hash value for an internal waiters map in `future::Cache`.
- Also rename variable names for hash values in `sync::Cache` to prevent similar
  bugs.
@tatsuya6502 tatsuya6502 self-assigned this Oct 2, 2023
@tatsuya6502 tatsuya6502 added the bug Something isn't working label Oct 2, 2023
@tatsuya6502 tatsuya6502 added this to the v0.12.1 milestone Oct 2, 2023
Copy link
Contributor

@Swatinem Swatinem left a comment

Choose a reason for hiding this comment

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

Thanks for finding and fixing this issue so quickly.

From an outsiders perspective, I’m wondering why you are passing these two key/hash pairs explicitly, instead of maybe wrapping them in two different newtypes, so the typesystem enforced that you are using the correct key/hash pair.

I’m also curious why it is necessary to use a TypeId here as well in the first place? I will have to dig a bit deeper and learn more of the inner working of moka :-)

@tatsuya6502
Copy link
Member Author

From an outsiders perspective, I’m wondering why you are passing these two key/hash pairs explicitly, instead of maybe wrapping them in two different newtypes, so the typesystem enforced that you are using the correct key/hash pair.

Hi. Thank you for the suggestion. Maybe we can change the hash type from bare u64 to something like HashVal<K>(u64, PhantomData<K>) where K is the key type of cht? I will think about it when I have more time.

The reason that we are passing hash explicitly is that a Cache instance has multiple internal data structures using the same hash, and we want to avoid to calculate the hash more than once.

For example, moka::sync::SegmentedCache uses the same hash for a key in the following purposes:

  • To select a Cache segment to store the entry.
  • To select a hash table segment in the cht (moka::cht::SegmentedHashMap)
  • To select a hash table bucket in the hash table segment.
  • To set or get counter bits in the popularity estimator (FrequencySketch).

I’m also curious why it is necessary to use a TypeId here as well in the first place?

You may have already seen the answer when creating other pull request, but TypeId is for the E of the return type Result<V, Arc<E>> of the or_try_insert_with method. This TypeId is used in a key to group together the or_try_insert_with calls having the same E.

@tatsuya6502 tatsuya6502 added this pull request to the merge queue Oct 3, 2023
Merged via the queue into main with commit b3074fc Oct 3, 2023
36 checks passed
@tatsuya6502 tatsuya6502 deleted the fix-get-with-in-future-cache branch October 3, 2023 01:47
@tatsuya6502 tatsuya6502 restored the fix-get-with-in-future-cache branch October 3, 2023 01:52
@tatsuya6502 tatsuya6502 deleted the fix-get-with-in-future-cache branch October 3, 2023 03:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Memory leak in moka 0.12
2 participants