Skip to main content

appcore_api/http/
reload_generation.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: reload_generation.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/09/02 12:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/09/02 12:00:00 by dnettoRaw
8//      ###########      S: 1.0.2-rc
9// =============================================================================
10
11//! Bounded ownership and payload-free observations for HTTP routing generations.
12
13use arc_swap::ArcSwap;
14use axum::Router;
15use parking_lot::Mutex;
16use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
17use std::sync::{Arc, Weak};
18
19pub(super) const MAX_RETAINED_GENERATIONS: usize = 2;
20
21/// Payload-free state for one HTTP routing generation.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[non_exhaustive]
24pub struct HttpRoutingGenerationSnapshot {
25    /// Monotonic identifier assigned by the composition root.
26    pub generation: u64,
27    /// Whether this generation can admit a new request.
28    pub accepting: bool,
29    /// Requests that currently retain this generation.
30    pub inflight: usize,
31}
32
33/// Bounded ownership snapshot for active and retiring routing generations.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[non_exhaustive]
36pub struct HttpRoutingGenerationsSnapshot {
37    /// Generation selected for new requests.
38    pub active: HttpRoutingGenerationSnapshot,
39    /// Previous or failed generation retained only until its requests finish.
40    pub retiring: Option<HttpRoutingGenerationSnapshot>,
41    /// Number of generations retained by the reload owner.
42    pub retained_generations: usize,
43    /// Hard owner limit: one active generation and one retiring generation.
44    pub max_retained_generations: usize,
45}
46
47pub(super) struct RoutingGeneration {
48    id: u64,
49    router: Router,
50    accepting: AtomicBool,
51    inflight: AtomicUsize,
52    retiring_slot: Weak<RetiringGenerationSlot>,
53}
54
55impl RoutingGeneration {
56    fn new(id: u64, router: Router, retiring_slot: Weak<RetiringGenerationSlot>) -> Self {
57        Self {
58            id,
59            router,
60            accepting: AtomicBool::new(true),
61            inflight: AtomicUsize::new(0),
62            retiring_slot,
63        }
64    }
65
66    pub(super) fn id(&self) -> u64 {
67        self.id
68    }
69
70    pub(super) fn router(&self) -> &Router {
71        &self.router
72    }
73
74    pub(super) fn inflight(&self) -> usize {
75        self.inflight.load(Ordering::Acquire)
76    }
77
78    pub(super) fn start_accepting(&self) {
79        self.accepting.store(true, Ordering::Release);
80    }
81
82    pub(super) fn stop_accepting(&self) {
83        self.accepting.store(false, Ordering::Release);
84    }
85
86    pub(super) fn try_admit(self: &Arc<Self>) -> Option<RoutingPermit> {
87        if !self.accepting.load(Ordering::Acquire) {
88            return None;
89        }
90        self.inflight
91            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
92                current.checked_add(1)
93            })
94            .ok()?;
95        if self.accepting.load(Ordering::Acquire) {
96            return Some(RoutingPermit {
97                generation: Arc::clone(self),
98            });
99        }
100        self.release_request();
101        None
102    }
103
104    fn release_request(&self) {
105        let Ok(previous) =
106            self.inflight
107                .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
108                    current.checked_sub(1)
109                })
110        else {
111            debug_assert!(false, "routing generation in-flight underflow");
112            return;
113        };
114        if previous == 1 && !self.accepting.load(Ordering::Acquire) {
115            if let Some(slot) = self.retiring_slot.upgrade() {
116                slot.clear_if_drained(self.id);
117            }
118        }
119    }
120
121    fn snapshot(&self) -> HttpRoutingGenerationSnapshot {
122        HttpRoutingGenerationSnapshot {
123            generation: self.id,
124            accepting: self.accepting.load(Ordering::Acquire),
125            inflight: self.inflight(),
126        }
127    }
128}
129
130pub(super) struct RoutingPermit {
131    generation: Arc<RoutingGeneration>,
132}
133
134impl Drop for RoutingPermit {
135    fn drop(&mut self) {
136        self.generation.release_request();
137    }
138}
139
140struct RetiringGenerationSlot {
141    generation: Mutex<Option<Arc<RoutingGeneration>>>,
142}
143
144impl RetiringGenerationSlot {
145    fn new() -> Self {
146        Self {
147            generation: Mutex::new(None),
148        }
149    }
150
151    fn retire(&self, generation: Arc<RoutingGeneration>) -> bool {
152        let mut slot = self.generation.lock();
153        if slot.is_some() {
154            return false;
155        }
156        *slot = Some(generation);
157        true
158    }
159
160    fn clear(&self, generation: u64) {
161        let mut slot = self.generation.lock();
162        if slot
163            .as_ref()
164            .is_some_and(|current| current.id == generation)
165        {
166            *slot = None;
167        }
168    }
169
170    fn clear_if_drained(&self, generation: u64) {
171        let mut slot = self.generation.lock();
172        if slot
173            .as_ref()
174            .is_some_and(|current| current.id == generation && current.inflight() == 0)
175        {
176            *slot = None;
177        }
178    }
179
180    fn snapshot(&self) -> Option<HttpRoutingGenerationSnapshot> {
181        let slot = self.generation.lock();
182        slot.as_ref().map(|generation| generation.snapshot())
183    }
184
185    fn clear_drained(&self) {
186        let mut slot = self.generation.lock();
187        if slot
188            .as_ref()
189            .is_some_and(|generation| generation.inflight() == 0)
190        {
191            *slot = None;
192        }
193    }
194}
195
196pub(super) struct RoutingTable {
197    active: ArcSwap<RoutingGeneration>,
198    retiring: Arc<RetiringGenerationSlot>,
199}
200
201impl RoutingTable {
202    pub(super) fn new(id: u64, router: Router) -> Self {
203        let retiring = Arc::new(RetiringGenerationSlot::new());
204        let active = Arc::new(RoutingGeneration::new(
205            id,
206            router,
207            Arc::downgrade(&retiring),
208        ));
209        Self {
210            active: ArcSwap::from(active),
211            retiring,
212        }
213    }
214
215    pub(super) fn active(&self) -> Arc<RoutingGeneration> {
216        self.active.load_full()
217    }
218
219    pub(super) fn generation(&self, id: u64, router: Router) -> Arc<RoutingGeneration> {
220        Arc::new(RoutingGeneration::new(
221            id,
222            router,
223            Arc::downgrade(&self.retiring),
224        ))
225    }
226
227    pub(super) fn activate(&self, generation: Arc<RoutingGeneration>) {
228        self.active.store(generation);
229    }
230
231    pub(super) fn retire(&self, generation: Arc<RoutingGeneration>) -> bool {
232        self.retiring.retire(generation)
233    }
234
235    pub(super) fn release_retiring(&self, generation: u64) {
236        self.retiring.clear(generation);
237    }
238
239    pub(super) fn release_drained_retiring(&self) {
240        self.retiring.clear_drained();
241    }
242
243    pub(super) fn generations_snapshot(&self) -> HttpRoutingGenerationsSnapshot {
244        let active = self.active();
245        let active_snapshot = active.snapshot();
246        let retiring = self
247            .retiring
248            .snapshot()
249            .filter(|snapshot| snapshot.generation != active_snapshot.generation);
250        HttpRoutingGenerationsSnapshot {
251            active: active_snapshot,
252            retiring,
253            retained_generations: 1 + usize::from(retiring.is_some()),
254            max_retained_generations: MAX_RETAINED_GENERATIONS,
255        }
256    }
257}