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" version = "2.1.0"
dependencies = [ dependencies = [
"if-addrs", "if-addrs",
"rand",
"regex-lite", "regex-lite",
"reqwest", "reqwest",
"rustls", "rustls",
@@ -298,6 +299,18 @@ dependencies = [
"wasi", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.4.2" version = "0.4.2"
@@ -306,7 +319,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"r-efi", "r-efi 6.0.0",
"wasip2", "wasip2",
"wasip3", "wasip3",
] ]
@@ -772,6 +785,15 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.37" version = "0.2.37"
@@ -800,12 +822,47 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]] [[package]]
name = "r-efi" name = "r-efi"
version = "6.0.0" version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" 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]] [[package]]
name = "regex" name = "regex"
version = "1.12.3" version = "1.12.3"
@@ -1830,6 +1887,26 @@ dependencies = [
"synstructure", "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]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.6" version = "0.1.6"

View File

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

View File

@@ -14,6 +14,7 @@ use crate::pp::PP;
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use rand::Rng;
use reqwest::Client; use reqwest::Client;
use tokio::signal; use tokio::signal;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
@@ -251,12 +252,28 @@ async fn run_env_mode(
return; 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; 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 { fn describe_duration(d: Duration) -> String {
let secs = d.as_secs(); let secs = d.as_secs();
if secs >= 3600 { if secs >= 3600 {
@@ -866,6 +883,29 @@ mod tests {
.await; .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 --- // --- describe_duration tests ---
#[test] #[test]
fn test_describe_duration_seconds_only() { fn test_describe_duration_seconds_only() {