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 /// Watches until `shutdown` completes, then stops.
92 ///
93 /// The shape a server wants: a watch is not something to remember to
94 /// stop, it is something that ends when the process is winding down.
95 ///
96 /// ```no_run
97 /// # async fn shutdown_signal() {}
98 /// # async fn run(watch: dynamic_config::RemoteWatch) {
99 /// watch.run_until(shutdown_signal()).await;
100 /// # }
101 /// ```
102 ///
103 /// No runtime is imposed: the future is driven by whichever executor is
104 /// already running the caller, and this adds no second cancellation
105 /// mechanism — it sets the same flag [`stop`](Self::stop) does, which is
106 /// what every loop already checks. The same caveat therefore applies: a
107 /// loop parked in a blocking query returns when the store answers or the
108 /// wait expires, so the store's wait time is the worst-case delay.
109 ///
110 /// Takes `self` so the handle cannot outlive the watch it was ending.
111 pub async fn run_until(self, shutdown: impl core::future::Future<Output = ()>) {
112 shutdown.await;
113
114 // Through `drop` rather than `stop` — they do the same thing, and
115 // going through the destructor keeps one path for "this watch is
116 // over" instead of two that could drift.
117 drop(self);
118 }
119}
120
121impl Default for RemoteWatch {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127impl Drop for RemoteWatch {
128 fn drop(&mut self) {
129 self.stop();
130 }
131}
132
133/// The loop's half of a [`RemoteWatch`].
134///
135/// A `Weak`, so a handle that is dropped without anyone remembering to call
136/// `stop` still ends the loop: the upgrade fails and
137/// [`keep_going`](Self::keep_going) answers `false`.
138#[derive(Debug, Clone)]
139pub struct Watching {
140 running: Weak<AtomicBool>,
141}
142
143impl Watching {
144 /// Whether the loop should go round again.
145 ///
146 /// `false` once the caller called [`RemoteWatch::stop`] or dropped the
147 /// handle. Check it before every request, not only after one: a loop that
148 /// checks only on the way out issues one more query than it was asked to.
149 #[must_use]
150 pub fn keep_going(&self) -> bool {
151 self.running
152 .upgrade()
153 .is_some_and(|running| running.load(Ordering::Acquire))
154 }
155
156 /// Sleeps for `total`, waking early if the watch is stopped.
157 ///
158 /// The polling loop every blocking store crate writes: sleep a slice,
159 /// check [`keep_going`](Self::keep_going), repeat — so a stopped watch
160 /// ends within a quarter second instead of at the end of its interval.
161 /// Here once, rather than once per store crate.
162 pub fn sleep_for(&self, total: Duration) {
163 const SLICE: Duration = Duration::from_millis(250);
164
165 let mut slept = Duration::ZERO;
166
167 while slept < total && self.keep_going() {
168 std::thread::sleep(SLICE.min(total - slept));
169 slept += SLICE;
170 }
171 }
172
173 /// Sleeps for `total`, jittered, waking early if the watch is stopped.
174 ///
175 /// A fleet started by one rollout polls in lockstep otherwise: fifty
176 /// replicas with a thirty-second interval become fifty simultaneous
177 /// requests every thirty seconds, and the store sees a spike rather than
178 /// a trickle. The spread is drawn once per loop from the clock, so two
179 /// processes on one machine differ and a restart does not land back in
180 /// the same phase.
181 pub fn sleep_jittered(&self, total: Duration, pace: &mut Pace) {
182 self.sleep_for(pace.spread(total));
183 }
184
185 /// A token for a watch that should never stop.
186 ///
187 /// For a loop the caller genuinely wants to outlive everything, so there is
188 /// no handle to hold. Prefer [`RemoteWatch::detach`], which says the same
189 /// thing at the point where somebody decided it.
190 #[must_use]
191 pub fn forever() -> Self {
192 // A `Weak` that can never upgrade would stop the loop immediately, so
193 // this leaks one live flag — one allocation, once, for the life of the
194 // process.
195 let running = Box::leak(Box::new(Arc::new(AtomicBool::new(true))));
196
197 Self {
198 running: Arc::downgrade(running),
199 }
200 }
201}
202
203/// The waits a watch loop makes.
204///
205/// Two of them, and a loop needs both: the pause between healthy rounds,
206/// spread so a fleet does not poll in lockstep, and the growing pause after
207/// a failure, so a store that is down is not hammered by everything that
208/// depends on it. A loop that sleeps its plain interval after an error is
209/// the shape that turns one outage into two.
210///
211/// ```
212/// # use std::time::Duration;
213/// # use dynamic_config::{Pace, Watching};
214/// # fn example(watching: &Watching) {
215/// let mut pace = Pace::new(Duration::from_secs(30));
216///
217/// while watching.keep_going() {
218/// match fetch() {
219/// Ok(()) => pace.succeeded(),
220/// Err(()) => pace.failed(),
221/// }
222///
223/// pace.wait(watching);
224/// }
225/// # }
226/// # fn fetch() -> Result<(), ()> { Ok(()) }
227/// ```
228#[derive(Debug, Clone)]
229pub struct Pace {
230 interval: Duration,
231 ceiling: Duration,
232 failures: u32,
233 entropy: u64,
234}
235
236impl Pace {
237 /// The default ceiling a backoff grows to.
238 const CEILING: Duration = Duration::from_secs(300);
239
240 /// Rounds `interval` apart when things are going well.
241 #[must_use]
242 pub fn new(interval: Duration) -> Self {
243 Self {
244 interval,
245 ceiling: Self::CEILING,
246 failures: 0,
247 entropy: seed(),
248 }
249 }
250
251 /// Caps how far a backoff grows. Five minutes by default.
252 #[must_use]
253 pub fn with_ceiling(mut self, ceiling: Duration) -> Self {
254 self.ceiling = ceiling;
255 self
256 }
257
258 /// A round went well: the next wait is the plain interval again.
259 pub fn succeeded(&mut self) {
260 self.failures = 0;
261 }
262
263 /// A round failed: the next wait is longer than the last one.
264 pub fn failed(&mut self) {
265 self.failures = self.failures.saturating_add(1);
266 }
267
268 /// How long to wait before the next round, jittered.
269 ///
270 /// The interval while things work; doubling from it after each failure,
271 /// up to the ceiling.
272 ///
273 /// # Two kinds of jitter, for two different problems
274 ///
275 /// A **healthy** wait is spread by a quarter either way. The interval is
276 /// a promise about how often a store is read, and a fleet only needs its
277 /// members nudged out of lockstep to stop arriving together.
278 ///
279 /// A wait **after a failure** is drawn from the whole range instead —
280 /// anywhere between nothing and the full backoff. That is the shape that
281 /// actually decorrelates a fleet, and a recovering store is the one
282 /// moment it matters: a thousand agents that failed at the same instant
283 /// have been counting the same doubling ever since, and a band a quarter
284 /// wide would land them back on the store in a clump.
285 ///
286 /// The two are not interchangeable. Drawing a healthy wait from the
287 /// whole range would halve its mean, which is not a jitter policy — it
288 /// is a different interval, and it doubles the read rate of every store
289 /// in the fleet.
290 #[must_use]
291 pub fn next_wait(&mut self) -> Duration {
292 if self.failures == 0 {
293 let interval = self.interval;
294
295 return self.spread(interval);
296 }
297
298 // Doubling, but never past the ceiling and never past what a
299 // `Duration` can hold — a loop that has failed for a week must
300 // not overflow its way back down to no wait at all.
301 let factor = 1u32.checked_shl(self.failures.min(16)).unwrap_or(u32::MAX);
302
303 let backoff = self
304 .interval
305 .checked_mul(factor)
306 .unwrap_or(self.ceiling)
307 .min(self.ceiling);
308
309 self.up_to(backoff)
310 }
311
312 /// Waits for [`next_wait`](Self::next_wait), waking early if the watch
313 /// is stopped.
314 pub fn wait(&mut self, watching: &Watching) {
315 let wait = self.next_wait();
316
317 watching.sleep_for(wait);
318 }
319
320 /// `base`, moved by up to a quarter of itself in either direction.
321 ///
322 /// Deterministic given the seed, so a test can pin it; drawn from the
323 /// clock at construction, so two processes do not share a phase.
324 #[must_use]
325 pub fn spread(&mut self, base: Duration) -> Duration {
326 let draw = self.draw();
327
328 let quarter = base / 4;
329 let offset = quarter
330 .checked_mul(u32::try_from(draw >> 33 & 0xFF).unwrap_or(0))
331 .unwrap_or(quarter)
332 / 255;
333
334 if draw & 1 == 0 {
335 base.saturating_add(offset)
336 } else {
337 base.saturating_sub(offset)
338 }
339 }
340
341 /// Anywhere from nothing up to `base` — full jitter.
342 ///
343 /// The backoff's own shape. Two processes that failed together stop
344 /// being correlated after one draw, rather than staying a quarter-width
345 /// band apart for as long as the outage lasts.
346 fn up_to(&mut self, base: Duration) -> Duration {
347 let draw = self.draw();
348
349 // Eight bits of the word, scaled across the range. The same
350 // arithmetic `spread` uses, so the two cannot drift apart in how
351 // they handle a `Duration` too large to multiply.
352 base.checked_mul(u32::try_from(draw >> 33 & 0xFF).unwrap_or(0))
353 .unwrap_or(base)
354 / 255
355 }
356
357 /// One step of the generator, and the only place it advances.
358 ///
359 /// An LCG rather than a dependency: the numbers only have to be
360 /// uncorrelated between processes, which is a far weaker ask than
361 /// anything a random number generator is built for.
362 fn draw(&mut self) -> u64 {
363 self.entropy = self
364 .entropy
365 .wrapping_mul(6_364_136_223_846_793_005)
366 .wrapping_add(1_442_695_040_888_963_407);
367
368 self.entropy
369 }
370}
371
372/// Something different per process, without a dependency.
373fn seed() -> u64 {
374 let since = std::time::SystemTime::now()
375 .duration_since(std::time::UNIX_EPOCH)
376 .map_or(0, |elapsed| elapsed.as_nanos() as u64);
377
378 since ^ u64::from(std::process::id()).wrapping_mul(0x9E37_79B9_7F4A_7C15)
379}