saddle_runtime/
request.rs1use std::sync::{
2 Arc,
3 atomic::{AtomicUsize, Ordering},
4};
5
6use saddle_core::{ErrorKind, Result, SaddleError};
7use tokio::sync::Notify;
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum ApplicationPhase {
12 Starting = 0,
13 Ready = 1,
14 Draining = 2,
15 Stopped = 3,
16}
17
18const PHASE_SHIFT: u32 = usize::BITS - 2;
19const COUNT_MASK: usize = (1 << PHASE_SHIFT) - 1;
20
21#[derive(Debug)]
22struct Shared {
23 state: AtomicUsize,
27 drained: Notify,
28}
29
30#[derive(Clone, Debug)]
36pub struct RequestLifecycle {
37 shared: Arc<Shared>,
38}
39
40impl RequestLifecycle {
41 pub(crate) fn new() -> Self {
42 Self {
43 shared: Arc::new(Shared {
44 state: AtomicUsize::new(encode(ApplicationPhase::Starting, 0)),
45 drained: Notify::new(),
46 }),
47 }
48 }
49
50 pub fn phase(&self) -> ApplicationPhase {
52 phase(self.shared.state.load(Ordering::Acquire))
53 }
54
55 pub fn try_accept(&self) -> Result<RequestGuard> {
61 self.try_claim().map(RequestClaim::publish)
62 }
63
64 pub(crate) fn try_claim(&self) -> Result<RequestClaim> {
69 let mut current = self.shared.state.load(Ordering::Acquire);
70 loop {
71 match phase(current) {
72 ApplicationPhase::Ready => {
73 if count(current) == COUNT_MASK {
74 return Err(SaddleError::new(
75 ErrorKind::Internal,
76 "runtime.request_count_overflow",
77 "request accounting capacity exhausted",
78 ));
79 }
80 match self.shared.state.compare_exchange_weak(
81 current,
82 current + 1,
83 Ordering::AcqRel,
84 Ordering::Acquire,
85 ) {
86 Ok(_) => {
87 return Ok(RequestClaim {
88 shared: Some(Arc::clone(&self.shared)),
89 });
90 }
91 Err(observed) => current = observed,
92 }
93 }
94 ApplicationPhase::Starting => {
95 return Err(SaddleError::new(
96 ErrorKind::Unavailable,
97 "runtime.not_ready",
98 "application is not ready",
99 ));
100 }
101 ApplicationPhase::Draining => {
102 return Err(SaddleError::new(
103 ErrorKind::Unavailable,
104 "runtime.shutting_down",
105 "application is shutting down",
106 ));
107 }
108 ApplicationPhase::Stopped => {
109 return Err(SaddleError::new(
110 ErrorKind::Unavailable,
111 "runtime.stopped",
112 "application is stopped",
113 ));
114 }
115 }
116 }
117 }
118
119 pub(crate) fn mark_ready(&self) {
120 let result = self.shared.state.compare_exchange(
121 encode(ApplicationPhase::Starting, 0),
122 encode(ApplicationPhase::Ready, 0),
123 Ordering::AcqRel,
124 Ordering::Acquire,
125 );
126 debug_assert!(result.is_ok());
127 }
128
129 pub(crate) fn begin_draining(&self) {
130 let mut current = self.shared.state.load(Ordering::Acquire);
131 while matches!(
132 phase(current),
133 ApplicationPhase::Starting | ApplicationPhase::Ready
134 ) {
135 let draining = encode(ApplicationPhase::Draining, count(current));
136 match self.shared.state.compare_exchange_weak(
137 current,
138 draining,
139 Ordering::AcqRel,
140 Ordering::Acquire,
141 ) {
142 Ok(_) => {
143 current = draining;
144 break;
145 }
146 Err(observed) => current = observed,
147 }
148 }
149 if count(current) == 0 {
150 self.shared.drained.notify_one();
151 }
152 }
153
154 pub(crate) async fn wait_until_drained(&self) {
155 loop {
156 if count(self.shared.state.load(Ordering::Acquire)) == 0 {
157 return;
158 }
159 self.shared.drained.notified().await;
160 }
161 }
162
163 pub(crate) fn mark_stopped(&self) {
164 let result = self.shared.state.compare_exchange(
165 encode(ApplicationPhase::Draining, 0),
166 encode(ApplicationPhase::Stopped, 0),
167 Ordering::AcqRel,
168 Ordering::Acquire,
169 );
170 debug_assert!(result.is_ok());
171 }
172}
173
174const fn encode(phase: ApplicationPhase, count: usize) -> usize {
175 ((phase as usize) << PHASE_SHIFT) | count
176}
177
178const fn phase(state: usize) -> ApplicationPhase {
179 match state >> PHASE_SHIFT {
180 0 => ApplicationPhase::Starting,
181 1 => ApplicationPhase::Ready,
182 2 => ApplicationPhase::Draining,
183 3 => ApplicationPhase::Stopped,
184 _ => unreachable!(),
185 }
186}
187
188const fn count(state: usize) -> usize {
189 state & COUNT_MASK
190}
191
192#[derive(Debug)]
194pub(crate) struct RequestClaim {
195 shared: Option<Arc<Shared>>,
196}
197
198impl RequestClaim {
199 pub(crate) fn publish(mut self) -> RequestGuard {
200 RequestGuard {
201 shared: self.shared.take().expect("request claim publishes once"),
202 }
203 }
204}
205
206impl Drop for RequestClaim {
207 fn drop(&mut self) {
208 if let Some(shared) = self.shared.take() {
209 complete(&shared);
210 }
211 }
212}
213
214#[derive(Debug)]
216pub struct RequestGuard {
217 shared: Arc<Shared>,
218}
219
220impl Drop for RequestGuard {
221 fn drop(&mut self) {
222 complete(&self.shared);
223 }
224}
225
226fn complete(shared: &Shared) {
227 let previous = shared.state.fetch_sub(1, Ordering::AcqRel);
228 debug_assert!(count(previous) > 0);
229 if count(previous) == 1 && phase(previous) == ApplicationPhase::Draining {
230 shared.drained.notify_one();
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use std::sync::{Arc, Barrier};
237
238 use super::*;
239
240 fn test_runtime() -> tokio::runtime::Runtime {
241 tokio::runtime::Builder::new_current_thread()
242 .build()
243 .expect("test runtime must build")
244 }
245
246 #[test]
247 fn only_ready_applications_accept_requests() {
248 let requests = RequestLifecycle::new();
249 assert_eq!(
250 requests.try_accept().unwrap_err().code(),
251 "runtime.not_ready"
252 );
253
254 requests.mark_ready();
255 let request = requests.try_accept().expect("ready request is admitted");
256 requests.begin_draining();
257
258 assert_eq!(requests.phase(), ApplicationPhase::Draining);
259 assert_eq!(
260 requests.try_accept().unwrap_err().code(),
261 "runtime.shutting_down"
262 );
263
264 drop(request);
265 test_runtime().block_on(requests.wait_until_drained());
266 requests.mark_stopped();
267 assert_eq!(requests.phase(), ApplicationPhase::Stopped);
268 }
269
270 #[test]
271 fn draining_waits_for_every_admitted_request() {
272 test_runtime().block_on(async {
273 let requests = RequestLifecycle::new();
274 requests.mark_ready();
275 let first = requests.try_accept().unwrap();
276 let second = requests.try_accept().unwrap();
277 requests.begin_draining();
278
279 let requests_for_waiter = requests.clone();
280 let waiter = tokio::spawn(async move {
281 requests_for_waiter.wait_until_drained().await;
282 });
283 tokio::task::yield_now().await;
284 assert!(!waiter.is_finished());
285
286 drop(first);
287 tokio::task::yield_now().await;
288 assert!(!waiter.is_finished());
289
290 drop(second);
291 waiter.await.unwrap();
292 });
293 }
294
295 #[test]
296 fn admission_racing_with_drain_never_leaks_a_request() {
297 const WORKERS: usize = 8;
298 let requests = RequestLifecycle::new();
299 requests.mark_ready();
300 let barrier = Arc::new(Barrier::new(WORKERS + 1));
301 let workers: Vec<_> = (0..WORKERS)
302 .map(|_| {
303 let requests = requests.clone();
304 let barrier = Arc::clone(&barrier);
305 std::thread::spawn(move || {
306 let admitted_before_drain = requests.try_accept().unwrap();
307 barrier.wait();
308 loop {
309 match requests.try_accept() {
310 Ok(request) => drop(request),
311 Err(error) => {
312 assert_eq!(error.code(), "runtime.shutting_down");
313 drop(admitted_before_drain);
314 break;
315 }
316 }
317 }
318 })
319 })
320 .collect();
321
322 barrier.wait();
323 requests.begin_draining();
324 for worker in workers {
325 worker.join().unwrap();
326 }
327 test_runtime().block_on(requests.wait_until_drained());
328 assert_eq!(count(requests.shared.state.load(Ordering::Acquire)), 0);
329 }
330
331 #[test]
332 fn guard_completion_is_safe_from_another_thread() {
333 let requests = RequestLifecycle::new();
334 requests.mark_ready();
335 let guard = requests.try_accept().unwrap();
336 requests.begin_draining();
337
338 std::thread::spawn(move || drop(guard)).join().unwrap();
339 test_runtime().block_on(requests.wait_until_drained());
340
341 assert_eq!(Arc::strong_count(&requests.shared), 1);
344 }
345}