1#[path = "grant_stream_owner/binding.rs"]
4mod binding;
5#[path = "grant_stream_owner/config.rs"]
6mod config;
7#[path = "grant_stream_owner/configured.rs"]
8mod configured;
9#[path = "grant_stream_owner/engine.rs"]
10mod engine;
11#[path = "grant_stream_owner/http.rs"]
12mod http;
13#[path = "grant_stream_owner/state.rs"]
14mod state;
15
16pub use config::{GrantStreamConfig, GrantSubjectConfig};
17pub use state::{StreamDiagnostics, StreamHealth, StreamState};
18
19use crate::auth_lys::live::drafts::{DraftDecisionHook, PullReport};
20use crate::config::types::LysConfig;
21use lys_identity::grants::change_stream::{ChangesPage, ChangesRequest, LogName};
22use std::sync::Arc;
23use std::thread::JoinHandle;
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
27#[error("{name}")]
28pub struct StreamError {
29 name: &'static str,
30 transport_ended: bool,
31}
32
33impl StreamError {
34 pub(super) const fn named(name: &'static str) -> Self {
35 Self {
36 name,
37 transport_ended: false,
38 }
39 }
40 const fn transport() -> Self {
41 Self {
42 name: "revocation_stream_unavailable",
43 transport_ended: true,
44 }
45 }
46 #[must_use]
48 pub const fn name(self) -> &'static str {
49 self.name
50 }
51}
52
53pub struct GrantStreamOwner {
55 state: Arc<StreamState>,
56 worker: Option<JoinHandle<Result<(), StreamError>>>,
57 draft_hook: Option<Arc<dyn DraftDecisionHook>>,
58}
59
60impl std::fmt::Debug for GrantStreamOwner {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.debug_struct("GrantStreamOwner")
63 .field("state", &self.state)
64 .field("worker", &self.worker.is_some())
65 .finish_non_exhaustive()
66 }
67}
68
69impl GrantStreamOwner {
70 pub fn start_for_server(
75 config: &crate::config::types::ServerConfig,
76 supervisor: &crate::server::ConnectionSupervisor,
77 health: &crate::health::HealthServerHandle,
78 runtime: Option<&tokio::runtime::Runtime>,
79 draft_hook: Option<Arc<dyn DraftDecisionHook>>,
80 ) -> Result<Option<Self>, crate::ServerError> {
81 let Some(lys) = config.auth.as_ref().and_then(|auth| auth.lys.as_ref()) else {
82 return Ok(None);
83 };
84 if lys.membership.is_none() && lys.grant_stream.is_none() {
85 crate::auth_lys::root_presence_start::start(lys, health, runtime)?;
87 return Ok(None);
88 }
89 let runtime = runtime.ok_or_else(|| config_error("grant_stream_runtime_missing"))?;
90 Self::start_configured(
91 lys,
92 supervisor,
93 health,
94 runtime.handle().clone(),
95 draft_hook,
96 )
97 }
98
99 pub fn start_configured(
108 lys: &LysConfig,
109 supervisor: &crate::server::ConnectionSupervisor,
110 health: &crate::health::HealthServerHandle,
111 handle: tokio::runtime::Handle,
112 draft_hook: Option<Arc<dyn DraftDecisionHook>>,
113 ) -> Result<Option<Self>, crate::ServerError> {
114 let Some(config) = lys.grant_stream.as_ref() else {
115 if lys.membership.is_some() {
116 return Err(config_error(
117 "auth.lys.membership requires auth.lys.grant_stream",
118 ));
119 }
120 return Ok(None);
121 };
122 let configured::Installation {
123 state,
124 engine,
125 http,
126 root,
127 } = configured::prepare(lys, config, supervisor, health, handle.clone())?;
128 let worker_state = Arc::clone(&state);
129 let worker = std::thread::Builder::new()
130 .name("liminal-grants".into())
131 .spawn(move || {
132 let result = handle.block_on(read(engine, http, root));
133 if let Err(error) = result {
134 worker_state.update(|inner| {
135 if !matches!(inner.diagnostics.readiness, "gap" | "reset") {
136 inner.diagnostics.readiness = "terminal";
137 }
138 inner.diagnostics.last_error = Some(error.name());
139 inner.diagnostics.stopped = true;
140 Ok(())
141 })?;
142 } else {
143 worker_state.update(|inner| {
144 inner.diagnostics.readiness = "unavailable";
145 inner.diagnostics.last_error = Some("grant_stream_stopped");
146 inner.diagnostics.stopped = true;
147 Ok(())
148 })?;
149 }
150 result
151 })
152 .map_err(|_| config_error("grant_stream_worker_unavailable"))?;
153 Ok(Some(Self {
154 state,
155 worker: Some(worker),
156 draft_hook,
157 }))
158 }
159
160 #[must_use]
162 pub fn state(&self) -> Arc<StreamState> {
163 Arc::clone(&self.state)
164 }
165
166 pub async fn draft_decided(&self) -> Result<PullReport, StreamError> {
172 let hook = self
173 .draft_hook
174 .as_ref()
175 .ok_or(StreamError::named("grant_stream_draft_hook_missing"))?;
176 let result = hook
177 .draft_decided()
178 .await
179 .map_err(|_| StreamError::named("grant_stream_draft_pull_failed"));
180 if let Err(error) = result.as_ref() {
181 self.state.update(|inner| {
182 inner.diagnostics.last_error = Some(error.name());
183 Ok(())
184 })?;
185 }
186 result
187 }
188
189 pub fn finish(&mut self) -> Result<(), StreamError> {
194 self.draft_hook = None;
195 self.state.stop.send_replace(true);
196 match self.worker.take() {
197 Some(worker) => worker
198 .join()
199 .map_err(|_| StreamError::named("grant_stream_worker_panicked"))?,
200 None => Ok(()),
201 }
202 }
203}
204
205impl Drop for GrantStreamOwner {
206 fn drop(&mut self) {
207 if let Err(error) = self.finish() {
208 tracing::error!(cause = error.name(), "grant stream shutdown failed");
209 }
210 }
211}
212
213fn config_error(message: &str) -> crate::ServerError {
214 crate::ServerError::ConfigValidation {
215 message: message.into(),
216 }
217}
218fn as_config(error: StreamError) -> crate::ServerError {
219 config_error(error.name())
220}
221
222pub fn finish_configured(owner: &mut Option<GrantStreamOwner>) -> Result<(), crate::ServerError> {
227 owner
228 .as_mut()
229 .map_or(Ok(()), |owner| owner.finish().map_err(as_config))
230}
231
232pub(crate) fn read_credential(
233 path: &std::path::Path,
234 bound: usize,
235) -> Result<zeroize::Zeroizing<String>, crate::ServerError> {
236 http::read_file(path, bound).map_err(as_config)
237}
238
239async fn read(
240 mut engine: engine::Engine,
241 http: http::Http,
242 root: Arc<crate::auth_lys::root_presence::RootPresence>,
243) -> Result<(), StreamError> {
244 let result = read_pages(&mut engine, &http, &root).await;
245 if let Err(error) = result.as_ref() {
246 engine.terminal(error.name())?;
247 }
248 result
249}
250
251async fn read_pages(
252 engine: &mut engine::Engine,
253 http: &http::Http,
254 root: &crate::auth_lys::root_presence::RootPresence,
255) -> Result<(), StreamError> {
256 let mut reopens = 0_usize;
257 loop {
258 if *http.stop.borrow() {
259 engine.lost()?;
260 return Ok(());
261 }
262 let before = engine.state.snapshot()?;
263 let question = ChangesRequest {
264 log: Some(LogName {
265 identity: engine.state.log.identity.clone(),
266 epoch: engine.state.log.epoch,
267 }),
268 after: before.cursor,
269 limit: engine.config.page_positions,
270 wait: true,
271 placements: true,
272 placed: before.placed,
273 };
274 engine.state.update(|inner| {
275 inner.diagnostics.requests = inner
276 .diagnostics
277 .requests
278 .checked_add(1)
279 .ok_or(StreamError::named("grant_stream_counter_exhausted"))?;
280 Ok(())
281 })?;
282 let page = match http
283 .post::<_, ChangesPage>("grants/changes", &question)
284 .await
285 {
286 Ok(page) => page,
287 Err(error) if error.name() == "grant_stream_stopped" => {
288 engine.lost()?;
289 return Ok(());
290 }
291 Err(error) if error.transport_ended => {
292 engine.lost()?;
293 if reopens >= engine.config.max_transport_reopens {
294 return Err(error);
295 }
296 reopens += 1;
297 engine.state.update(|inner| {
298 inner.diagnostics.reopens = inner
299 .diagnostics
300 .reopens
301 .checked_add(1)
302 .ok_or(StreamError::named("grant_stream_counter_exhausted"))?;
303 Ok(())
304 })?;
305 continue;
306 }
307 Err(error) => {
308 engine.lost()?;
309 return Err(error);
310 }
311 };
312 if let Err(error) = engine.page(page) {
313 if error.name() == "grant_stream_membership_failed" && *http.stop.borrow() {
314 engine.lost()?;
315 return Ok(());
316 }
317 return Err(error);
318 }
319 if engine.take_root_owed() {
321 root.ask_now().await;
322 }
323 let after = engine.state.snapshot()?;
324 if (before.cursor, before.placed) == (after.cursor, after.placed) {
325 return Err(StreamError::named("grant_stream_no_progress"));
326 }
327 reopens = 0;
328 }
329}
330
331#[cfg(test)]
332#[path = "grant_stream_owner/tests.rs"]
333pub(crate) mod tests;