-
Notifications
You must be signed in to change notification settings - Fork 645
Cache the version_id for download requests #3848
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
Cache the version_id for download requests #3848
Conversation
src/app.rs
Outdated
/// Cache the `version_id` of a `canonical_crate_name:semver` pair | ||
/// | ||
/// This is used by the download endpoint to reduce the number of database queries. The | ||
/// `version_id` is only cached under the canonical spelling of the crate name. | ||
pub(crate) version_id_cacher: DashMap<String, i32>, |
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.
We can support serving responses for non-canonical spellings with little effort:
struct CachedVersion {
canonical_crate_name: String,
version_id: i32,
}
version_id_cacher: DashMap<String, CachedVersion>
Then the cache key could be normalized with crate_name.lower().replace('_', '-')
, while still having access to the canonical crate name to perform the redirect.
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 was originally storing the canonical crate name as well, but ended up dropping that to minimize memory usage, especially since there are so few non-canonical requests.
I'm not actually that worried about memory usage, so this approach does sound reasonable to me as well. Maybe it's worth looking at memory usage in production first, and then adding this if we're sure we have plenty of room to spare.
src/controllers/version/downloads.rs
Outdated
|
||
match app | ||
.version_id_cacher | ||
.entry(format!("{}:{}", crate_name, version)) |
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.
DashMap::entry
acquires a write lock, even if the entry is present in the map. This would cause every request to do a write lock on the assigned shard, which we should try to avoid. We should do something similar to DownloadsCounter, where we first call .get
to try and use the read lock and only if it fails call .entry
.
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.
Excellent point, I'll switch to .get
.
I did some experiments and this should use at most 10-20 MB with the current set of crates, so this should be fine for the foreseeable future for memory usage. Regarding cache expiration, I'd prefer to actually have some cache expiration built in with a relatively short TTL (1-5 minutes?), to limit the effect of deleted crates. We could just add a There should also eventually be some automatic cleanup in place to ensure the map doesn't grow too much, but that can be done in a later PR. Deleting a random shard whenever we reach more than N items in the cache should be enough, without adding LRU logic. It would also be interesting to have a "cache hit" metric, especially if we implement some TTL. |
☔ The latest upstream changes (presumably #3858) made this pull request unmergeable. Please resolve the merge conflicts. |
I just discovered https://github.com/moka-rs/moka, which sounds like it might be useful for this use case. Would it make sense to use that instead of building our own cache? |
284565d
to
96309ba
Compare
@jtgeibel FYI I've rebased the branch and extracted the test code refactoring into a second (or rather: first) commit |
☔ The latest upstream changes (presumably #3884) made this pull request unmergeable. Please resolve the merge conflicts. |
96309ba
to
7d378c8
Compare
rebased once more and resolved the conflict with #3884 |
49b83d3
to
550d810
Compare
b9b0305
to
5d0ec1c
Compare
2e7b6d3
to
5ff528b
Compare
f2d2dd8
to
72240d3
Compare
…ount()` to proper functions
This adds a caching layer in the app backend to reduce the number of database queries. While this query is extremely fast, a massive number of concurrent download requests can still result in a request backlog. In this implementation cached values are never dropped. This means that memory usage will grow over time, however the memory usage is expected to be well within our instance size and the application is automatically rebooted at least once every 24 hours. Additionally, on the rare occasion that a crate version is deleted, the cache will continue to serve redirects and attempt to record download counts for that version. If a version is deleted and continues to see download requests, it is expected that the entire shard of downloads containing that version_id will fail to persist. Even if we expired cached entries after a certain time period, there would still be a window where a deleted version could cause download counts for other versions to not be persisted. It is possible we could make the download counter logic robust against this scenario, but given that deleting versions is rare we can mitigate this by restarting the app after deleting crates or versions.
72240d3
to
7c99034
Compare
Implement `version_id` cache for `download` endpoint using `moka` crate This PR builds on top of #3848, but uses https://github.com/moka-rs/moka instead of https://github.com/xacrimon/dashmap for caching the `version_id` in the `download` API endpoint. I've implemented it using environment variables and default values that will allow us to fine tune the values in production, if necessary.
This adds a caching layer in the app backend to reduce the number of
database queries. While this query is extremely fast, a massive number
of concurrent download requests can still result in a request backlog.
In this implementation cached values are never dropped. This means
that memory usage will grow over time, however the memory usage is
expected to be well within our instance size and the application is
automatically rebooted at least once every 24 hours. Additionally, on
the rare occasion that a crate version is deleted, the cache will
continue to serve redirects and attempt to record download counts for
that version.
If a version is deleted and continues to see download requests, it is
expected that the entire shard of downloads containing that version_id
will fail to persist. Even if we expired cached entries after a certain
time period, there would still be a window where a deleted version
could cause download counts for other versions to not be persisted. It
is possible we could make the download counter logic robust against this
scenario, but given that deleting versions is rare we can mitigate this
by restarting the app after deleting crates or versions.
r? @pietroalbini