Skip to main content

aion_server/worker/
delivery_intent.rs

1//! Whether the caller that started a delivery still wants it, asked again while
2//! the delivery waits.
3//!
4//! # Why this is the CALLER's state and not the transport's
5//!
6//! A liminal push blocks for the worker's correlated reply, and re-polls a
7//! predicate throughout that wait so a delivery nobody is waiting for any more
8//! can be abandoned instead of held open. Two of the questions that predicate
9//! asks belong to the transport — the deployment is draining, the worker is
10//! still registered — but the third does not: the outbox arm also asks whether
11//! **the pass that started this delivery still owns the row's claim**.
12//!
13//! That question cannot be answered from inside the transport, and the reason is
14//! one fact about [`DeliveryGate`](super::outbox_dispatcher::DeliveryGate): its
15//! `holds` is plain set membership, so **a key that was never begun reads
16//! identically to a key that was released**. A delivery dispatched from the
17//! worker dispatcher never takes a hold, so a transport that derived the row's
18//! dispatch key for itself would find `holds == false` at the delivery's very
19//! first poll and abandon before the worker could possibly reply — reported as
20//! `delivery wait abandoned`, which reads to an operator as a **worker** fault.
21//! Dropping the term instead would silently remove the outbox arm's live claim
22//! guard. Neither is acceptable, so the predicate travels as an argument.
23//!
24//! # Why a named trait and not a closure
25//!
26//! Each implementation below names **one caller** and states that caller's whole
27//! abandonment condition in one place. A bare `impl Fn() -> bool` would let any
28//! call site assemble a predicate inline, which is precisely how a dispatcher
29//! pass could silently borrow the outbox's claim check, or how "no abandonment
30//! condition" could be spelled `|| true` and never be found again. This is the
31//! same law [`Undeliverable`](super::task_delivery::Undeliverable) follows: the
32//! decision has a name, and one definition per caller.
33
34use std::sync::Arc;
35
36use super::outbox_dispatcher::DeliveryGate;
37use super::registry::{ConnectedWorkerRegistry, WorkerId};
38
39/// Asked repeatedly while a delivery waits: does the caller still want it?
40///
41/// Returning `false` abandons the wait. A late reply that arrives afterwards is
42/// discarded, so an implementation must only answer `false` when the caller
43/// genuinely no longer owns the delivery — not merely because it is slow.
44pub trait DeliveryIntent: Send + Sync {
45    /// `true` while the caller still wants this delivery to complete.
46    fn still_wanted(&self) -> bool;
47}
48
49/// The outbox pass's intent: it wants the delivery while the deployment is not
50/// draining, it still holds the row's claim, **and** the chosen worker is still
51/// registered.
52///
53/// The claim is the half no transport can ask about — see the module docs.
54///
55/// # Why the registration term is here and not left to the transport
56///
57/// An intent answers the caller's whole question. If a transport also applied
58/// terms of its own, `still_wanted()` would be a partial answer whose real
59/// meaning depended on which transport asked it, and the two halves could drift
60/// apart with nothing to catch it. So every term the outbox arm's original
61/// closure asked lives here — including the registration re-check, which that
62/// closure did ask (`liminal_transport.rs:1209-1220`) and which would otherwise
63/// be silently lost in the move.
64pub struct OutboxClaim {
65    gate: DeliveryGate,
66    dispatch_key: String,
67    registry: ConnectedWorkerRegistry,
68    worker: WorkerId,
69}
70
71impl std::fmt::Debug for OutboxClaim {
72    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        formatter
74            .debug_struct("OutboxClaim")
75            .field("dispatch_key", &self.dispatch_key)
76            .field("worker", &self.worker)
77            .finish_non_exhaustive()
78    }
79}
80
81impl OutboxClaim {
82    /// Bind this intent to the gate, the dispatch key of the row being
83    /// delivered, and the worker the pass selected.
84    #[must_use]
85    pub fn new(
86        gate: DeliveryGate,
87        dispatch_key: String,
88        registry: ConnectedWorkerRegistry,
89        worker: WorkerId,
90    ) -> Self {
91        Self {
92            gate,
93            dispatch_key,
94            registry,
95            worker,
96        }
97    }
98}
99
100impl DeliveryIntent for OutboxClaim {
101    fn still_wanted(&self) -> bool {
102        if self.gate.is_draining() || !self.gate.holds(&self.dispatch_key) {
103            return false;
104        }
105        worker_still_registered(&self.registry, self.worker)
106    }
107}
108
109/// The registration re-check both real intents apply, with one definition so the
110/// two cannot answer it differently — including on the error path, where an
111/// unreadable registry must not license a continued wait.
112fn worker_still_registered(registry: &ConnectedWorkerRegistry, worker: WorkerId) -> bool {
113    match registry.worker_by_id(worker) {
114        Ok(Some(_)) => true,
115        Ok(None) => false,
116        Err(error) => {
117            // A registry that cannot be read cannot license a continued wait:
118            // the honest answer is to abandon and let the caller's retry path
119            // run, loudly, rather than hold a delivery open on an unreadable
120            // premise. This mirrors the original closure's behaviour, warning
121            // line included.
122            tracing::warn!(
123                %error,
124                worker = ?worker,
125                "delivery wait could not verify worker registration; abandoning"
126            );
127            false
128        }
129    }
130}
131
132/// The worker dispatcher's intent: it wants the delivery while the deployment is
133/// not draining **and** the chosen worker is still registered.
134///
135/// 🔴 It deliberately does **not** consult the delivery gate's held keys. This
136/// pass never took a hold, and a never-held key is indistinguishable from a
137/// released one, so consulting it would abandon every dispatcher-originated
138/// delivery at its first poll.
139pub struct DispatcherPass {
140    gate: DeliveryGate,
141    registry: ConnectedWorkerRegistry,
142    worker: WorkerId,
143}
144
145impl std::fmt::Debug for DispatcherPass {
146    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        formatter
148            .debug_struct("DispatcherPass")
149            .field("worker", &self.worker)
150            .finish_non_exhaustive()
151    }
152}
153
154impl DispatcherPass {
155    /// Bind this intent to the deployment's drain gate and the worker this pass
156    /// selected.
157    #[must_use]
158    pub fn new(gate: DeliveryGate, registry: ConnectedWorkerRegistry, worker: WorkerId) -> Self {
159        Self {
160            gate,
161            registry,
162            worker,
163        }
164    }
165}
166
167impl DeliveryIntent for DispatcherPass {
168    fn still_wanted(&self) -> bool {
169        if self.gate.is_draining() {
170            return false;
171        }
172        match self.registry.worker_by_id(self.worker) {
173            Ok(Some(_)) => true,
174            Ok(None) => false,
175            Err(error) => {
176                // A registry that cannot be read cannot license a continued
177                // wait: the honest answer is to abandon and let the caller's
178                // retry path run, loudly, rather than hold a delivery open on an
179                // unreadable premise.
180                tracing::warn!(
181                    %error,
182                    worker = ?self.worker,
183                    "delivery wait could not verify worker registration; abandoning"
184                );
185                false
186            }
187        }
188    }
189}
190
191/// A caller with no abandonment condition at all.
192///
193/// Named, rather than spelled `|| true` at a call site, so that the decision is
194/// **visible and greppable**: every place a delivery is allowed to wait
195/// unconditionally says so by naming this type.
196#[derive(Debug, Clone, Copy, Default)]
197pub struct AlwaysWanted;
198
199impl DeliveryIntent for AlwaysWanted {
200    fn still_wanted(&self) -> bool {
201        true
202    }
203}
204
205/// Shared handle to a caller's intent, for the delivery paths that must move it
206/// onto a blocking thread.
207pub type SharedDeliveryIntent = Arc<dyn DeliveryIntent>;