Skip to content

Commit 14e691b

Browse files
Rate-limit CLI update notices (clockworklabs#5184)
# Description of Changes Fixes clockworklabs#5183. This rate-limits the CLI update notice so the same advertised latest version is printed at most once every 24 hours. It keeps the existing 24 hour release lookup cache and adds backward-compatible notice metadata to the same cache file. # API and ABI breaking changes None. # Expected complexity level and risk 1. The change is local to the lightweight update notice cache path and only affects how often the notice is printed. # Testing - [x] `cargo fmt --all` - [x] `cargo test -p spacetimedb-update update_notice` - [x] Manual binary check with `cargo run`: ```bash tmpdir=$(mktemp -d) mkdir -p "$tmpdir/config" printf '{"last_check_secs":4102444800,"latest_version":"999.0.0"}' > "$tmpdir/config/.update_check_cache" cargo build -p spacetimedb-cli -p spacetimedb-update SPACETIMEDB_UPDATE_MULTICALL_APPLET=spacetime cargo run -p spacetimedb-update -- --root-dir="$tmpdir" help 2> /tmp/update-notice-1.err >/tmp/update-notice-1.out SPACETIMEDB_UPDATE_MULTICALL_APPLET=spacetime cargo run -p spacetimedb-update -- --root-dir="$tmpdir" help 2> /tmp/update-notice-2.err >/tmp/update-notice-2.out rg "A new version of SpacetimeDB is available" /tmp/update-notice-1.err ! rg "A new version of SpacetimeDB is available" /tmp/update-notice-2.err ``` --------- Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com>
1 parent 99b5ef6 commit 14e691b

1 file changed

Lines changed: 92 additions & 0 deletions

File tree

crates/update/src/update_notice.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use crate::cli::install::fetch_latest_release_version;
1313

1414
/// How long to cache the update check result.
1515
const CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
16+
/// How often to show the user the same update notice.
17+
const NOTICE_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
1618
const UPDATE_CHECK_TIMEOUT: Duration = Duration::from_secs(2);
1719

1820
/// Cache file name.
@@ -24,6 +26,12 @@ struct Cache {
2426
last_check_secs: u64,
2527
/// The latest version string (without "v" prefix).
2628
latest_version: String,
29+
/// Unix timestamp of the last printed update notice.
30+
#[serde(default)]
31+
last_notice_secs: u64,
32+
/// The latest version from the last printed update notice.
33+
#[serde(default)]
34+
notice_latest_version: String,
2735
}
2836

2937
impl Cache {
@@ -41,6 +49,16 @@ impl Cache {
4149
fn path(config_dir: &Path) -> PathBuf {
4250
config_dir.join(CACHE_FILENAME)
4351
}
52+
53+
fn should_print_notice(&self, latest: &semver::Version, now: u64) -> bool {
54+
self.notice_latest_version != latest.to_string()
55+
|| now.saturating_sub(self.last_notice_secs) >= NOTICE_INTERVAL.as_secs()
56+
}
57+
58+
fn mark_notice_printed(&mut self, latest: &semver::Version, now: u64) {
59+
self.last_notice_secs = now;
60+
self.notice_latest_version = latest.to_string();
61+
}
4462
}
4563

4664
fn now_secs() -> u64 {
@@ -78,6 +96,7 @@ fn latest_version_or_cached(config_dir: &Path) -> Option<semver::Version> {
7896
Cache {
7997
last_check_secs: now,
8098
latest_version: version.to_string(),
99+
..Default::default()
81100
}
82101
.write(config_dir);
83102
Some(version)
@@ -115,11 +134,84 @@ pub(crate) fn maybe_print_update_notice(config_dir: &Path) {
115134
};
116135

117136
if latest > current {
137+
let now = now_secs();
138+
let mut cache = Cache::read(config_dir).unwrap_or_default();
139+
if !cache.should_print_notice(&latest, now) {
140+
return;
141+
}
142+
118143
eprintln!(
119144
"{}",
120145
format!("A new version of SpacetimeDB is available: v{latest} (current: v{current})").yellow()
121146
);
122147
eprintln!("Run `spacetime version upgrade` to update.");
123148
eprintln!();
149+
150+
cache.mark_notice_printed(&latest, now);
151+
if cache.latest_version.is_empty() {
152+
cache.latest_version = latest.to_string();
153+
}
154+
cache.write(config_dir);
155+
}
156+
}
157+
158+
#[cfg(test)]
159+
mod tests {
160+
use super::*;
161+
162+
fn version(version: &str) -> semver::Version {
163+
semver::Version::parse(version).unwrap()
164+
}
165+
166+
#[test]
167+
fn update_notice_prints_when_never_shown() {
168+
let cache = Cache {
169+
latest_version: "2.0.0".to_string(),
170+
..Default::default()
171+
};
172+
173+
assert!(cache.should_print_notice(&version("2.0.0"), 100));
174+
}
175+
176+
#[test]
177+
fn update_notice_is_suppressed_within_interval_for_same_versions() {
178+
let mut cache = Cache::default();
179+
let latest = version("2.0.0");
180+
cache.mark_notice_printed(&latest, 100);
181+
182+
assert!(!cache.should_print_notice(&latest, 100 + NOTICE_INTERVAL.as_secs() - 1));
183+
}
184+
185+
#[test]
186+
fn update_notice_reprints_after_interval_for_same_versions() {
187+
let mut cache = Cache::default();
188+
let latest = version("2.0.0");
189+
cache.mark_notice_printed(&latest, 100);
190+
191+
assert!(cache.should_print_notice(&latest, 100 + NOTICE_INTERVAL.as_secs()));
192+
}
193+
194+
#[test]
195+
fn update_notice_reprints_when_latest_version_changes() {
196+
let mut cache = Cache::default();
197+
cache.mark_notice_printed(&version("2.0.0"), 100);
198+
199+
assert!(cache.should_print_notice(&version("2.1.0"), 101));
200+
}
201+
202+
#[test]
203+
fn update_notice_cache_reads_old_format() {
204+
let tempdir = tempfile::tempdir().unwrap();
205+
std::fs::write(
206+
Cache::path(tempdir.path()),
207+
r#"{"last_check_secs":123,"latest_version":"2.0.0"}"#,
208+
)
209+
.unwrap();
210+
211+
let cache = Cache::read(tempdir.path()).unwrap();
212+
assert_eq!(cache.last_check_secs, 123);
213+
assert_eq!(cache.latest_version, "2.0.0");
214+
assert_eq!(cache.last_notice_secs, 0);
215+
assert!(cache.notice_latest_version.is_empty());
124216
}
125217
}

0 commit comments

Comments
 (0)