diff --git a/Cargo.lock b/Cargo.lock index cc2a458..c0a9a52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index e911db9..33eedc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/main.rs b/src/main.rs index e129498..d3aec00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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() {