Skip to content

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

Merged
merged 3 commits into from
Nov 7, 2021

Conversation

jtgeibel
Copy link
Member

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

@Turbo87 Turbo87 added A-backend ⚙️ C-internal 🔧 Category: Nonessential work that would make the codebase more consistent or clear labels Aug 19, 2021
src/app.rs Outdated
Comment on lines 37 to 39
/// 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>,
Copy link
Member

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.

Copy link
Member Author

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.


match app
.version_id_cacher
.entry(format!("{}:{}", crate_name, version))
Copy link
Member

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.

Copy link
Member Author

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.

@pietroalbini
Copy link
Member

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 fetched_at timestamp in the cache value, and if it's older than the TTL then ignore the cached value and fetch from the database again. This shouldn't make the code too more complex and shouldn't use too much memory either.

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.

@bors
Copy link
Contributor

bors commented Aug 22, 2021

☔ The latest upstream changes (presumably #3858) made this pull request unmergeable. Please resolve the merge conflicts.

@Turbo87
Copy link
Member

Turbo87 commented Sep 1, 2021

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?

@Turbo87 Turbo87 force-pushed the cache-version-id-for-downloads branch from 284565d to 96309ba Compare September 1, 2021 13:28
@Turbo87
Copy link
Member

Turbo87 commented Sep 1, 2021

@jtgeibel FYI I've rebased the branch and extracted the test code refactoring into a second (or rather: first) commit

@bors
Copy link
Contributor

bors commented Sep 3, 2021

☔ The latest upstream changes (presumably #3884) made this pull request unmergeable. Please resolve the merge conflicts.

@Turbo87 Turbo87 force-pushed the cache-version-id-for-downloads branch from 96309ba to 7d378c8 Compare October 11, 2021 12:17
@Turbo87
Copy link
Member

Turbo87 commented Oct 11, 2021

rebased once more and resolved the conflict with #3884

@Turbo87 Turbo87 force-pushed the cache-version-id-for-downloads branch from 49b83d3 to 550d810 Compare October 11, 2021 17:07
@Turbo87 Turbo87 force-pushed the cache-version-id-for-downloads branch 7 times, most recently from b9b0305 to 5d0ec1c Compare October 25, 2021 13:39
@Turbo87 Turbo87 force-pushed the cache-version-id-for-downloads branch 4 times, most recently from 2e7b6d3 to 5ff528b Compare November 1, 2021 23:04
@Turbo87 Turbo87 force-pushed the cache-version-id-for-downloads branch 3 times, most recently from f2d2dd8 to 72240d3 Compare November 6, 2021 10:35
jtgeibel and others added 3 commits November 7, 2021 12:43
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.
@Turbo87 Turbo87 force-pushed the cache-version-id-for-downloads branch from 72240d3 to 7c99034 Compare November 7, 2021 11:44
bors added a commit that referenced this pull request Nov 7, 2021
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.
@bors bors merged commit 7c99034 into rust-lang:master Nov 7, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-backend ⚙️ C-internal 🔧 Category: Nonessential work that would make the codebase more consistent or clear
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants