Skip to main content

hypercall/readiness/
mod.rs

1//! Readiness framework for service startup gating.
2//!
3//! This module provides a generic readiness registry that tracks multiple
4//! readiness checks (e.g., snapshot sync, database connection, etc.) and
5//! provides a unified interface for:
6//! - GET /ready endpoint (200 OK vs 503 Service Unavailable)
7//! - readiness_middleware (block requests until service is ready)
8//!
9//! # Example
10//!
11//! ```ignore
12//! use std::sync::Arc;
13//! use hypercall::readiness::{ReadinessRegistry, SyncStatusReadiness};
14//! use hypercall::snapshot::SyncStatus;
15//!
16//! let sync_status = Arc::new(SyncStatus::new());
17//! let check = Arc::new(SyncStatusReadiness::new("portfolio", sync_status.clone()));
18//! let registry = ReadinessRegistry::new(vec![check]);
19//!
20//! // Initially not ready
21//! assert!(!registry.all_ready());
22//!
23//! // After catchup completes
24//! sync_status.set_ready();
25//! assert!(registry.all_ready());
26//! ```
27
28use std::sync::atomic::{AtomicBool, Ordering};
29use std::sync::Arc;
30
31use serde::Serialize;
32
33use crate::snapshot::SyncStatus;
34use crate::vol_oracle::SharedVolOracle;
35
36/// A single component's readiness report.
37#[derive(Debug, Clone, Serialize)]
38pub struct ReadinessReport {
39    /// Name of the component being checked (e.g., "portfolio", "orderbook")
40    pub name: String,
41    /// Whether this component is ready to serve requests
42    pub ready: bool,
43    /// Optional detail about the component's state (e.g., "CatchingUp", "Ready")
44    pub detail: Option<String>,
45}
46
47/// Trait for readiness checks.
48///
49/// Implementors should be cheap to query (no async, no blocking).
50pub trait Readiness: Send + Sync {
51    /// Generate a readiness report for this component.
52    fn report(&self) -> ReadinessReport;
53}
54
55/// Adapter that wraps a SyncStatus to implement Readiness.
56///
57/// This is the primary adapter for snapshot-based services like PortfolioCache.
58pub struct SyncStatusReadiness {
59    name: &'static str,
60    sync: Arc<SyncStatus>,
61}
62
63impl SyncStatusReadiness {
64    /// Create a new SyncStatusReadiness adapter.
65    ///
66    /// # Arguments
67    /// * `name` - Human-readable name for this component (e.g., "portfolio")
68    /// * `sync` - The SyncStatus to track
69    pub fn new(name: &'static str, sync: Arc<SyncStatus>) -> Self {
70        Self { name, sync }
71    }
72}
73
74impl Readiness for SyncStatusReadiness {
75    fn report(&self) -> ReadinessReport {
76        let state = self.sync.state();
77        ReadinessReport {
78            name: self.name.to_string(),
79            ready: self.sync.is_ready(),
80            detail: Some(state.to_string()),
81        }
82    }
83}
84
85/// Adapter that exposes routed risk-vol readiness for startup gating.
86pub struct VolOracleReadiness {
87    name: &'static str,
88    oracle: SharedVolOracle,
89    latched_ready: AtomicBool,
90}
91
92impl VolOracleReadiness {
93    pub fn new(name: &'static str, oracle: SharedVolOracle) -> Self {
94        Self {
95            name,
96            oracle,
97            latched_ready: AtomicBool::new(false),
98        }
99    }
100}
101
102impl Readiness for VolOracleReadiness {
103    fn report(&self) -> ReadinessReport {
104        let statuses = self.oracle.statuses();
105        let current_ready = !statuses.is_empty() && statuses.iter().all(|status| status.ready);
106        if current_ready {
107            self.latched_ready.store(true, Ordering::Relaxed);
108        }
109        let ready = self.latched_ready.load(Ordering::Relaxed);
110        let detail = if ready && current_ready {
111            "all configured underlyings ready".to_string()
112        } else if ready {
113            "startup readiness latched; runtime oracle health should be checked via monitoring"
114                .to_string()
115        } else {
116            let degraded = statuses
117                .iter()
118                .filter(|status| !status.ready)
119                .map(|status| {
120                    format!(
121                        "{}:{}:{}",
122                        status.provider.as_str(),
123                        status.underlying,
124                        status.last_error.as_deref().unwrap_or("surface not ready")
125                    )
126                })
127                .collect::<Vec<_>>();
128            if degraded.is_empty() {
129                "no configured vol surfaces".to_string()
130            } else {
131                degraded.join("; ")
132            }
133        };
134
135        ReadinessReport {
136            name: self.name.to_string(),
137            ready,
138            detail: Some(detail),
139        }
140    }
141}
142
143/// Registry of readiness checks.
144///
145/// Aggregates multiple readiness checks and provides methods to query
146/// overall readiness and individual component reports.
147pub struct ReadinessRegistry {
148    checks: Vec<Arc<dyn Readiness>>,
149}
150
151impl ReadinessRegistry {
152    /// Create a new registry with the given checks.
153    pub fn new(checks: Vec<Arc<dyn Readiness>>) -> Self {
154        Self { checks }
155    }
156
157    /// Get readiness reports from all components.
158    pub fn reports(&self) -> Vec<ReadinessReport> {
159        self.checks.iter().map(|c| c.report()).collect()
160    }
161
162    /// Check if all components are ready.
163    pub fn all_ready(&self) -> bool {
164        self.checks.iter().all(|c| c.report().ready)
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_sync_status_readiness_initializing() {
174        let sync = Arc::new(SyncStatus::new());
175        let check = SyncStatusReadiness::new("portfolio", sync.clone());
176
177        let report = check.report();
178        assert_eq!(report.name, "portfolio");
179        assert!(!report.ready);
180        assert_eq!(report.detail.as_deref(), Some("Initializing"));
181    }
182
183    #[test]
184    fn test_sync_status_readiness_catching_up() {
185        let sync = Arc::new(SyncStatus::new());
186        sync.set_catching_up();
187        let check = SyncStatusReadiness::new("portfolio", sync.clone());
188
189        let report = check.report();
190        assert_eq!(report.name, "portfolio");
191        assert!(!report.ready);
192        assert_eq!(report.detail.as_deref(), Some("CatchingUp"));
193    }
194
195    #[test]
196    fn test_sync_status_readiness_ready() {
197        let sync = Arc::new(SyncStatus::new());
198        sync.set_ready();
199        let check = SyncStatusReadiness::new("portfolio", sync.clone());
200
201        let report = check.report();
202        assert_eq!(report.name, "portfolio");
203        assert!(report.ready);
204        assert_eq!(report.detail.as_deref(), Some("Ready"));
205    }
206
207    #[test]
208    fn test_registry_all_ready_when_empty() {
209        let registry = ReadinessRegistry::new(vec![]);
210        // Empty registry is considered ready (no checks to fail)
211        assert!(registry.all_ready());
212        assert!(registry.reports().is_empty());
213    }
214
215    #[test]
216    fn test_registry_single_check_not_ready() {
217        let sync = Arc::new(SyncStatus::new());
218        let check: Arc<dyn Readiness> =
219            Arc::new(SyncStatusReadiness::new("portfolio", sync.clone()));
220        let registry = ReadinessRegistry::new(vec![check]);
221
222        assert!(!registry.all_ready());
223        let reports = registry.reports();
224        assert_eq!(reports.len(), 1);
225        assert_eq!(reports[0].name, "portfolio");
226        assert!(!reports[0].ready);
227    }
228
229    #[test]
230    fn test_registry_single_check_ready() {
231        let sync = Arc::new(SyncStatus::new());
232        sync.set_ready();
233        let check: Arc<dyn Readiness> =
234            Arc::new(SyncStatusReadiness::new("portfolio", sync.clone()));
235        let registry = ReadinessRegistry::new(vec![check]);
236
237        assert!(registry.all_ready());
238        let reports = registry.reports();
239        assert_eq!(reports.len(), 1);
240        assert!(reports[0].ready);
241    }
242
243    #[test]
244    fn test_registry_multiple_checks_partial_ready() {
245        let sync1 = Arc::new(SyncStatus::new());
246        sync1.set_ready();
247        let sync2 = Arc::new(SyncStatus::new()); // Still Initializing
248
249        let check1: Arc<dyn Readiness> =
250            Arc::new(SyncStatusReadiness::new("portfolio", sync1.clone()));
251        let check2: Arc<dyn Readiness> =
252            Arc::new(SyncStatusReadiness::new("orderbook", sync2.clone()));
253        let registry = ReadinessRegistry::new(vec![check1, check2]);
254
255        // Not all ready because orderbook is still Initializing
256        assert!(!registry.all_ready());
257
258        let reports = registry.reports();
259        assert_eq!(reports.len(), 2);
260
261        let portfolio = reports.iter().find(|r| r.name == "portfolio").unwrap();
262        assert!(portfolio.ready);
263
264        let orderbook = reports.iter().find(|r| r.name == "orderbook").unwrap();
265        assert!(!orderbook.ready);
266    }
267
268    #[test]
269    fn test_registry_multiple_checks_all_ready() {
270        let sync1 = Arc::new(SyncStatus::new());
271        sync1.set_ready();
272        let sync2 = Arc::new(SyncStatus::new());
273        sync2.set_ready();
274
275        let check1: Arc<dyn Readiness> =
276            Arc::new(SyncStatusReadiness::new("portfolio", sync1.clone()));
277        let check2: Arc<dyn Readiness> =
278            Arc::new(SyncStatusReadiness::new("orderbook", sync2.clone()));
279        let registry = ReadinessRegistry::new(vec![check1, check2]);
280
281        assert!(registry.all_ready());
282    }
283
284    #[test]
285    fn test_registry_state_transitions() {
286        let sync = Arc::new(SyncStatus::new());
287        let check: Arc<dyn Readiness> =
288            Arc::new(SyncStatusReadiness::new("portfolio", sync.clone()));
289        let registry = ReadinessRegistry::new(vec![check]);
290
291        // Initially not ready
292        assert!(!registry.all_ready());
293        assert_eq!(
294            registry.reports()[0].detail.as_deref(),
295            Some("Initializing")
296        );
297
298        // After set_catching_up
299        sync.set_catching_up();
300        assert!(!registry.all_ready());
301        assert_eq!(registry.reports()[0].detail.as_deref(), Some("CatchingUp"));
302
303        // After set_ready
304        sync.set_ready();
305        assert!(registry.all_ready());
306        assert_eq!(registry.reports()[0].detail.as_deref(), Some("Ready"));
307    }
308}