fix: add proportional jitter to reduce synchronized API calls

This commit is contained in:
Justin Hutchings
2026-04-10 10:18:59 -07:00
parent 7b20b7a477
commit e816cce5a8
3 changed files with 119 additions and 1 deletions

79
Cargo.lock generated
View File

@@ -84,6 +84,7 @@ name = "cloudflare-ddns"
version = "2.1.0"
dependencies = [
"if-addrs",
"rand",
"regex-lite",
"reqwest",
"rustls",
@@ -298,6 +299,18 @@ dependencies = [
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi 5.3.0",
"wasip2",
]
[[package]]
name = "getrandom"
version = "0.4.2"
@@ -306,7 +319,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"r-efi 6.0.0",
"wasip2",
"wasip3",
]
@@ -772,6 +785,15 @@ dependencies = [
"zerovec",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
@@ -800,12 +822,47 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "regex"
version = "1.12.3"
@@ -1830,6 +1887,26 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zerofrom"
version = "0.1.6"

View File

@@ -14,6 +14,7 @@ tokio = { version = "1", features = ["rt", "macros", "time", "signal", "net"] }
regex-lite = "0.1"
url = "2"
if-addrs = "0.15"
rand = "0.9"
[profile.release]
opt-level = "z"

View File

@@ -14,6 +14,7 @@ use crate::pp::PP;
use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use rand::Rng;
use reqwest::Client;
use tokio::signal;
use tokio::time::{sleep, Duration};
@@ -251,12 +252,28 @@ async fn run_env_mode(
return;
}
// Apply proportional jitter before each update to spread API calls
// across clients and reduce synchronized traffic spikes at Cloudflare.
let max_jitter = interval.as_secs() / 5;
if max_jitter > 0 {
let jitter_secs = rand::rng().random_range(0..=max_jitter);
sleep(std::time::Duration::from_secs(jitter_secs)).await;
}
updater::update_once(config, handle, notifier, heartbeat, cf_cache, ppfmt, &mut noop_reported, detection_client).await;
}
}
}
}
fn jitter_duration(interval_secs: u64, rand_val: u64) -> std::time::Duration {
let max_jitter = interval_secs / 5;
if max_jitter == 0 {
return std::time::Duration::ZERO;
}
std::time::Duration::from_secs(rand_val % (max_jitter + 1))
}
fn describe_duration(d: Duration) -> String {
let secs = d.as_secs();
if secs >= 3600 {
@@ -866,6 +883,29 @@ mod tests {
.await;
}
// --- jitter_duration tests ---
#[test]
fn test_jitter_duration_standard() {
// 5-minute interval: max jitter = 60s
let d = super::jitter_duration(300, 30);
assert_eq!(d, std::time::Duration::from_secs(30));
let d = super::jitter_duration(300, 61);
assert_eq!(d, std::time::Duration::from_secs(61 % 61)); // wraps within [0, 60]
}
#[test]
fn test_jitter_duration_short_interval() {
// interval < 5s: must return zero
assert_eq!(super::jitter_duration(4, 99), std::time::Duration::ZERO);
assert_eq!(super::jitter_duration(0, 99), std::time::Duration::ZERO);
}
#[test]
fn test_jitter_duration_deterministic() {
// rand_val=0 always returns zero duration
assert_eq!(super::jitter_duration(300, 0), std::time::Duration::ZERO);
}
// --- describe_duration tests ---
#[test]
fn test_describe_duration_seconds_only() {