Skip to main content

dynamic_config/remote/
watch.rs

1//! Stopping a blocking watch.
2//!
3//! A watch nobody owns is a leak nobody asked for, so the handle stops the
4//! loop when it is dropped and `RemoteWatch::detach` is how a caller says
5//! *this one really should run forever*. Only blocking loops need this: an
6//! async watch is a future, and dropping it is stopping it.
7
8use std::sync::{Arc, Weak};
9use std::time::Duration;
10
11use crate::sync::atomic::{AtomicBool, Ordering};
12
13/// A running blocking watch, from the caller's side.
14///
15/// Dropping it stops the loop — the same contract the file watcher's
16/// `WatchHandle` has, for the same reason: a watch nobody owns is a leak nobody
17/// asked for. [`detach`](Self::detach) is the way to say *this one really should
18/// run forever*.
19///
20/// Only blocking loops need this. An async watch is a future: drop it and it is
21/// cancelled, on any executor.
22///
23/// ```no_run
24/// # use dynamic_config::RemoteWatch;
25/// # struct Consul;
26/// # impl Consul {
27/// #     fn watch(&self, _: dynamic_config::Watching, _: fn(dynamic_config::Fetched) -> Result<(), dynamic_config::Error>) -> Result<(), dynamic_config::Error> { Ok(()) }
28/// # }
29/// # fn example(consul: Consul) {
30/// # fn apply(_: dynamic_config::Fetched) -> Result<(), dynamic_config::Error> { Ok(()) }
31/// let watch = RemoteWatch::new();
32/// let watching = watch.watching();
33///
34/// std::thread::spawn(move || consul.watch(watching, apply));
35///
36/// // ... and later, or by dropping `watch`:
37/// watch.stop();
38/// # }
39/// ```
40#[must_use = "dropping the handle stops the watch; bind it, or call `.detach()` \
41              to watch for the rest of the process"]
42#[derive(Debug)]
43pub struct RemoteWatch {
44    running: Arc<AtomicBool>,
45}
46
47impl RemoteWatch {
48    /// A handle for a watch that has not been handed to a loop yet.
49    pub fn new() -> Self {
50        Self {
51            running: Arc::new(AtomicBool::new(true)),
52        }
53    }
54
55    /// The loop's half of this handle.
56    ///
57    /// Hand it to the watch; keep the `RemoteWatch` yourself.
58    #[must_use]
59    pub fn watching(&self) -> Watching {
60        Watching {
61            running: Arc::downgrade(&self.running),
62        }
63    }
64
65    /// Stops the loop at its next check.
66    ///
67    /// *At its next check* is the whole caveat, and it is not small: a loop
68    /// parked in a blocking query does not return until the store answers or
69    /// the wait expires, so the store's wait time is the worst-case delay. Each
70    /// companion crate documents its own.
71    pub fn stop(&self) {
72        self.running.store(false, Ordering::Release);
73    }
74
75    /// Whether the loop has been told to stop.
76    #[must_use]
77    pub fn is_stopped(&self) -> bool {
78        !self.running.load(Ordering::Acquire)
79    }
80
81    /// Watches for the remainder of the process.
82    ///
83    /// Leaks the handle on purpose, exactly as the file watcher's
84    /// `WatchHandle::detach` does: a watch that must never stop has no owner to
85    /// hold it, and pretending otherwise is how it ends up stopped at the end of
86    /// `main`'s first statement.
87    pub fn detach(self) {
88        std::mem::forget(self);
89    }
90}
91
92impl Default for RemoteWatch {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98impl Drop for RemoteWatch {
99    fn drop(&mut self) {
100        self.stop();
101    }
102}
103
104/// The loop's half of a [`RemoteWatch`].
105///
106/// A `Weak`, so a handle that is dropped without anyone remembering to call
107/// `stop` still ends the loop: the upgrade fails and
108/// [`keep_going`](Self::keep_going) answers `false`.
109#[derive(Debug, Clone)]
110pub struct Watching {
111    running: Weak<AtomicBool>,
112}
113
114impl Watching {
115    /// Whether the loop should go round again.
116    ///
117    /// `false` once the caller called [`RemoteWatch::stop`] or dropped the
118    /// handle. Check it before every request, not only after one: a loop that
119    /// checks only on the way out issues one more query than it was asked to.
120    #[must_use]
121    pub fn keep_going(&self) -> bool {
122        self.running
123            .upgrade()
124            .is_some_and(|running| running.load(Ordering::Acquire))
125    }
126
127    /// Sleeps for `total`, waking early if the watch is stopped.
128    ///
129    /// The polling loop every blocking store crate writes: sleep a slice,
130    /// check [`keep_going`](Self::keep_going), repeat — so a stopped watch
131    /// ends within a quarter second instead of at the end of its interval.
132    /// Here once, rather than once per store crate.
133    pub fn sleep_for(&self, total: Duration) {
134        const SLICE: Duration = Duration::from_millis(250);
135
136        let mut slept = Duration::ZERO;
137
138        while slept < total && self.keep_going() {
139            std::thread::sleep(SLICE.min(total - slept));
140            slept += SLICE;
141        }
142    }
143
144    /// Sleeps for `total`, jittered, waking early if the watch is stopped.
145    ///
146    /// A fleet started by one rollout polls in lockstep otherwise: fifty
147    /// replicas with a thirty-second interval become fifty simultaneous
148    /// requests every thirty seconds, and the store sees a spike rather than
149    /// a trickle. The spread is drawn once per loop from the clock, so two
150    /// processes on one machine differ and a restart does not land back in
151    /// the same phase.
152    pub fn sleep_jittered(&self, total: Duration, pace: &mut Pace) {
153        self.sleep_for(pace.spread(total));
154    }
155
156    /// A token for a watch that should never stop.
157    ///
158    /// For a loop the caller genuinely wants to outlive everything, so there is
159    /// no handle to hold. Prefer [`RemoteWatch::detach`], which says the same
160    /// thing at the point where somebody decided it.
161    #[must_use]
162    pub fn forever() -> Self {
163        // A `Weak` that can never upgrade would stop the loop immediately, so
164        // this leaks one live flag — one allocation, once, for the life of the
165        // process.
166        let running = Box::leak(Box::new(Arc::new(AtomicBool::new(true))));
167
168        Self {
169            running: Arc::downgrade(running),
170        }
171    }
172}
173
174/// The waits a watch loop makes.
175///
176/// Two of them, and a loop needs both: the pause between healthy rounds,
177/// spread so a fleet does not poll in lockstep, and the growing pause after
178/// a failure, so a store that is down is not hammered by everything that
179/// depends on it. A loop that sleeps its plain interval after an error is
180/// the shape that turns one outage into two.
181///
182/// ```
183/// # use std::time::Duration;
184/// # use dynamic_config::{Pace, Watching};
185/// # fn example(watching: &Watching) {
186/// let mut pace = Pace::new(Duration::from_secs(30));
187///
188/// while watching.keep_going() {
189///     match fetch() {
190///         Ok(()) => pace.succeeded(),
191///         Err(()) => pace.failed(),
192///     }
193///
194///     pace.wait(watching);
195/// }
196/// # }
197/// # fn fetch() -> Result<(), ()> { Ok(()) }
198/// ```
199#[derive(Debug, Clone)]
200pub struct Pace {
201    interval: Duration,
202    ceiling: Duration,
203    failures: u32,
204    entropy: u64,
205}
206
207impl Pace {
208    /// The default ceiling a backoff grows to.
209    const CEILING: Duration = Duration::from_secs(300);
210
211    /// Rounds `interval` apart when things are going well.
212    #[must_use]
213    pub fn new(interval: Duration) -> Self {
214        Self {
215            interval,
216            ceiling: Self::CEILING,
217            failures: 0,
218            entropy: seed(),
219        }
220    }
221
222    /// Caps how far a backoff grows. Five minutes by default.
223    #[must_use]
224    pub fn with_ceiling(mut self, ceiling: Duration) -> Self {
225        self.ceiling = ceiling;
226        self
227    }
228
229    /// A round went well: the next wait is the plain interval again.
230    pub fn succeeded(&mut self) {
231        self.failures = 0;
232    }
233
234    /// A round failed: the next wait is longer than the last one.
235    pub fn failed(&mut self) {
236        self.failures = self.failures.saturating_add(1);
237    }
238
239    /// How long to wait before the next round, jittered.
240    ///
241    /// The interval while things work; doubling from it after each failure,
242    /// up to the ceiling.
243    #[must_use]
244    pub fn next_wait(&mut self) -> Duration {
245        let base = if self.failures == 0 {
246            self.interval
247        } else {
248            // Doubling, but never past the ceiling and never past what a
249            // `Duration` can hold — a loop that has failed for a week must
250            // not overflow its way back down to no wait at all.
251            let factor = 1u32.checked_shl(self.failures.min(16)).unwrap_or(u32::MAX);
252
253            self.interval
254                .checked_mul(factor)
255                .unwrap_or(self.ceiling)
256                .min(self.ceiling)
257        };
258
259        self.spread(base)
260    }
261
262    /// Waits for [`next_wait`](Self::next_wait), waking early if the watch
263    /// is stopped.
264    pub fn wait(&mut self, watching: &Watching) {
265        let wait = self.next_wait();
266
267        watching.sleep_for(wait);
268    }
269
270    /// `base`, moved by up to a quarter of itself in either direction.
271    ///
272    /// Deterministic given the seed, so a test can pin it; drawn from the
273    /// clock at construction, so two processes do not share a phase.
274    #[must_use]
275    pub fn spread(&mut self, base: Duration) -> Duration {
276        // An LCG rather than a dependency: the numbers only have to be
277        // uncorrelated between processes, which is a far weaker ask than
278        // anything a random number generator is built for.
279        self.entropy = self
280            .entropy
281            .wrapping_mul(6_364_136_223_846_793_005)
282            .wrapping_add(1_442_695_040_888_963_407);
283
284        let quarter = base / 4;
285        let offset = quarter
286            .checked_mul(u32::try_from(self.entropy >> 33 & 0xFF).unwrap_or(0))
287            .unwrap_or(quarter)
288            / 255;
289
290        if self.entropy & 1 == 0 {
291            base.saturating_add(offset)
292        } else {
293            base.saturating_sub(offset)
294        }
295    }
296}
297
298/// Something different per process, without a dependency.
299fn seed() -> u64 {
300    let since = std::time::SystemTime::now()
301        .duration_since(std::time::UNIX_EPOCH)
302        .map_or(0, |elapsed| elapsed.as_nanos() as u64);
303
304    since ^ u64::from(std::process::id()).wrapping_mul(0x9E37_79B9_7F4A_7C15)
305}