Skip to main content

aion_server/worker/queue_service/
census.rs

1//! Live poller census for one pool address, and the classification it feeds.
2//!
3//! The census is taken from the connected-worker registry at the same moment
4//! selection misses, so the taxonomy verdict describes the fleet the selection
5//! actually saw — never a second, later, disagreeing observation.
6
7use std::time::Duration;
8
9use super::declarations::QueueDeclaration;
10use super::taxonomy::QueueServiceReason;
11
12/// What the connected-worker registry holds for one
13/// `(namespace, task_queue, activity_type[, node])` address.
14#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
15pub struct PoolCensus {
16    /// Workers connected for `(namespace, task_queue)`, whatever they serve.
17    pub workers_in_pool: usize,
18    /// Of those, workers advertising this activity type.
19    pub workers_serving_activity: usize,
20    /// Of those, workers that also satisfy the dispatch's node pin. Equal to
21    /// [`Self::workers_serving_activity`] for an unpinned dispatch.
22    ///
23    /// Deliberately NOT filtered by dispatch eligibility, so
24    /// [`classify`] can tell an empty pool from an excluded one (#197 R3).
25    pub compatible_workers: usize,
26    /// Of [`Self::compatible_workers`], those a dispatch could actually be
27    /// handed to right now — the rest are excluded by the liveness verdict.
28    pub eligible_compatible_workers: usize,
29    /// Of the excluded compatible workers, those excluded by
30    /// `DispatchExclusion::ReachabilityLost` rather than by an opening
31    /// probation.
32    ///
33    /// 🔴 The whole point of the distinction. An opening probation clears
34    /// itself in seconds, so parking silently through one is correct and
35    /// announcing it would be noise on every healthy connect. A lost
36    /// reachability does NOT clear on its own, so a dispatch parked on one
37    /// waits indefinitely — and must say so, or it is a silent stall.
38    pub compatible_workers_reachability_lost: usize,
39    /// Of [`Self::compatible_workers`], those already holding every dispatch
40    /// they advertised they would run at once.
41    ///
42    /// 🔴 A THIRD condition, and the reason [`classify`] needs a third answer.
43    /// An at-capacity pool is neither empty nor unreachable: its workers are
44    /// connected, serving this activity, and working. The remedy is the
45    /// OPPOSITE of the unreachable one — starting another worker helps here and
46    /// does not help there — so reporting either existing reason for it would
47    /// send an operator the wrong way.
48    pub compatible_workers_at_capacity: usize,
49    /// Of [`Self::compatible_workers`], those that have registered but have not
50    /// yet announced how much work they can take.
51    ///
52    /// 🔴 DISJOINT from [`Self::compatible_workers_at_capacity`], and counted
53    /// apart from it on purpose. Selection excludes both, by the same test —
54    /// unknown capacity is treated as full — so a census that folded them
55    /// together would be arithmetically fine and operationally a lie: it would
56    /// report a pool of idle, mute workers as a pool of busy ones, and attach
57    /// the advice for a busy pool ("start another worker") to a condition
58    /// another worker of the same build cannot fix.
59    ///
60    /// Only the liminal transport can produce it, and normally only for the one
61    /// round trip between a worker's registration and its capacity
62    /// announcement.
63    pub compatible_workers_capacity_unannounced: usize,
64    /// How long ago a compatible worker was last in service, or `None` when
65    /// none has ever been seen for this address in this server's life.
66    ///
67    /// Zero while a compatible worker is connected right now.
68    pub last_compatible_poller_age: Option<Duration>,
69}
70
71impl PoolCensus {
72    /// Whether the address currently has a compatible worker REGISTERED,
73    /// whatever its dispatch eligibility.
74    ///
75    /// A field-level fact, not a decision. It stays deliberately blind to
76    /// eligibility because [`classify`] needs the blind count to tell an empty
77    /// pool from an excluded one (#197 R3). Callers asking "will a dispatch
78    /// aimed here actually proceed?" want [`Self::will_be_served`].
79    #[must_use]
80    pub const fn is_served(&self) -> bool {
81        self.compatible_workers > 0
82    }
83
84    /// Whether a dispatch aimed here can be expected to proceed without an
85    /// operator doing something first.
86    ///
87    /// 🔴 The distinction [`Self::is_served`] cannot draw. A pool whose workers
88    /// are all serving an opening probation is not dispatchable this instant
89    /// but will be within seconds, so it answers `true` — waiting is the
90    /// correct outcome and the caller should not be warned. A pool whose
91    /// workers have all LOST reachability answers `false`: nothing about it
92    /// improves on its own, so a caller told "served" would be told a run is
93    /// about to proceed when it is about to park indefinitely.
94    ///
95    /// A pool whose workers are all AT CAPACITY answers `true` for the same
96    /// reason as the probation case: it is not dispatchable this instant and
97    /// needs nobody to do anything — a running activity finishes, a slot frees,
98    /// and the parked dispatch is selected. Waiting is the correct outcome.
99    ///
100    /// A pool whose workers have NOT ANNOUNCED their capacity answers `false`,
101    /// and is the reason this is no longer two clauses. Nothing about that pool
102    /// improves on its own: the announcement either already arrived — in which
103    /// case these workers are not in this bucket — or it never will, because the
104    /// worker is a build that does not send one or its publish was lost. Saying
105    /// "will be served" of a fleet that is registered, idle, mute and
106    /// permanently unselectable is the most expensive kind of wrong answer,
107    /// because the whole point of this predicate is to tell a caller whether
108    /// waiting is enough.
109    #[must_use]
110    pub const fn will_be_served(&self) -> bool {
111        if self.eligible_compatible_workers > 0 {
112            return true;
113        }
114        self.compatible_workers > 0
115            && self.compatible_workers_reachability_lost == 0
116            && self.compatible_workers_capacity_unannounced == 0
117    }
118}
119
120/// Classify one selection miss into the taxonomy.
121///
122/// Returns `None` when the caller must simply retry selection rather than
123/// report a state that is no longer true. That covers two cases: the address is
124/// genuinely served because a worker arrived between the two lock
125/// acquisitions, and the address's compatible workers are all serving an
126/// opening probation, which clears on its own within seconds.
127///
128/// 🔴 It does NOT cover a pool whose compatible workers have all LOST
129/// reachability. That does not clear on its own, so returning `None` there
130/// parks the dispatch indefinitely with nothing published about why — the
131/// silent stall this arm exists to prevent.
132///
133/// Ordering is deliberate: a structurally undeclared queue is reported as
134/// [`QueueServiceReason::NoQueueDeclaration`] even when unrelated workers sit
135/// in the pool, because that is the deeper fact and the only one that refuses
136/// unconditionally. [`QueueDeclaration::Unknown`] never manufactures a
137/// structural refusal — the same fail-open discipline worker registration
138/// applies when no catalog is reachable.
139///
140/// [`QueueServiceReason::Saturated`] is never produced here: it is not a
141/// selection miss. It is emitted by the delivery path
142/// ([`super::delivery::deliver_within_schedule_to_start`]) when a compatible
143/// worker is live and refuses intake past the schedule-to-start clock.
144#[must_use]
145pub fn classify(declaration: QueueDeclaration, census: &PoolCensus) -> Option<QueueServiceReason> {
146    if census.eligible_compatible_workers > 0 {
147        return None;
148    }
149    if declaration == QueueDeclaration::NotDeclared {
150        return Some(QueueServiceReason::NoQueueDeclaration);
151    }
152    if census.compatible_workers > 0 {
153        // Every compatible worker is excluded by the liveness verdict. Which
154        // exclusion decides whether this is worth saying: an opening probation
155        // is the ordinary cost of connecting and clears itself, and reporting
156        // it would fire on every healthy worker's first seconds.
157        //
158        // `PollersIncompatible` would be a LIE here and not merely a vague
159        // one — its sentence is "workers are connected but none serves this
160        // activity", and every one of these serves it. The remedy it implies
161        // (deploy a worker that covers the type) is wrong; the real remedy is
162        // to read the liveness verdict for workers already present.
163        if census.compatible_workers_reachability_lost > 0 {
164            return Some(QueueServiceReason::PollersUnreachable);
165        }
166        // Checked BEFORE the at-capacity arm, and the precedence is the point.
167        // Selection excludes an unannounced worker by the same test it excludes
168        // a full one, so both conditions can be true of one pool at once — and
169        // only one reason can be published. The unannounced one wins because it
170        // is the one an operator may have to act on: a busy pool clears itself
171        // as work completes, whereas a worker that has not announced its
172        // capacity past one round trip never will without being read or
173        // replaced. Reporting the self-healing condition and burying the
174        // durable one would be the wrong way round.
175        //
176        // `> 0` rather than `== compatible_workers` for the same reason: one
177        // mute worker in an otherwise busy pool is still a fact worth surfacing,
178        // and its sentence stays true of the pool ("not one of them has
179        // announced" is scoped by the sentence to the workers it describes).
180        if census.compatible_workers_capacity_unannounced > 0 {
181            return Some(QueueServiceReason::PollersCapacityUnannounced);
182        }
183        // Every compatible worker is connected, serving this activity, and
184        // FULL. That is a third condition, not a quieter version of either one
185        // above: the pool is healthy and the work is queued behind real work.
186        //
187        // Reported only when the whole compatible population is at capacity. A
188        // mixed pool — some full, some serving an opening probation — is
189        // dominated by the probation, which clears itself within seconds, and
190        // announcing anything for it would fire on every healthy worker start.
191        if census.compatible_workers_at_capacity == census.compatible_workers {
192            return Some(QueueServiceReason::PollersAtCapacity);
193        }
194        return None;
195    }
196    if census.workers_in_pool > 0 {
197        return Some(QueueServiceReason::PollersIncompatible);
198    }
199    Some(QueueServiceReason::NoLivePollers)
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn a_served_address_is_not_classified() {
208        let census = PoolCensus {
209            workers_in_pool: 1,
210            workers_serving_activity: 1,
211            compatible_workers: 1,
212            eligible_compatible_workers: 1,
213            compatible_workers_reachability_lost: 0,
214            last_compatible_poller_age: Some(Duration::ZERO),
215            compatible_workers_at_capacity: 0,
216            compatible_workers_capacity_unannounced: 0,
217        };
218        assert_eq!(classify(QueueDeclaration::Declared, &census), None);
219        assert_eq!(classify(QueueDeclaration::NotDeclared, &census), None);
220    }
221
222    #[test]
223    fn an_undeclared_queue_is_structural() {
224        assert_eq!(
225            classify(QueueDeclaration::NotDeclared, &PoolCensus::default()),
226            Some(QueueServiceReason::NoQueueDeclaration)
227        );
228    }
229
230    #[test]
231    fn an_empty_pool_on_a_declared_queue_has_no_live_pollers() {
232        assert_eq!(
233            classify(QueueDeclaration::Declared, &PoolCensus::default()),
234            Some(QueueServiceReason::NoLivePollers)
235        );
236    }
237
238    #[test]
239    fn an_unknowable_declaration_never_produces_a_structural_refusal() {
240        assert_eq!(
241            classify(QueueDeclaration::Unknown, &PoolCensus::default()),
242            Some(QueueServiceReason::NoLivePollers)
243        );
244    }
245
246    /// A BUSY pool is neither empty nor unreachable, and the ladder must say
247    /// so in its own words.
248    ///
249    /// This is the arm the outage produced and nothing reported: every worker
250    /// connected, serving the activity, and full. Before the arm existed the
251    /// ladder had two ways to describe it and both were false — it fell through
252    /// to `NoLivePollers` ("no worker is connected") once the sweep had
253    /// deregistered the busy worker, and would otherwise have returned `None`
254    /// and parked in silence.
255    #[test]
256    fn a_pool_whose_every_worker_is_full_reports_at_capacity() {
257        let census = PoolCensus {
258            workers_in_pool: 2,
259            workers_serving_activity: 2,
260            compatible_workers: 2,
261            eligible_compatible_workers: 0,
262            compatible_workers_reachability_lost: 0,
263            compatible_workers_at_capacity: 2,
264            compatible_workers_capacity_unannounced: 0,
265            last_compatible_poller_age: Some(Duration::ZERO),
266        };
267        assert_eq!(
268            classify(QueueDeclaration::Declared, &census),
269            Some(QueueServiceReason::PollersAtCapacity)
270        );
271        // The discriminating half: the SAME pool with nobody connected is the
272        // reason this arm exists to stop being confused with.
273        assert_eq!(
274            classify(QueueDeclaration::Declared, &PoolCensus::default()),
275            Some(QueueServiceReason::NoLivePollers)
276        );
277        // And a busy pool is one a dispatch should WAIT on: nothing about it
278        // needs an operator, so the caller must not be told it will not be
279        // served.
280        assert!(census.will_be_served());
281    }
282
283    /// A pool whose workers have not announced their capacity is reported as
284    /// UNANNOUNCED, never as busy.
285    ///
286    /// Selection excludes both by the same test — unknown capacity is treated as
287    /// full — so this is exactly the confusion the separate count exists to
288    /// prevent. Reporting these workers as at-capacity would tell an operator
289    /// their idle, mute fleet was busy, and hand them the advice for a busy pool
290    /// ("start another worker") for a condition another worker of the same build
291    /// reproduces exactly.
292    #[test]
293    fn a_pool_whose_workers_have_not_announced_capacity_is_not_reported_as_busy() {
294        let census = PoolCensus {
295            workers_in_pool: 2,
296            workers_serving_activity: 2,
297            compatible_workers: 2,
298            eligible_compatible_workers: 0,
299            compatible_workers_reachability_lost: 0,
300            compatible_workers_at_capacity: 0,
301            compatible_workers_capacity_unannounced: 2,
302            last_compatible_poller_age: Some(Duration::ZERO),
303        };
304        assert_eq!(
305            classify(QueueDeclaration::Declared, &census),
306            Some(QueueServiceReason::PollersCapacityUnannounced)
307        );
308        assert_ne!(
309            classify(QueueDeclaration::Declared, &census),
310            Some(QueueServiceReason::PollersAtCapacity),
311            "an unannounced pool must never wear the busy pool's name: their remedies differ"
312        );
313        // And it must NOT be reported as "will be served". Nothing about an
314        // unannounced pool improves on its own: either the announcement has
315        // already landed — in which case these workers are not in this bucket —
316        // or it never will. Answering `true` here told a caller that waiting was
317        // enough for a fleet that is registered, idle, and permanently
318        // unselectable.
319        assert!(
320            !census.will_be_served(),
321            "an all-unannounced pool is not going to serve anything by itself"
322        );
323    }
324
325    /// One unannounced worker in an otherwise busy pool still reports
326    /// UNANNOUNCED.
327    ///
328    /// The precedence is the subject. Both conditions are true here and only one
329    /// reason can be published; the unannounced one wins because it is the one
330    /// that may not clear on its own. A busy pool frees a slot as work
331    /// completes; a worker that has not announced past one round trip never will
332    /// without being read or replaced.
333    #[test]
334    fn an_unannounced_worker_outranks_a_busy_one() {
335        let census = PoolCensus {
336            workers_in_pool: 3,
337            workers_serving_activity: 3,
338            compatible_workers: 3,
339            eligible_compatible_workers: 0,
340            compatible_workers_reachability_lost: 0,
341            compatible_workers_at_capacity: 2,
342            compatible_workers_capacity_unannounced: 1,
343            last_compatible_poller_age: Some(Duration::ZERO),
344        };
345        assert_eq!(
346            classify(QueueDeclaration::Declared, &census),
347            Some(QueueServiceReason::PollersCapacityUnannounced)
348        );
349    }
350
351    /// The vacuity control for both tests above: with nobody unannounced, a full
352    /// pool still reports AT CAPACITY.
353    ///
354    /// Without this, a precedence bug that returned `PollersCapacityUnannounced`
355    /// unconditionally would satisfy every assertion above.
356    #[test]
357    fn a_busy_pool_with_nobody_unannounced_still_reports_at_capacity() {
358        let census = PoolCensus {
359            workers_in_pool: 2,
360            workers_serving_activity: 2,
361            compatible_workers: 2,
362            eligible_compatible_workers: 0,
363            compatible_workers_reachability_lost: 0,
364            compatible_workers_at_capacity: 2,
365            compatible_workers_capacity_unannounced: 0,
366            last_compatible_poller_age: Some(Duration::ZERO),
367        };
368        assert_eq!(
369            classify(QueueDeclaration::Declared, &census),
370            Some(QueueServiceReason::PollersAtCapacity)
371        );
372    }
373
374    /// A pool that is full AND has lost reachability reports the incident, not
375    /// the capacity: an unreachable worker is not going to free a slot, so
376    /// "wait for capacity" would be advice to wait for something that is not
377    /// coming.
378    #[test]
379    fn lost_reachability_outranks_capacity() {
380        let census = PoolCensus {
381            workers_in_pool: 2,
382            workers_serving_activity: 2,
383            compatible_workers: 2,
384            eligible_compatible_workers: 0,
385            compatible_workers_reachability_lost: 1,
386            compatible_workers_at_capacity: 2,
387            compatible_workers_capacity_unannounced: 0,
388            last_compatible_poller_age: Some(Duration::ZERO),
389        };
390        assert_eq!(
391            classify(QueueDeclaration::Declared, &census),
392            Some(QueueServiceReason::PollersUnreachable)
393        );
394    }
395
396    /// A MIXED pool — one worker full, one still serving its opening
397    /// probation — is not reported at all. The probation clears itself within
398    /// seconds and is the ordinary cost of connecting, so announcing anything
399    /// here would fire on every healthy worker start.
400    #[test]
401    fn a_pool_only_partly_at_capacity_is_not_reported() {
402        let census = PoolCensus {
403            workers_in_pool: 2,
404            workers_serving_activity: 2,
405            compatible_workers: 2,
406            eligible_compatible_workers: 0,
407            compatible_workers_reachability_lost: 0,
408            compatible_workers_at_capacity: 1,
409            compatible_workers_capacity_unannounced: 0,
410            last_compatible_poller_age: Some(Duration::ZERO),
411        };
412        assert_eq!(classify(QueueDeclaration::Declared, &census), None);
413    }
414
415    /// A pool of `compatible` workers of whom `eligible` can be dispatched to,
416    /// with `lost` of the remainder excluded for lost reachability rather than
417    /// for an opening probation.
418    fn pool(compatible: usize, eligible: usize, lost: usize) -> PoolCensus {
419        PoolCensus {
420            workers_in_pool: compatible,
421            workers_serving_activity: compatible,
422            compatible_workers: compatible,
423            eligible_compatible_workers: eligible,
424            compatible_workers_reachability_lost: lost,
425            last_compatible_poller_age: Some(Duration::ZERO),
426            compatible_workers_at_capacity: 0,
427            compatible_workers_capacity_unannounced: 0,
428        }
429    }
430
431    /// A pool whose workers are all still serving their opening probation is
432    /// NOT reported: the exclusion clears itself within seconds, and reporting
433    /// it would fire on every healthy worker's first moments.
434    #[test]
435    fn a_pool_excluded_only_by_an_opening_probation_is_not_reported() {
436        assert_eq!(classify(QueueDeclaration::Declared, &pool(2, 0, 0)), None);
437    }
438
439    /// The same pool, excluded for lost reachability, IS reported — because
440    /// that exclusion does not clear on its own, so a dispatch parked on it
441    /// waits indefinitely with nothing said.
442    #[test]
443    fn a_pool_that_lost_reachability_is_reported() {
444        assert_eq!(
445            classify(QueueDeclaration::Declared, &pool(2, 0, 2)),
446            Some(QueueServiceReason::PollersUnreachable)
447        );
448    }
449
450    /// 🔴 THE DISCRIMINATOR, and the reason the two tests above are not enough
451    /// on their own.
452    ///
453    /// Each of them asserts one census against one expected answer, and BOTH
454    /// would still pass if `classify` had been written to ignore the cause and
455    /// return a constant — the first passes on a constant `None`, the second
456    /// on a constant `Some(PollersUnreachable)`. Neither witnesses that the
457    /// CAUSE is what decides.
458    ///
459    /// This one does: two censuses identical in every field except the
460    /// exclusion cause must produce different answers. That is the property the
461    /// whole change exists for, and it cannot be satisfied by a constant.
462    #[test]
463    fn the_exclusion_cause_is_what_decides() {
464        let probation = pool(2, 0, 0);
465        let unreachable = pool(2, 0, 2);
466        assert_eq!(
467            probation.workers_in_pool, unreachable.workers_in_pool,
468            "precondition: the two pools differ ONLY in the exclusion cause"
469        );
470        assert_eq!(probation.compatible_workers, unreachable.compatible_workers);
471        assert_eq!(
472            probation.eligible_compatible_workers,
473            unreachable.eligible_compatible_workers
474        );
475
476        assert_ne!(
477            classify(QueueDeclaration::Declared, &probation),
478            classify(QueueDeclaration::Declared, &unreachable),
479            "a pool serving its probation and a pool that lost reachability must not get the \
480             same answer: one clears itself in seconds and the other never does, and the \
481             operator's remedies are different"
482        );
483    }
484
485    /// 🔴 THE CONTROL for the reporting test, and it is not optional.
486    ///
487    /// Without it, a `classify` hard-coded to report `PollersUnreachable`
488    /// whenever nothing is dispatchable would satisfy every test above AND
489    /// misdiagnose a genuinely empty pool — telling an operator that starting
490    /// the worker they need would not help, which is the opposite of the truth.
491    #[test]
492    fn a_genuinely_empty_pool_is_still_reported_as_having_no_pollers() {
493        assert_eq!(
494            classify(QueueDeclaration::Declared, &PoolCensus::default()),
495            Some(QueueServiceReason::NoLivePollers)
496        );
497    }
498
499    /// The second control: workers present that do NOT serve the activity are
500    /// still `PollersIncompatible`. The new arm sits ahead of that one, so a
501    /// mistake in its guard would swallow this case and send an operator to
502    /// read a liveness verdict for a worker whose only problem is that it does
503    /// not cover the activity at all.
504    #[test]
505    fn workers_that_do_not_serve_the_activity_are_still_incompatible_not_unreachable() {
506        let census = PoolCensus {
507            workers_in_pool: 3,
508            workers_serving_activity: 0,
509            compatible_workers: 0,
510            eligible_compatible_workers: 0,
511            compatible_workers_reachability_lost: 0,
512            last_compatible_poller_age: None,
513            compatible_workers_at_capacity: 0,
514            compatible_workers_capacity_unannounced: 0,
515        };
516        assert_eq!(
517            classify(QueueDeclaration::Declared, &census),
518            Some(QueueServiceReason::PollersIncompatible)
519        );
520    }
521
522    /// One eligible worker is enough: the address is served and the caller
523    /// simply retries, whatever the others are excluded for.
524    #[test]
525    fn one_eligible_worker_means_the_address_is_served() {
526        assert_eq!(classify(QueueDeclaration::Declared, &pool(3, 1, 2)), None);
527    }
528
529    #[test]
530    fn pollers_that_do_not_cover_the_activity_are_incompatible() {
531        let census = PoolCensus {
532            workers_in_pool: 2,
533            workers_serving_activity: 0,
534            compatible_workers: 0,
535            eligible_compatible_workers: 0,
536            compatible_workers_reachability_lost: 0,
537            last_compatible_poller_age: None,
538            compatible_workers_at_capacity: 0,
539            compatible_workers_capacity_unannounced: 0,
540        };
541        assert_eq!(
542            classify(QueueDeclaration::Declared, &census),
543            Some(QueueServiceReason::PollersIncompatible)
544        );
545    }
546
547    #[test]
548    fn pollers_off_the_pinned_node_are_incompatible() {
549        let census = PoolCensus {
550            workers_in_pool: 1,
551            workers_serving_activity: 1,
552            compatible_workers: 0,
553            eligible_compatible_workers: 0,
554            compatible_workers_reachability_lost: 0,
555            last_compatible_poller_age: Some(Duration::from_secs(9)),
556            compatible_workers_at_capacity: 0,
557            compatible_workers_capacity_unannounced: 0,
558        };
559        assert_eq!(
560            classify(QueueDeclaration::Declared, &census),
561            Some(QueueServiceReason::PollersIncompatible)
562        );
563    }
564}