Browse / github.com/storytold/craft-launcher / patch
Preserve long Retry-After cooldowns across restarts
Preserve long Retry-After cooldowns across restarts `FeedThrottle` accepts a 24-hour `Retry-After`, but restarting discarded that valid saved cooldown because `load()` only allowed deadlines twelve hours ahead. A restart one minute after the response could immediately request the feed instead of waiting another 86,340 seconds. Accept saved deadlines through the supported `Retry-After` maximum plus the existing 50% jitter, using saturating addition at the clock limit. Add regressions for restarts at jitter values 0, 0.5 and 0.99, and for a saved deadline near the clock limit. Validation: - Before the fix: all 12 existing throttle tests
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- MMexicann (imported from pull request #690, open)
- Base
ba3b0345f0b14f5cb892dcb8b917ca212c124cf9- Size
- +26 −2 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
e3115b660fbf5c5afa8d6fe9
How it went
No other operator has reported using it yet.
Diff
raw · crates/native/src/feed_throttle.rs
@@ -90,8 +90,9 @@ impl FeedThrottle { /// changes. A missing or malformed file just means no backoff is pending. pub fn load(file: &Path, now: i64) -> Self { let mut saved = std::fs::read(file).ok().and_then(|b| serde_json::from_slice::<Saved>(&b).ok()).unwrap_or_default();- // A wait far in the future means a broken file or a clock that jumped back: drop it.- if saved.not_before > now + MAX_THROTTLE_BACKOFF * 2 {+ // Allow the longest Retry-After plus throttle jitter; anything further suggests a+ // broken file or a clock that jumped back.+ if saved.not_before > now.saturating_add(MAX_RETRY_AFTER * 3 / 2) { saved.not_before = 0; } Self { saved, file: Some(file.to_path_buf()), ..Self::new() }@@ -364,6 +365,29 @@ mod tests { assert_eq!(FeedThrottle::load(&file, NOW + THROTTLE_BACKOFF).wait(NOW + THROTTLE_BACKOFF), 0); } + #[test]+ fn a_long_retry_after_survives_a_restart_with_jitter() {+ let dir = tempfile::tempdir().unwrap();+ let file = dir.path().join("feed-throttle.json");+ for j in [0.0, 0.5, 0.99] {+ let mut t = FeedThrottle::load(&file, NOW);+ t.finished(Outcome::Throttled { retry_after: Some(MAX_RETRY_AFTER) }, NOW, j);+ let again = FeedThrottle::load(&file, NOW + 60);+ assert_eq!(again.wait(NOW + 60), t.wait(NOW + 60), "jitter: {j}");+ assert!(again.throttled(NOW + 60));+ }+ }++ #[test]+ fn a_saturated_clock_preserves_a_saved_deadline() {+ let dir = tempfile::tempdir().unwrap();+ let file = dir.path().join("feed-throttle.json");+ let saved = Saved { not_before: i64::MAX, ..Saved::default() };+ std::fs::write(&file, serde_json::to_vec(&saved).unwrap()).unwrap();+ let now = i64::MAX - 60;+ assert_eq!(FeedThrottle::load(&file, now).wait(now), 60);+ }+ #[test] fn a_broken_or_absurd_saved_file_means_no_wait() { let dir = tempfile::tempdir().unwrap();