aion_server/worker/queue_service/
delivery.rs1use std::time::{Duration, Instant};
13
14use tokio::sync::mpsc::error::TrySendError;
15
16use crate::worker::registry::{WorkerMessage, WorkerTaskSender};
17
18pub const PARK_POLL_INTERVAL: Duration = Duration::from_millis(500);
26
27#[derive(Clone, Debug, Eq, PartialEq)]
29pub enum DeliveryRefusal {
30 Saturated {
33 waited: Duration,
35 },
36 Full,
39 Closed,
41 NotAccepting {
43 reason: String,
45 },
46}
47
48pub fn deliver_within_schedule_to_start(
59 sender: &WorkerTaskSender,
60 message: WorkerMessage,
61 schedule_to_start: Option<Duration>,
62 accepting: &mut dyn FnMut() -> Result<(), String>,
63) -> Result<(), DeliveryRefusal> {
64 let started_at = Instant::now();
65 let mut pending = message;
66 loop {
67 match sender.try_send(pending) {
68 Ok(()) => return Ok(()),
69 Err(TrySendError::Closed(_)) => return Err(DeliveryRefusal::Closed),
70 Err(TrySendError::Full(returned)) => {
71 let Some(budget) = schedule_to_start else {
72 return Err(DeliveryRefusal::Full);
73 };
74 let waited = started_at.elapsed();
75 let Some(remaining) = budget.checked_sub(waited) else {
76 return Err(DeliveryRefusal::Saturated { waited });
77 };
78 if remaining.is_zero() {
79 return Err(DeliveryRefusal::Saturated { waited });
80 }
81 if let Err(reason) = accepting() {
82 return Err(DeliveryRefusal::NotAccepting { reason });
83 }
84 std::thread::sleep(remaining.min(PARK_POLL_INTERVAL));
85 pending = returned;
86 }
87 }
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 fn task() -> WorkerMessage {
96 WorkerMessage::ActivityTask(Box::default())
97 }
98
99 fn always_accepting() -> impl FnMut() -> Result<(), String> {
100 || Ok(())
101 }
102
103 #[test]
104 fn a_free_intake_accepts_the_task_immediately() -> Result<(), String> {
105 let (sender, mut receiver) = tokio::sync::mpsc::channel(1);
106 let mut accepting = always_accepting();
107 deliver_within_schedule_to_start(&sender, task(), None, &mut accepting)
108 .map_err(|refusal| format!("unexpected refusal: {refusal:?}"))?;
109 receiver
110 .try_recv()
111 .map_err(|error| format!("task never reached the worker: {error}"))?;
112 Ok(())
113 }
114
115 #[test]
116 fn a_full_intake_with_no_clock_fails_at_once() {
117 let (sender, _receiver) = tokio::sync::mpsc::channel(1);
118 let mut accepting = always_accepting();
119 assert!(
120 sender.try_send(task()).is_ok(),
121 "the fixture must fill the single intake slot"
122 );
123 let started = Instant::now();
124 let refusal = deliver_within_schedule_to_start(&sender, task(), None, &mut accepting);
125 assert_eq!(refusal, Err(DeliveryRefusal::Full));
126 assert!(
127 started.elapsed() < PARK_POLL_INTERVAL,
128 "the unclocked path must not wait"
129 );
130 }
131
132 #[test]
133 fn a_closed_intake_is_never_reported_as_saturation() {
134 let (sender, receiver) = tokio::sync::mpsc::channel(1);
135 drop(receiver);
136 let mut accepting = always_accepting();
137 let refusal = deliver_within_schedule_to_start(
138 &sender,
139 task(),
140 Some(Duration::from_secs(30)),
141 &mut accepting,
142 );
143 assert_eq!(refusal, Err(DeliveryRefusal::Closed));
144 }
145
146 #[test]
149 fn an_intake_that_stays_full_past_the_clock_is_saturated() {
150 let (sender, _receiver) = tokio::sync::mpsc::channel(1);
151 let mut accepting = always_accepting();
152 assert!(sender.try_send(task()).is_ok(), "fixture must fill intake");
153 let budget = Duration::from_millis(300);
154 let started = Instant::now();
155 let refusal =
156 deliver_within_schedule_to_start(&sender, task(), Some(budget), &mut accepting);
157 let elapsed = started.elapsed();
158 assert!(
159 matches!(refusal, Err(DeliveryRefusal::Saturated { waited }) if waited >= budget),
160 "expected saturation after at least {budget:?}, got {refusal:?}"
161 );
162 assert!(
163 elapsed < Duration::from_secs(5),
164 "saturation must be bounded by the clock, took {elapsed:?}"
165 );
166 }
167
168 #[test]
171 fn an_intake_that_frees_up_within_the_clock_is_served() -> Result<(), String> {
172 let (sender, mut receiver) = tokio::sync::mpsc::channel(1);
173 let mut accepting = always_accepting();
174 sender
175 .try_send(task())
176 .map_err(|error| format!("fixture fill failed: {error}"))?;
177 let drainer = std::thread::spawn(move || {
178 std::thread::sleep(Duration::from_millis(100));
179 let taken = receiver.try_recv().is_ok();
180 (receiver, taken)
181 });
182 let outcome = deliver_within_schedule_to_start(
183 &sender,
184 task(),
185 Some(Duration::from_secs(5)),
186 &mut accepting,
187 );
188 let (_receiver, taken) = drainer.join().map_err(|_| "drainer thread panicked")?;
189 assert!(taken, "the fixture drainer never freed the intake slot");
190 assert_eq!(outcome, Ok(()));
191 Ok(())
192 }
193
194 #[test]
196 fn a_drain_during_the_retry_window_fails_with_the_drain_reason() {
197 let (sender, _receiver) = tokio::sync::mpsc::channel(1);
198 assert!(sender.try_send(task()).is_ok(), "fixture must fill intake");
199 let mut accepting = || Err("server draining".to_owned());
200 let refusal = deliver_within_schedule_to_start(
201 &sender,
202 task(),
203 Some(Duration::from_secs(30)),
204 &mut accepting,
205 );
206 assert_eq!(
207 refusal,
208 Err(DeliveryRefusal::NotAccepting {
209 reason: "server draining".to_owned()
210 })
211 );
212 }
213}