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>;