Repository navigation
feat(updater)!: cache update checks in memory - #3639
beanow-at-crabnebula wants to merge 17 commits into
Conversation
Add a shared in-memory cache for update manifest requests, with plugin configuration for disabling caching and limiting response lifetime. Downloads continue to use their existing request path. BREAKING CHANGE: Update checks use HTTP caching by default; network errors from check() are wrapped in Error::ReqwestMiddleware; the updater now requires Rust 1.89.0. Assisted-by: gpt-6-sol
Let Rust and JavaScript callers select the default, bypass, or checkNow policy per update check while retaining the application cache configuration as the default. Assisted-by: gpt-6-sol
Exercise cache lifetimes, conditional revalidation, per-check modes, shared state, and Vary behavior against a local HTTP server. Make existing updater test manifests cacheable. Assisted-by: gpt-6-sol
Document the new cache settings, update the Rust version requirement, and let the API example choose how its user-initiated update check uses the cache. Assisted-by: gpt-6-sol
Hints at the two main places you want to look. (Tauri config and per-request override.) Also sets values in API example as a way to get manual test coverage of this setting.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Package Changes Through 301a33fThere are 4 changes which include opener with patch, opener-js with patch, updater with minor, updater-js with minor Planned Package VersionsThe following package releases are the planned based on the context of changes in this pull request.
Add another change file through the GitHub UI by following this link. Read about change files or the docs at github.com/jbolda/covector |
Switches http-cache-reqwest from our git branch to the 1.0.0-alpha.10 release. Also pins http-cache to 1.0.0-alpha.9 directly. Adds a test that max_ttl still applies after a checkNow revalidation. ### Why Upstream released the default_ttl and revalidation patches we were waiting on. The release also fixes max_ttl being lost after a 304, which let cached responses outlive maxTtlMins after a checkNow. http-cache-reqwest's requirement on http-cache allows newer alphas. Pinning it directly keeps downstream apps on a tested version. The lockfile moves winapi-util back to windows-sys 0.59, matching v2. The git dependency had flipped it to 0.48. The DEFAULT_TTL comment no longer points at the settled upstream issue. ### Testing The new test fails on the old git branch and passes on the release. Resolver v3 produces the same lockfile as v2. Assisted-by: claude-opus-5-5
Merges tauri-apps/plugins-workspace v2 at df59420. Conflicts resolved: - Cargo.lock: took upstream's lock and re-resolved for our dependencies. http goes from 1.3.1 to 1.5.0, http-cache-semantics needs at least 1.4. - updater Cargo.toml: kept the tauri "test" feature and our dev-dependencies. rust-version follows the workspace again, which is now 1.90. - updater README: takes the 1.90 MSRV. - updater.rs: import order only. - e2e update.rs: uses the new serve_update helper, which now responds with the cacheable manifest_response. The http-cache changeset no longer mentions raising MSRV, upstream already raised it past 1.89. ### Testing Updater unit tests, doctests and clippy pass. The e2e test crate builds, and api-iife.js is unchanged after a rebuild. Assisted-by: claude-opus-5-5
Legend-Master
left a comment
There was a problem hiding this comment.
Looks good to me in general, just some small nitpicks
| "json", | ||
| "stream", | ||
| ] } | ||
| reqwest-middleware = "0.5.2" |
There was a problem hiding this comment.
A small question here, how much does this increase the final binary size?
There was a problem hiding this comment.
With the release profile (lto = true, codegen-units = 1, opt-level = "s", panic = "abort")
It seems to add about 304/305 KB, on Linux x86_64.
The majority comes from new crates. And some of it is using more functionality from existing crates.
Rough LLM estimates of the attribution, based on nm results:
| Group | Crates | Δ |
|---|---|---|
| HTTP cache logic | http_cache 45.4K, http_cache_reqwest 27.3K, http_cache_semantics 22.0K, http_serde 5.4K, postcard 2.2K |
~102 KB |
| Moka in-memory cache | moka 58.1K, crossbeam_channel 4.7K, crossbeam_epoch 2.7K, async_lock/event_listener/parking_lot_core 5.1K |
~71 KB |
| Generic std/core code | containers, iterators, formatting and drop glue built for the new types | ~31 KB |
| Existing crates used more | time 14.0K, http 9.1K, hashbrown 6.2K, futures_util 2.1K, soup 3.1K |
~34 KB |
| Updater plugin's own code | tauri_plugin_updater (cache.rs and the new wiring) |
~6 KB |
The other ~53 KB of the code section isn't tied to any symbol: mostly constant data and strings, plus unwind tables. Other sections add about 11 KB, which is how the stripped binary ends up +305 KB.
There was a problem hiding this comment.
Since you commented specifically on reqwest-middleware.
It is in the data, but it fell under the 1.5 KB cutoff I used to keep the table short.
reqwest-middlewarecosts 1,495 bytes in the size-tuned LTO build and 2,939 bytes in the plain release build.
There was a problem hiding this comment.
Depending on how far people are pushing their optimizations that could be significant.
I could look into a cargo feature for this, and that could replace the confusion between disabled and disabled_by_default.
However since we're aiming for default enabled, it would mean people need to use default-features = false. Which may cause it's own friction down the road. Let me know if you want to try this direction.
There was a problem hiding this comment.
I think this would be much easier to understand than the disabled and disabled_by_default configs. Also 300KB seems to be quite significant in terms of how big of an increase in percentage.
There was a problem hiding this comment.
For reference, this is what a custom implementation could look like.
beanow-at-crabnebula/plugins-workspace@beanow/updater-cache...beanow-at-crabnebula:plugins-workspace:beanow/updater-cache-lru
Minor improvements from the previous prototype that was measured (body size cap).
| Build (Linux x86_64, stripped) | vs v2 |
|---|---|
moka (beanow/updater-cache) |
+315,616 B |
QuickManager prototype |
+194,512 B |
| LRU storage | +158,016 B |
| LRU storage + 16 MiB cap (branch head) | +158,144 B |
There was a problem hiding this comment.
For a more complex use-case I wouldn't roll my own, but this seems reasonable to me.
What do you think?
Plus if we do decide that our use-case becomes more complex, switching to the quickmanager option stays a viable path.
There was a problem hiding this comment.
Ideally I don't want to maintain this module ourselves and 30KB sounds ok. Still feel like +200KB is a bit bad for a enabled by default feature. Since we're going with opt-in for now, maybe remove it from the default features?
There was a problem hiding this comment.
I'll take that as going with the QuickManager route.
Yes we could take it out of default features. Though I think that comes with a major downside, if people don't discover this feature it means we get no meaningful feedback during the opt-in period to know it's OK to start enabling by default.
At the very least it'd be good to see this compiles and the middleware in Bypass mode is all good.
There was a problem hiding this comment.
Switched to quick_cache and took another look at what could be done to shave some bytes off the upstream crates. The only easy win I could find was optional cargo features in quick_cache that http-cache-quickcache doesn't expose: 06chaynes/http-cache#180
Good for around 8912 bytes if we can disable that.
Everything else was significantly more invasive upstream.
Removing reqwest-middleware based on our cache feature does have some saving as well, but would change the public Rust API for Error variants depending on what features you selected. Which may not be worth doing.
We could coerce that into a single variant holding Box<dyn std::error::Error + Send + Sync> so it's the same contract regardless of your feature selection.
Doing that saves roughly 8KB when the cache feature is off.
| Build | Current PR | Prototype* | Change |
|---|---|---|---|
| Cache on | +202,296 B | +199,376 B | −2,920 B |
| Cache off | +24,032 B | +15,952 B | −8,080 B |
*= prototype here is a quick LLM attempt at implementing the optional middleware crate and coercing the error variant to be consistent.
middlewest doesn't save anything on its own. With the cache on it saves 1,280 B. With the cache off it adds 1,624 B. Both are noise next to the 200 KB the cache adds. So the reason to switch would be if we want to use it's functionality to cache redirects in their own cache entries.
Renames the cache config `disabled` to `enabled` and `disabledByDefault` to `enabledByDefault`. The values are inverted, so behavior stays the same: caching is on, but opt-in per request in this release. ### Why Positive names are easier to read than double negatives like `disabledByDefault = false`. ### Testing `cargo test -p tauri-plugin-updater --lib` and clippy pass. Assisted-by: claude-opus-5-5
Replaces the `enabled` cache config with a `cache` cargo feature, on by default. Without it, `http-cache-reqwest`, `http-cache` and moka are left out of the build. `check` then never caches, even when a `cacheMode` is passed. `CacheMode`, `CacheConfig` and `CacheManager` stay public in both builds. So the JS `cacheMode` option and the `cache` config section still parse. `CacheManager` no longer implements `Default`. `UpdaterBuilder::new` now takes the shared one from the plugin state. ### Why Having both `enabled` and `enabledByDefault` in the config was confusing. A cargo feature also removes the cache code and dependencies from apps that don't want it. ### Testing `cargo clippy -p tauri-plugin-updater --all-targets -- -D warnings` and the lib tests pass with default features, with `--all-features`, and with the cache off (`--no-default-features --features rustls-tls,system-proxy,zip`). A new test checks that without the feature, every request reaches the server. Linux only. Assisted-by: claude-opus-5-5
Adds a clippy run and a test run for `tauri-plugin-updater` with `--no-default-features --features rustls-tls,system-proxy,zip`. ### Why The `cache` feature is on in both the default and `--all-features` runs. So CI never built the cache-off code path or ran its test. The test workflow doesn't fail on warnings, so the clippy run catches unused code that only shows up without the feature. ### Testing Both commands pass locally on Linux. Assisted-by: claude-opus-5-5
# Conflicts: # Cargo.lock
`caches_response_without_caching_headers_for_default_ttl` now gives the 200 and 204 cases a cache each. ### Why mockito hands out servers from a pool, so the second server can get the same URL as the first. With a shared cache, the 204 case was then served the cached 200 response and never reached its server. The test failed every time it ran on its own, and only passed in the full suite. ### Testing The test now passes 20 out of 20 runs on its own, and in the full suite. Assisted-by: claude-opus-5-5
| "url-standard", | ||
| ], optional = true } | ||
| # Not used directly, pins the version http-cache-reqwest would otherwise let float. | ||
| http-cache = { version = "=1.0.0-alpha.9", default-features = false, optional = true } |
There was a problem hiding this comment.
Found possibly two more upstream issues.
- Revalidation only handles 200 and 304, not 204. This matters because a
checkNowfollowed bydefaultcould serve stale results on the default check, when our API promises to update the cache. NoCachedoesn't sendno-cacheon a cache miss.
There was a problem hiding this comment.
The second one is more nuanced, and not as problematic.
The 204 case though has an open PR: 06chaynes/http-cache#177
Stores cached responses with `http-cache-quickcache`'s `QuickManager` instead of `MokaManager`. The capacity stays at 6 entries. `quick_cache` is a direct dependency only to build the cache passed to `QuickManager::new`. ### Why moka's eviction and TTL machinery is far more than a cache of a few update responses needs. Freshness is already checked on every read from the stored cache policy, so the storage needs no TTL. QuickManager keeps us on a maintained upstream manager instead of our own storage. It also drops `moka`, `crossbeam-epoch`, `portable-atomic` and `tagptr`. ### Testing Stripped release builds of `tests/app-updater` on Linux x86_64, against `v2` at `df594206`: | Build | Size | Added | | ------------------------ | ------------ | -------- | | `v2`, no cache | 4,878,688 B | | | moka | 5,202,088 B | +323,400 | | QuickManager | 5,080,984 B | +202,296 | | QuickManager, cache off | 4,902,720 B | +24,032 | Clippy and tests pass with all features, and with `--no-default-features --features rustls-tls,system-proxy,zip`. Assisted-by: claude-opus-5-5
Relates to #3641
Update checks now go through an in-memory HTTP cache. Downloads are not cached.
It adds 3 config options to
tauri.config.json, seeCacheConfiginsrc/cache.rs.And individual checks can pick a cache mode:
default,bypassorcheckNow.In JS this is
check({ cacheMode }), in RustUpdaterBuilder::cache_mode_override.The cache follows the server's caching headers.
Responses without
max-ageorExpiresare cached for 1 hour.No response is cached longer than
maxTtlMins, 12 hours by default.checkNowalways contacts the server, but revalidates with a conditional request when it can.Opt-in first, cache by default later
For this release the cache is opt-in. It will create the struct for an in-memory cache, the middleware, etc.
So the new code will be compiled, just bypass the cache by default.
For a future release the intent is to apply the breaking behavioral change.
Making
CacheConfig::disabled_by_defaultdefault tofalse.The most important action for developers is to set
check({ cacheMode: 'checkNow' })for any user-facing "check now" buttons, where there is clear user-intent to revalidate and get the latest metadata.For apps relying primarily on "on startup" update checks, this is less important because the cache is in-memory and would be empty on startup.
Testing
Unit tests run against a local HTTP server.
They cover cache lifetimes, the TTL cap, revalidation, each cache mode, shared state and
Vary.The e2e manifests now send caching headers, so update tests also go through the cache.
The API example sets cache options and uses
checkNowfor its manual check button.Other BREAKING change
In Rust, network errors from
check()are nowError::ReqwestMiddleware(Reqwest(..))instead ofError::Reqwest. Most apps would not be affected by this, unless they are handling these errors in Rust and match on the particular error variant.Assisted-by: gpt-6-sol
Assisted-by: gpt-6.1-sol
Assisted-by: claude-opus-5-5