Skip to main content

aion_server/worker/queue_service/
declarations.rs

1//! Whether a task queue is declared by any deployed contract.
2//!
3//! This reads the SAME durable `.v4` worker-contract records the registration
4//! handshake admits workers against (`crate::worker::contracts`) and workflow
5//! start admission refuses against (`aion::lifecycle` `NoQueueDeclaration`) —
6//! there is no second queue-declaration truth in the server.
7//!
8//! Fail-open by construction. A server whose catalog declares no queues at all
9//! (an engine-less state built from parts, a fresh boot, an embedded engine
10//! serving activities in process) can contradict nothing, so it answers
11//! [`QueueDeclaration::Unknown`] and no structural refusal is ever
12//! manufactured from an absence of information. The same holds for a catalog
13//! read that could not decode every retained entry — the mixed state the
14//! handshake migration creates, old packages retained beside new `.v4` ones:
15//! what such a read did not find, it may simply not have been able to see.
16//! That is the same discipline the handshake lane adopted after its
17//! engine-less registration defect — whose observed symptom was this very
18//! seam waiting forever at INFO.
19
20use std::sync::{Arc, OnceLock};
21
22use aion::loader::DeclaredQueues;
23
24/// What the deployed contract records say about a task queue.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum QueueDeclaration {
27    /// At least one retained `.v4` contract declares this queue.
28    Declared,
29    /// Contracts declaring queues exist, a read covered every retained entry,
30    /// and none of them declares this one.
31    NotDeclared,
32    /// Nothing in reach can answer: no source installed, no queue-declaring
33    /// contract deployed at all, a read that could not decode every retained
34    /// entry, or a catalog that could not be read.
35    Unknown,
36}
37
38/// A reader over the deployed queue declarations.
39pub trait QueueDeclarations: Send + Sync {
40    /// Answer for one task queue. Implementations never fail: an unreadable
41    /// catalog is [`QueueDeclaration::Unknown`], reported by the implementation.
42    fn declaration_for(&self, task_queue: &str) -> QueueDeclaration;
43}
44
45/// Shared, install-once handle the bridge holds from construction and the boot
46/// path fills in once the engine exists.
47///
48/// Mirrors the outbox-delivery callback install: the dispatcher is built before
49/// the engine, so the seam it will consult is handed over afterwards through a
50/// clone of this handle rather than by rebuilding the dispatcher.
51#[derive(Clone, Default)]
52pub struct QueueDeclarationSource {
53    inner: Arc<OnceLock<Arc<dyn QueueDeclarations>>>,
54}
55
56impl std::fmt::Debug for QueueDeclarationSource {
57    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        formatter
59            .debug_struct("QueueDeclarationSource")
60            .field("installed", &self.inner.get().is_some())
61            .finish()
62    }
63}
64
65impl QueueDeclarationSource {
66    /// Install the reader. A second install is ignored and logged: the source
67    /// is process-wide and must not silently change identity.
68    pub fn install(&self, source: Arc<dyn QueueDeclarations>) {
69        if self.inner.set(source).is_err() {
70            tracing::warn!("queue declaration source already installed; ignoring duplicate set");
71        }
72    }
73
74    /// Whether a reader has been installed.
75    #[must_use]
76    pub fn is_installed(&self) -> bool {
77        self.inner.get().is_some()
78    }
79
80    /// Answer for one task queue, or [`QueueDeclaration::Unknown`] when no
81    /// reader is installed.
82    #[must_use]
83    pub fn declaration_for(&self, task_queue: &str) -> QueueDeclaration {
84        self.inner
85            .get()
86            .map_or(QueueDeclaration::Unknown, |source| {
87                source.declaration_for(task_queue)
88            })
89    }
90}
91
92/// Reads declarations out of the engine's live workflow catalog.
93pub struct EngineQueueDeclarations {
94    engine: Arc<aion::Engine>,
95}
96
97impl EngineQueueDeclarations {
98    /// Build a reader over `engine`'s catalog.
99    #[must_use]
100    pub const fn new(engine: Arc<aion::Engine>) -> Self {
101        Self { engine }
102    }
103}
104
105impl std::fmt::Debug for EngineQueueDeclarations {
106    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        formatter.write_str("EngineQueueDeclarations")
108    }
109}
110
111/// Maps one catalog read to the answer for `task_queue`.
112///
113/// A positive find is positive whatever else the read missed. An ABSENCE is
114/// only reported as [`QueueDeclaration::NotDeclared`] when the read has
115/// standing to make that claim: a catalog that declares no queue at all
116/// contradicts nothing, and neither does a read that could not decode every
117/// retained entry — the entry it could not read may be the one that declares
118/// this very queue.
119fn declaration_from(read: &DeclaredQueues, task_queue: &str) -> QueueDeclaration {
120    if read.declares(task_queue) {
121        QueueDeclaration::Declared
122    } else if read.found_no_declaration() || !read.covers_every_entry() {
123        QueueDeclaration::Unknown
124    } else {
125        QueueDeclaration::NotDeclared
126    }
127}
128
129impl QueueDeclarations for EngineQueueDeclarations {
130    fn declaration_for(&self, task_queue: &str) -> QueueDeclaration {
131        match self.engine.declared_task_queues() {
132            Ok(read) => declaration_from(&read, task_queue),
133            Err(error) => {
134                tracing::warn!(
135                    task_queue,
136                    %error,
137                    "queue declaration lookup failed; treating the declaration as unknown"
138                );
139                QueueDeclaration::Unknown
140            }
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use std::collections::BTreeSet;
148
149    use super::super::census::{PoolCensus, classify};
150    use super::super::taxonomy::QueueServiceReason;
151    use super::*;
152
153    fn queues(names: &[&str]) -> BTreeSet<String> {
154        names.iter().map(|name| (*name).to_owned()).collect()
155    }
156
157    /// A catalog that decoded every entry has standing to say "nobody declares
158    /// this queue", and that refusal stays structural.
159    #[test]
160    fn a_complete_read_still_reports_an_absent_queue_as_undeclared() {
161        let read = DeclaredQueues::new(queues(&["orders"]), Vec::new());
162        assert_eq!(
163            declaration_from(&read, "checkout"),
164            QueueDeclaration::NotDeclared
165        );
166        assert_eq!(
167            declaration_from(&read, "orders"),
168            QueueDeclaration::Declared
169        );
170    }
171
172    #[test]
173    fn a_complete_read_that_found_nothing_is_unknown() {
174        let read = DeclaredQueues::new(BTreeSet::new(), Vec::new());
175        assert_eq!(declaration_from(&read, "orders"), QueueDeclaration::Unknown);
176    }
177
178    /// The blocker pin, mapping half. A read that could not decode every entry
179    /// has seen only part of the catalog: a queue it did not find may well be
180    /// declared by the entry it could not read.
181    #[test]
182    fn a_queue_absent_from_a_partial_read_is_never_undeclared() {
183        let read = DeclaredQueues::new(queues(&["orders"]), vec!["sha256:legacy".to_owned()]);
184        assert_eq!(
185            declaration_from(&read, "checkout"),
186            QueueDeclaration::Unknown
187        );
188    }
189
190    #[test]
191    fn a_positive_find_survives_a_partial_read() {
192        let read = DeclaredQueues::new(queues(&["orders"]), vec!["sha256:legacy".to_owned()]);
193        assert_eq!(
194            declaration_from(&read, "orders"),
195            QueueDeclaration::Declared
196        );
197    }
198
199    /// The blocker pin, consequence half. The mixed catalog the handshake
200    /// migration creates must never terminally refuse a dispatch to the
201    /// pre-`.v4` package's queue — it is a fleet condition, retryable and
202    /// serviceable the moment a compatible worker connects.
203    #[test]
204    fn a_mixed_catalog_never_structurally_refuses_the_undecodable_queue() {
205        let read = DeclaredQueues::new(queues(&["orders"]), vec!["sha256:legacy".to_owned()]);
206        let reason = classify(
207            declaration_from(&read, "checkout"),
208            &PoolCensus {
209                workers_in_pool: 1,
210                workers_serving_activity: 0,
211                compatible_workers: 0,
212                eligible_compatible_workers: 0,
213                compatible_workers_reachability_lost: 0,
214                last_compatible_poller_age: None,
215            },
216        );
217        assert_ne!(reason, Some(QueueServiceReason::NoQueueDeclaration));
218        assert_eq!(reason, Some(QueueServiceReason::PollersIncompatible));
219    }
220
221    struct FixedDeclarations(QueueDeclaration);
222
223    impl QueueDeclarations for FixedDeclarations {
224        fn declaration_for(&self, _task_queue: &str) -> QueueDeclaration {
225            self.0
226        }
227    }
228
229    #[test]
230    fn an_uninstalled_source_answers_unknown() {
231        let source = QueueDeclarationSource::default();
232        assert!(!source.is_installed());
233        assert_eq!(source.declaration_for("general"), QueueDeclaration::Unknown);
234    }
235
236    #[test]
237    fn an_installed_source_answers_and_is_visible_to_every_clone() {
238        let source = QueueDeclarationSource::default();
239        source
240            .clone()
241            .install(Arc::new(FixedDeclarations(QueueDeclaration::NotDeclared)));
242        assert!(source.is_installed());
243        assert_eq!(
244            source.declaration_for("general"),
245            QueueDeclaration::NotDeclared
246        );
247    }
248
249    #[test]
250    fn a_duplicate_install_never_changes_the_answer() {
251        let source = QueueDeclarationSource::default();
252        source.install(Arc::new(FixedDeclarations(QueueDeclaration::Declared)));
253        source.install(Arc::new(FixedDeclarations(QueueDeclaration::NotDeclared)));
254        assert_eq!(
255            source.declaration_for("general"),
256            QueueDeclaration::Declared
257        );
258    }
259}