1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
//! The no-progress bound every engine stop path waits under (AE-017).
//!
//! An engine stop drains three worker threads — the cleanup executor, the
//! process-exit drainer, and the process-exit callback dispatcher — and each
//! of them may be in the middle of real work when the stop arrives: a
//! terminal being recorded to the store, an abort being delivered, an exit
//! event being observed. Before this module, each drain waited under ONE
//! total bound derived from the signal-delivery readiness window (50 ms × 9
//! = 450 ms), and a box that was merely busy blew it: on 2026-08-29 a tree
//! with no change at all failed its own stop 3/3 under a foreign build and
//! passed 3/3 on the same binary thirteen minutes later.
//!
//! # What the bound means now
//!
//! It is a NO-PROGRESS bound. The clock restarts every time the drained
//! worker completes a job and fires only when nothing has completed for the
//! whole window. A loaded box that is still finishing callbacks is therefore
//! never called a wedge, however long its queue; a genuinely blocked callback
//! is named within one window of blocking; and the progress warnings emitted
//! while the drain waits are the evidence of which one it was. The bound is
//! GIVEN — by the engine builder, from the server's configuration — never
//! derived from an unrelated policy and never defaulted here.
//!
//! The contract a blocked job used to satisfy still holds: a callback that
//! never returns produces a bounded, retryable failure, not a hang. It is
//! bounded from its last progress rather than from the stop request.
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::mpsc::{Receiver, RecvTimeoutError};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
/// What a drained worker reports about itself: jobs completed, the job in
/// flight, and the depth of its queue. Updated by the worker thread; read
/// by the stop path.
#[derive(Debug, Default)]
pub(super) struct DrainProgress {
completed: AtomicU64,
in_flight_since: Mutex<Option<Instant>>,
queued: AtomicUsize,
}
/// A point-in-time reading of a worker's progress, for a warning or an error.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct ProgressSnapshot {
/// Jobs the worker has completed since it started.
pub(super) completed: u64,
/// How long the job in flight has been running, when one is.
pub(super) in_flight_for: Option<Duration>,
/// Jobs waiting behind the one in flight.
pub(super) queued: usize,
}
impl DrainProgress {
pub(super) fn new() -> Arc<Self> {
Arc::new(Self::default())
}
/// The worker took a job.
pub(super) fn begin(&self) {
// A poisoned lock here means a worker panicked mid-update; the stop
// path must still be able to read, so the poison is cleared rather
// than propagated — this is a gauge, not a ledger.
let mut in_flight = self
.in_flight_since
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*in_flight = Some(Instant::now());
}
/// The worker finished the job it took.
pub(super) fn finish(&self) {
let mut in_flight = self
.in_flight_since
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*in_flight = None;
drop(in_flight);
self.completed.fetch_add(1, Ordering::AcqRel);
}
pub(super) fn enqueued(&self) {
self.queued.fetch_add(1, Ordering::AcqRel);
}
pub(super) fn dequeued(&self) {
// Saturating: a dequeue racing a stop must never wrap the gauge.
let _ = self
.queued
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
current.checked_sub(1)
});
}
pub(super) fn queued(&self) -> usize {
self.queued.load(Ordering::Acquire)
}
pub(super) fn snapshot(&self) -> ProgressSnapshot {
let in_flight_for = self
.in_flight_since
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.map(|since| since.elapsed());
ProgressSnapshot {
completed: self.completed.load(Ordering::Acquire),
in_flight_for,
queued: self.queued(),
}
}
}
/// Why a drain gave up: nothing completed for the whole bound.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct DrainTimedOut {
/// The bound the drain waited under.
pub(super) bound: Duration,
/// How long it has been since the worker last completed a job.
pub(super) since_progress: Duration,
/// The worker's queue depth at the moment the drain gave up.
pub(super) queued: usize,
}
/// The bound, and the waiting that interprets it.
#[derive(Clone, Copy, Debug)]
pub(super) struct DrainBound {
bound: Duration,
}
impl DrainBound {
pub(super) const fn new(bound: Duration) -> Self {
Self { bound }
}
/// How often a waiting loop wakes to re-read progress and the stop
/// signal: one hundredth of the bound, so a stop is observed within one
/// percent of the operator's patience whatever that patience is. Never
/// zero — a zero wait would spin.
pub(super) fn poll_interval(self) -> Duration {
(self.bound / 100).max(Duration::from_micros(1))
}
/// How often the drain says aloud that it is still waiting: a quarter of
/// the bound, so an operator watching a 30 s drain sees the in-flight job
/// named three times before the drain gives up.
pub(super) fn progress_interval(self) -> Duration {
(self.bound / 4).max(self.poll_interval())
}
/// Wait for `stopped` to deliver (or disconnect), restarting the clock
/// every time `progress` records a completed job, warning at
/// [`Self::progress_interval`] while it waits, and giving up only when
/// nothing has completed for the whole bound.
pub(super) fn wait(
self,
worker: &'static str,
stopped: &Receiver<()>,
progress: &DrainProgress,
) -> Result<(), DrainTimedOut> {
let poll = self.poll_interval();
let warn_every = self.progress_interval();
let mut last_completed = progress.snapshot().completed;
let mut last_progress_at = Instant::now();
let mut last_warned_at = last_progress_at;
loop {
match stopped.recv_timeout(poll) {
Ok(()) | Err(RecvTimeoutError::Disconnected) => return Ok(()),
Err(RecvTimeoutError::Timeout) => {}
}
let now = Instant::now();
let snapshot = progress.snapshot();
if snapshot.completed != last_completed {
last_completed = snapshot.completed;
last_progress_at = now;
}
let since_progress = now.duration_since(last_progress_at);
if since_progress >= self.bound {
tracing::error!(
worker,
bound_ms = self.bound.as_millis(),
since_progress_ms = since_progress.as_millis(),
in_flight_ms = snapshot.in_flight_for.map(|age| age.as_millis()),
queued = snapshot.queued,
"engine stop gave up draining: nothing completed for the whole bound"
);
return Err(DrainTimedOut {
bound: self.bound,
since_progress,
queued: snapshot.queued,
});
}
if now.duration_since(last_warned_at) >= warn_every {
last_warned_at = now;
tracing::warn!(
worker,
bound_ms = self.bound.as_millis(),
since_progress_ms = since_progress.as_millis(),
in_flight_ms = snapshot.in_flight_for.map(|age| age.as_millis()),
queued = snapshot.queued,
completed = snapshot.completed,
"engine stop still draining; the bound restarts on every completed job"
);
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
use super::{DrainBound, DrainProgress};
/// A worker whose jobs each finish inside the bound but whose queue
/// takes many bounds in total is a slow box, not a wedge: the drain
/// waits it out.
#[test]
fn a_progressing_worker_is_waited_out_past_the_total_bound()
-> Result<(), Box<dyn std::error::Error>> {
let bound = DrainBound::new(Duration::from_millis(80));
let progress = DrainProgress::new();
let (stopped_sender, stopped) = mpsc::sync_channel(1);
let worker_progress = std::sync::Arc::clone(&progress);
let worker = thread::spawn(move || {
// Five jobs of 40 ms: 200 ms total, every gap under the 80 ms bound.
for _ in 0..5 {
worker_progress.begin();
thread::sleep(Duration::from_millis(40));
worker_progress.finish();
}
let _ = stopped_sender.send(());
});
let started = Instant::now();
bound
.wait("test-worker", &stopped, &progress)
.map_err(|timed_out| {
format!("a progressing worker was called a wedge: {timed_out:?}")
})?;
assert!(
started.elapsed() >= Duration::from_millis(150),
"the drain waited for the whole queue, not one bound"
);
worker.join().map_err(|_| "worker panicked")?;
Ok(())
}
/// A job that never completes is named within one bound of its last
/// progress, with the in-flight age and queue depth on the error.
#[test]
fn a_blocked_worker_fails_bounded_from_its_last_progress()
-> Result<(), Box<dyn std::error::Error>> {
let bound = DrainBound::new(Duration::from_millis(60));
let progress = DrainProgress::new();
let (_stopped_sender, stopped) = mpsc::sync_channel::<()>(1);
progress.enqueued();
progress.enqueued();
progress.dequeued();
progress.begin();
let started = Instant::now();
let timed_out = bound
.wait("test-worker", &stopped, &progress)
.err()
.ok_or("a blocked worker must time the drain out")?;
let elapsed = started.elapsed();
assert!(
elapsed >= Duration::from_millis(60),
"not before the bound: {elapsed:?}"
);
assert!(
elapsed < Duration::from_millis(600),
"not long after it: {elapsed:?}"
);
assert_eq!(timed_out.bound, Duration::from_millis(60));
assert!(timed_out.since_progress >= Duration::from_millis(60));
assert_eq!(timed_out.queued, 1, "the queue depth rides the error");
Ok(())
}
/// The clock restarts on progress: a worker that completes one job late
/// in the first window and then blocks fails one bound after THAT job,
/// not one bound after the stop request.
#[test]
fn the_clock_restarts_on_progress_not_on_the_stop_request()
-> Result<(), Box<dyn std::error::Error>> {
let bound = DrainBound::new(Duration::from_millis(100));
let progress = DrainProgress::new();
let (_stopped_sender, stopped) = mpsc::sync_channel::<()>(1);
let worker_progress = std::sync::Arc::clone(&progress);
let worker = thread::spawn(move || {
worker_progress.begin();
thread::sleep(Duration::from_millis(70));
worker_progress.finish();
worker_progress.begin();
// Never finishes.
thread::sleep(Duration::from_secs(2));
});
let started = Instant::now();
let timed_out = bound
.wait("test-worker", &stopped, &progress)
.err()
.ok_or("the blocked second job must time the drain out")?;
let elapsed = started.elapsed();
assert!(
elapsed >= Duration::from_millis(160),
"the bound restarted after the 70 ms job: {elapsed:?}"
);
assert!(timed_out.since_progress < Duration::from_millis(160));
drop(worker);
Ok(())
}
#[test]
fn a_stopped_worker_returns_at_once() -> Result<(), Box<dyn std::error::Error>> {
let bound = DrainBound::new(Duration::from_secs(30));
let progress = DrainProgress::new();
let (stopped_sender, stopped) = mpsc::sync_channel(1);
stopped_sender.send(())?;
let started = Instant::now();
bound
.wait("test-worker", &stopped, &progress)
.map_err(|timed_out| format!("{timed_out:?}"))?;
assert!(started.elapsed() < Duration::from_secs(1));
Ok(())
}
#[test]
fn intervals_derive_from_the_bound_and_never_reach_zero() {
let bound = DrainBound::new(Duration::from_secs(30));
assert_eq!(bound.poll_interval(), Duration::from_millis(300));
assert_eq!(bound.progress_interval(), Duration::from_millis(7_500));
let tiny = DrainBound::new(Duration::from_nanos(1));
assert!(tiny.poll_interval() > Duration::ZERO);
assert!(tiny.progress_interval() >= tiny.poll_interval());
}
}