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