liminal_server/auth_lys/grant_stream_owner/
state.rs1use crate::auth_membership::{AuthorityState, MembershipAuthority};
4use crate::auth_revocation::{
5 DecisionReply, GrantDecisionSource, GrantId, GrantLogId, Revision, RevocationRefusal,
6};
7use lys_pass::membership::GrantLog;
8use serde::Serialize;
9use std::collections::BTreeSet;
10use std::sync::{Arc, Mutex};
11use tokio::sync::watch;
12
13#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
15pub struct StreamDiagnostics {
16 pub readiness: &'static str,
18 pub cursor: Option<u64>,
20 pub placed: Option<u64>,
22 pub last_error: Option<&'static str>,
24 pub missing: Option<u64>,
26 pub generation: u64,
28 pub requests: u64,
30 pub reopens: u64,
32 pub stopped: bool,
34}
35
36#[derive(Clone, Debug, Serialize)]
38#[serde(untagged)]
39pub enum StreamHealth {
40 Known(StreamDiagnostics),
42 Unavailable {
44 readiness: &'static str,
46 cursor: Option<u64>,
48 placed: Option<u64>,
50 last_error: &'static str,
52 },
53}
54
55impl StreamHealth {
56 #[must_use]
58 pub fn is_ready(&self) -> bool {
59 matches!(self, Self::Known(state) if state.readiness == "ready")
60 }
61}
62
63pub(super) struct Inner {
64 pub diagnostics: StreamDiagnostics,
65 pub revoked: BTreeSet<GrantId>,
66}
67
68pub struct StreamState {
70 pub(super) reader_claim: std::sync::atomic::AtomicBool,
71 pub(super) log: GrantLog,
72 pub(super) inner: Mutex<Inner>,
73 pub(super) changed: watch::Sender<Result<StreamDiagnostics, super::StreamError>>,
74 pub(super) stop: watch::Sender<bool>,
75}
76
77impl std::fmt::Debug for StreamState {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.debug_struct("StreamState")
80 .field("diagnostics", &self.snapshot())
81 .finish_non_exhaustive()
82 }
83}
84
85impl StreamState {
86 #[must_use]
88 pub fn for_log(log: GrantLog) -> Arc<Self> {
89 let diagnostics = StreamDiagnostics {
90 readiness: "not_ready",
91 cursor: None,
92 placed: None,
93 last_error: None,
94 missing: None,
95 generation: 0,
96 requests: 0,
97 reopens: 0,
98 stopped: false,
99 };
100 Arc::new(Self {
101 reader_claim: std::sync::atomic::AtomicBool::new(false),
102 log,
103 inner: Mutex::new(Inner {
104 diagnostics: diagnostics.clone(),
105 revoked: BTreeSet::new(),
106 }),
107 changed: watch::channel(Ok(diagnostics)).0,
108 stop: watch::channel(false).0,
109 })
110 }
111
112 pub fn snapshot(&self) -> Result<StreamDiagnostics, super::StreamError> {
117 self.inner
118 .lock()
119 .map(|inner| inner.diagnostics.clone())
120 .map_err(|_| super::StreamError::named("grant_stream_state_poisoned"))
121 }
122
123 #[must_use]
125 pub fn health(&self) -> StreamHealth {
126 match self.snapshot() {
127 Ok(status) => StreamHealth::Known(status),
128 Err(error) => StreamHealth::Unavailable {
129 readiness: "terminal",
130 cursor: None,
131 placed: None,
132 last_error: error.name(),
133 },
134 }
135 }
136
137 #[must_use]
139 pub fn subscribe(&self) -> watch::Receiver<Result<StreamDiagnostics, super::StreamError>> {
140 self.changed.subscribe()
141 }
142
143 #[must_use]
145 pub fn cancellation(&self) -> watch::Receiver<bool> {
146 self.stop.subscribe()
147 }
148
149 pub(super) fn update(
150 &self,
151 edit: impl FnOnce(&mut Inner) -> Result<(), super::StreamError>,
152 ) -> Result<(), super::StreamError> {
153 let result = (|| {
154 let mut inner = self
155 .inner
156 .lock()
157 .map_err(|_| super::StreamError::named("grant_stream_state_poisoned"))?;
158 edit(&mut inner)?;
159 Ok(inner.diagnostics.clone())
160 })();
161 drop(self.changed.send_replace(result.clone()));
162 result.map(|_| ())
163 }
164
165 pub(super) fn unavailable(&self, name: &'static str) -> Result<(), super::StreamError> {
166 self.update(|inner| {
167 inner.diagnostics.readiness = "unavailable";
168 inner.diagnostics.last_error = Some(name);
169 Ok(())
170 })
171 }
172}
173
174impl MembershipAuthority for StreamState {
175 fn state(&self) -> AuthorityState {
176 let status = match self.snapshot() {
177 Ok(status) => status,
178 Err(error) => {
179 return AuthorityState::Unavailable {
180 cause: error.name().into(),
181 };
182 }
183 };
184 match status.readiness {
185 "ready" => {
186 status
187 .cursor
188 .map_or(AuthorityState::NotReady, |revision| AuthorityState::Ready {
189 log: self.log.clone(),
190 revision,
191 generation: status.generation,
192 })
193 }
194 "not_ready" => AuthorityState::NotReady,
195 "gap" => match (status.cursor, status.missing) {
196 (Some(after), Some(missing)) => AuthorityState::Gap { after, missing },
197 _ => AuthorityState::Unavailable {
198 cause: "revocation_stream_gap".into(),
199 },
200 },
201 "reset" => AuthorityState::Reset,
202 _ => AuthorityState::Unavailable {
203 cause: status
204 .last_error
205 .unwrap_or("revocation_stream_unavailable")
206 .into(),
207 },
208 }
209 }
210}
211
212impl GrantDecisionSource for StreamState {
213 fn decide(
214 &self,
215 log: &GrantLogId,
216 grants: &[GrantId],
217 at_least: Revision,
218 ) -> Result<DecisionReply, RevocationRefusal> {
219 let inner = self
220 .inner
221 .lock()
222 .map_err(|_| RevocationRefusal::StreamUnavailable)?;
223 if log.identity() != self.log.identity || log.epoch() != self.log.epoch {
224 return Err(RevocationRefusal::LogReset);
225 }
226 if inner.diagnostics.readiness != "ready" {
227 return Err(RevocationRefusal::StreamUnavailable);
228 }
229 let revision = inner
230 .diagnostics
231 .cursor
232 .ok_or(RevocationRefusal::RevisionUnavailable)?;
233 if revision < at_least.get() {
234 return Err(RevocationRefusal::RevisionUnavailable);
235 }
236 let answer = DecisionReply {
239 log: log.clone(),
240 revision: Revision::new(revision),
241 decisions: grants
242 .iter()
243 .map(|grant| (grant.clone(), inner.revoked.contains(grant)))
244 .collect(),
245 };
246 drop(inner);
247 Ok(answer)
248 }
249}