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 let mut current = self.shared.state.load(Ordering::Acquire);
62 loop {
63 match phase(current) {
64 ApplicationPhase::Ready => {
65 if count(current) == COUNT_MASK {
66 return Err(SaddleError::new(
67 ErrorKind::Internal,
68 "runtime.request_count_overflow",
69 "request accounting capacity exhausted",
70 ));
71 }
72 match self.shared.state.compare_exchange_weak(
73 current,
74 current + 1,
75 Ordering::AcqRel,
76 Ordering::Acquire,
77 ) {
78 Ok(_) => {
79 return Ok(RequestGuard {
80 shared: Arc::clone(&self.shared),
81 });
82 }
83 Err(observed) => current = observed,
84 }
85 }
86 ApplicationPhase::Starting => {
87 return Err(SaddleError::new(
88 ErrorKind::Unavailable,
89 "runtime.not_ready",
90 "application is not ready",
91 ));
92 }
93 ApplicationPhase::Draining => {
94 return Err(SaddleError::new(
95 ErrorKind::Unavailable,
96 "runtime.shutting_down",
97 "application is shutting down",
98 ));
99 }
100 ApplicationPhase::Stopped => {
101 return Err(SaddleError::new(
102 ErrorKind::Unavailable,
103 "runtime.stopped",
104 "application is stopped",
105 ));
106 }
107 }
108 }
109 }
110
111 pub(crate) fn mark_ready(&self) {
112 let result = self.shared.state.compare_exchange(
113 encode(ApplicationPhase::Starting, 0),
114 encode(ApplicationPhase::Ready, 0),
115 Ordering::AcqRel,
116 Ordering::Acquire,
117 );
118 debug_assert!(result.is_ok());
119 }
120
121 pub(crate) fn begin_draining(&self) {
122 let mut current = self.shared.state.load(Ordering::Acquire);
123 while matches!(
124 phase(current),
125 ApplicationPhase::Starting | ApplicationPhase::Ready
126 ) {
127 let draining = encode(ApplicationPhase::Draining, count(current));
128 match self.shared.state.compare_exchange_weak(
129 current,
130 draining,
131 Ordering::AcqRel,
132 Ordering::Acquire,
133 ) {
134 Ok(_) => {
135 current = draining;
136 break;
137 }
138 Err(observed) => current = observed,
139 }
140 }
141 if count(current) == 0 {
142 self.shared.drained.notify_one();
143 }
144 }
145
146 pub(crate) async fn wait_until_drained(&self) {
147 loop {
148 if count(self.shared.state.load(Ordering::Acquire)) == 0 {
149 return;
150 }
151 self.shared.drained.notified().await;
152 }
153 }
154
155 pub(crate) fn mark_stopped(&self) {
156 let result = self.shared.state.compare_exchange(
157 encode(ApplicationPhase::Draining, 0),
158 encode(ApplicationPhase::Stopped, 0),
159 Ordering::AcqRel,
160 Ordering::Acquire,
161 );
162 debug_assert!(result.is_ok());
163 }
164}
165
166const fn encode(phase: ApplicationPhase, count: usize) -> usize {
167 ((phase as usize) << PHASE_SHIFT) | count
168}
169
170const fn phase(state: usize) -> ApplicationPhase {
171 match state >> PHASE_SHIFT {
172 0 => ApplicationPhase::Starting,
173 1 => ApplicationPhase::Ready,
174 2 => ApplicationPhase::Draining,
175 3 => ApplicationPhase::Stopped,
176 _ => unreachable!(),
177 }
178}
179
180const fn count(state: usize) -> usize {
181 state & COUNT_MASK
182}
183
184#[derive(Debug)]
186pub struct RequestGuard {
187 shared: Arc<Shared>,
188}
189
190impl Drop for RequestGuard {
191 fn drop(&mut self) {
192 let previous = self.shared.state.fetch_sub(1, Ordering::AcqRel);
193 debug_assert!(count(previous) > 0);
194 if count(previous) == 1 && phase(previous) == ApplicationPhase::Draining {
195 self.shared.drained.notify_one();
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use std::sync::{Arc, Barrier};
203
204 use super::*;
205
206 fn test_runtime() -> tokio::runtime::Runtime {
207 tokio::runtime::Builder::new_current_thread()
208 .build()
209 .expect("test runtime must build")
210 }
211
212 #[test]
213 fn only_ready_applications_accept_requests() {
214 let requests = RequestLifecycle::new();
215 assert_eq!(
216 requests.try_accept().unwrap_err().code(),
217 "runtime.not_ready"
218 );
219
220 requests.mark_ready();
221 let request = requests.try_accept().expect("ready request is admitted");
222 requests.begin_draining();
223
224 assert_eq!(requests.phase(), ApplicationPhase::Draining);
225 assert_eq!(
226 requests.try_accept().unwrap_err().code(),
227 "runtime.shutting_down"
228 );
229
230 drop(request);
231 test_runtime().block_on(requests.wait_until_drained());
232 requests.mark_stopped();
233 assert_eq!(requests.phase(), ApplicationPhase::Stopped);
234 }
235
236 #[test]
237 fn draining_waits_for_every_admitted_request() {
238 test_runtime().block_on(async {
239 let requests = RequestLifecycle::new();
240 requests.mark_ready();
241 let first = requests.try_accept().unwrap();
242 let second = requests.try_accept().unwrap();
243 requests.begin_draining();
244
245 let requests_for_waiter = requests.clone();
246 let waiter = tokio::spawn(async move {
247 requests_for_waiter.wait_until_drained().await;
248 });
249 tokio::task::yield_now().await;
250 assert!(!waiter.is_finished());
251
252 drop(first);
253 tokio::task::yield_now().await;
254 assert!(!waiter.is_finished());
255
256 drop(second);
257 waiter.await.unwrap();
258 });
259 }
260
261 #[test]
262 fn admission_racing_with_drain_never_leaks_a_request() {
263 const WORKERS: usize = 8;
264 let requests = RequestLifecycle::new();
265 requests.mark_ready();
266 let barrier = Arc::new(Barrier::new(WORKERS + 1));
267 let workers: Vec<_> = (0..WORKERS)
268 .map(|_| {
269 let requests = requests.clone();
270 let barrier = Arc::clone(&barrier);
271 std::thread::spawn(move || {
272 let admitted_before_drain = requests.try_accept().unwrap();
273 barrier.wait();
274 loop {
275 match requests.try_accept() {
276 Ok(request) => drop(request),
277 Err(error) => {
278 assert_eq!(error.code(), "runtime.shutting_down");
279 drop(admitted_before_drain);
280 break;
281 }
282 }
283 }
284 })
285 })
286 .collect();
287
288 barrier.wait();
289 requests.begin_draining();
290 for worker in workers {
291 worker.join().unwrap();
292 }
293 test_runtime().block_on(requests.wait_until_drained());
294 assert_eq!(count(requests.shared.state.load(Ordering::Acquire)), 0);
295 }
296
297 #[test]
298 fn guard_completion_is_safe_from_another_thread() {
299 let requests = RequestLifecycle::new();
300 requests.mark_ready();
301 let guard = requests.try_accept().unwrap();
302 requests.begin_draining();
303
304 std::thread::spawn(move || drop(guard)).join().unwrap();
305 test_runtime().block_on(requests.wait_until_drained());
306
307 assert_eq!(Arc::strong_count(&requests.shared), 1);
310 }
311}