notedthat_api_http/
readiness.rs1use serde::Serialize;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum Unready {
27 Timeout,
29 Unreachable,
31 NotFound,
35}
36
37impl Unready {
38 #[must_use]
41 pub fn is_outage(self) -> bool {
42 match self {
43 Self::Timeout | Self::Unreachable => true,
44 Self::NotFound => false,
45 }
46 }
47
48 #[must_use]
50 pub fn as_str(self) -> &'static str {
51 match self {
52 Self::Timeout => "timeout",
53 Self::Unreachable => "unreachable",
54 Self::NotFound => "not_found",
55 }
56 }
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct Check {
62 pub backend: &'static str,
65 pub outcome: Result<(), Unready>,
67}
68
69impl Check {
70 #[must_use]
72 pub fn ok(backend: &'static str) -> Self {
73 Self {
74 backend,
75 outcome: Ok(()),
76 }
77 }
78
79 #[must_use]
81 pub fn unready(backend: &'static str, reason: Unready) -> Self {
82 Self {
83 backend,
84 outcome: Err(reason),
85 }
86 }
87
88 #[must_use]
91 pub fn is_ready(&self) -> bool {
92 match self.outcome {
93 Ok(()) => true,
94 Err(reason) => !reason.is_outage(),
95 }
96 }
97
98 #[must_use]
101 pub fn is_degraded(&self) -> bool {
102 matches!(self.outcome, Err(reason) if !reason.is_outage())
103 }
104
105 #[must_use]
109 pub fn to_json(&self) -> serde_json::Value {
110 match self.outcome {
111 Ok(()) => serde_json::json!({ "backend": self.backend, "status": "ok" }),
112 Err(reason) => serde_json::json!({
113 "backend": self.backend,
114 "status": if reason.is_outage() { "unavailable" } else { "degraded" },
115 "reason": reason.as_str(),
116 }),
117 }
118 }
119}
120
121#[derive(Clone, Debug, PartialEq, Eq)]
123pub struct ReadinessSnapshot {
124 pub storage: Check,
126 pub search: Check,
128}
129
130impl ReadinessSnapshot {
131 #[must_use]
134 pub fn ok(storage_backend: &'static str, search_backend: &'static str) -> Self {
135 Self {
136 storage: Check::ok(storage_backend),
137 search: Check::ok(search_backend),
138 }
139 }
140
141 #[must_use]
143 pub fn is_ready(&self) -> bool {
144 self.storage.is_ready() && self.search.is_ready()
145 }
146
147 #[must_use]
149 pub fn is_degraded(&self) -> bool {
150 self.storage.is_degraded() || self.search.is_degraded()
151 }
152}
153
154pub type ReadinessReceiver = tokio::sync::watch::Receiver<ReadinessSnapshot>;
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn an_ok_check_renders_without_a_reason() {
166 assert_eq!(
167 Check::ok("s3").to_json(),
168 serde_json::json!({ "backend": "s3", "status": "ok" })
169 );
170 }
171
172 #[test]
173 fn an_unready_check_renders_its_reason_and_nothing_else() {
174 let json = Check::unready("qdrant", Unready::Timeout).to_json();
175 assert_eq!(
176 json,
177 serde_json::json!({ "backend": "qdrant", "status": "unavailable", "reason": "timeout" })
178 );
179 }
180
181 #[test]
182 fn reasons_are_the_documented_vocabulary() {
183 for (reason, expected) in [
184 (Unready::Timeout, "timeout"),
185 (Unready::Unreachable, "unreachable"),
186 (Unready::NotFound, "not_found"),
187 ] {
188 assert_eq!(reason.as_str(), expected);
189 assert_eq!(serde_json::to_value(reason).unwrap(), expected);
190 }
191 }
192
193 #[test]
194 fn a_not_found_check_is_degraded_and_still_ready() {
195 let check = Check::unready("fs", Unready::NotFound);
196 assert!(check.is_ready());
197 assert!(check.is_degraded());
198 assert!(!Check::ok("fs").is_degraded());
199 assert!(!Check::unready("fs", Unready::Timeout).is_degraded());
200 assert_eq!(
201 check.to_json(),
202 serde_json::json!({ "backend": "fs", "status": "degraded", "reason": "not_found" })
203 );
204 }
205
206 #[test]
207 fn ready_unless_a_backend_is_out() {
208 assert!(ReadinessSnapshot::ok("fs", "qdrant").is_ready());
209 let witness_gone = ReadinessSnapshot {
210 storage: Check::unready("fs", Unready::NotFound),
211 search: Check::ok("qdrant"),
212 };
213 assert!(witness_gone.is_ready(), "the backend answered; it is up");
214 assert!(
215 witness_gone.is_degraded(),
216 "and the body says so at the top"
217 );
218 assert!(!ReadinessSnapshot::ok("fs", "qdrant").is_degraded());
219 for reason in [Unready::Timeout, Unready::Unreachable] {
220 let storage_down = ReadinessSnapshot {
221 storage: Check::unready("s3", reason),
222 search: Check::ok("qdrant"),
223 };
224 assert!(!storage_down.is_ready());
225 let search_down = ReadinessSnapshot {
226 storage: Check::ok("fs"),
227 search: Check::unready("qdrant", reason),
228 };
229 assert!(!search_down.is_ready());
230 }
231 }
232}