1use super::GrantMode;
14use super::lys_pass_adapter::{LiveAnswer, LiveLysAsk};
15use lys_pass::deliberate::CheckAnswer;
16use lys_pass::drafts::{DraftRequest, request_digest};
17use lys_pass::{Client, Error, Mode, Target};
18use std::sync::Arc;
19use std::sync::atomic::{AtomicU64, Ordering};
20use std::sync::{Mutex, Weak};
21use std::time::{Duration, Instant};
22
23static ASK_METRICS: Mutex<Vec<Weak<AskMetrics>>> = Mutex::new(Vec::new());
24
25#[derive(Debug, Default)]
26struct AskMetrics {
27 count: AtomicU64,
28 maximum_ns: AtomicU64,
29}
30
31#[derive(Clone, Copy, Debug, Default, serde::Serialize)]
33pub(crate) struct AskStatistics {
34 pub count: u64,
35 pub max_elapsed_ms: f64,
36}
37
38impl AskMetrics {
39 fn record(&self, elapsed: Duration) {
40 let nanos = u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX);
41 self.maximum_ns.fetch_max(nanos, Ordering::Relaxed);
42 self.count.fetch_add(1, Ordering::Release);
43 }
44
45 fn snapshot(&self) -> AskStatistics {
46 AskStatistics {
47 count: self.count.load(Ordering::Acquire),
48 max_elapsed_ms: Duration::from_nanos(self.maximum_ns.load(Ordering::Relaxed))
49 .as_secs_f64()
50 * 1000.0,
51 }
52 }
53}
54
55pub(crate) fn ask_statistics() -> Result<AskStatistics, Error> {
57 let mut clients = ASK_METRICS
58 .lock()
59 .map_err(|_| Error::CannotAsk("Lys ask metrics registry poisoned"))?;
60 let mut total = AskStatistics::default();
61 clients.retain(|client| {
62 let Some(client) = client.upgrade() else {
63 return false;
64 };
65 let snapshot = client.snapshot();
66 total.count = total.count.saturating_add(snapshot.count);
67 total.max_elapsed_ms = total.max_elapsed_ms.max(snapshot.max_elapsed_ms);
68 true
69 });
70 drop(clients);
71 Ok(total)
72}
73
74pub struct HttpLysAsk {
76 client: Client,
77 metrics: Arc<AskMetrics>,
78 handle: tokio::runtime::Handle,
79 asker: Option<String>,
82}
83
84pub fn lys_origin(url: &str) -> Result<reqwest::Url, Error> {
90 let mut url: reqwest::Url = url
91 .parse()
92 .map_err(|_| Error::Invalid("issuer is not a URL"))?;
93 if matches!(url.path(), "/api" | "/api/") {
94 url.set_path("/");
95 }
96 Ok(url)
97}
98
99impl std::fmt::Debug for HttpLysAsk {
100 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 formatter.debug_struct("HttpLysAsk").finish_non_exhaustive()
102 }
103}
104
105impl HttpLysAsk {
106 pub fn new(issuer: &str, handle: tokio::runtime::Handle) -> Result<Self, Error> {
111 let issuer = lys_origin(issuer)?;
112 let metrics = Arc::new(AskMetrics::default());
113 {
114 let mut clients = ASK_METRICS
115 .lock()
116 .map_err(|_| Error::CannotAsk("Lys ask metrics registry poisoned"))?;
117 clients.retain(|client| client.strong_count() > 0);
118 clients.push(Arc::downgrade(&metrics));
119 }
120 Ok(Self {
121 client: Client::new(
122 reqwest::Client::builder()
123 .tcp_keepalive(Duration::from_secs(30))
124 .pool_idle_timeout(None)
125 .build()
126 .map_err(|error| Error::Transport(Box::new(error)))?,
127 issuer,
128 )?,
129 handle,
130 metrics,
131 asker: None,
132 })
133 }
134
135 #[must_use]
138 pub fn asking_as(mut self, credential: String) -> Self {
139 self.asker = Some(credential);
140 self
141 }
142
143 #[cfg(test)]
144 fn statistics(&self) -> AskStatistics {
145 self.metrics.snapshot()
146 }
147
148 fn block<T, F>(&self, route: &'static str, future: F) -> Result<T, Error>
150 where
151 T: Send,
152 F: std::future::Future<Output = Result<T, Error>> + Send,
153 {
154 let started = Instant::now();
155 let handle = &self.handle;
156 let answer =
157 std::thread::scope(|scope| scope.spawn(move || handle.block_on(future)).join())
158 .unwrap_or_else(|_| Err(Error::CannotAsk("the live ask did not complete")));
159 let elapsed = started.elapsed();
160 self.metrics.record(elapsed);
161 tracing::debug!(target: "liminal::lys_ask", route, ok = answer.is_ok(),
162 elapsed_ms = elapsed.as_secs_f64() * 1000.0, "Lys ask completed");
163 answer
164 }
165
166 fn draft(
169 &self,
170 pass: &str,
171 holder: &str,
172 target: &Target,
173 grant: &str,
174 judgment: &CheckAnswer,
175 ) -> Result<String, Error> {
176 let words = format!(
177 "{holder} asks {} on {}:{}",
178 target.action, target.kind, target.id
179 );
180 let draft = DraftRequest {
181 operation: format!(
182 "liminal:{holder}:{}:{}:{}",
183 target.kind, target.id, target.action
184 ),
185 grant: grant.to_owned(),
186 target: Target::new(&target.kind, &target.id, &target.action)?,
187 request_digest: request_digest(&words),
188 words,
189 };
190 self.block(
191 "/api/product-drafts",
192 self.client.create_draft(pass, &draft, judgment),
193 )
194 .map(|created| created.draft)
195 }
196}
197
198impl LiveLysAsk for HttpLysAsk {
199 fn ask(
200 &self,
201 pass: &str,
202 holder: &str,
203 target: &Target,
204 _now: u64,
205 ) -> Result<LiveAnswer, Error> {
206 let answer = self.block(
207 "/api/grants/check/batch",
208 self.client.check_batch(
209 self.asker.as_deref().unwrap_or(pass),
210 holder,
211 std::slice::from_ref(target),
212 None,
213 ),
214 )?;
215 let Some(result) = answer.results.into_iter().next() else {
216 return Err(Error::CannotAsk("the live answer carried no result"));
217 };
218 match (
219 result.allowed,
220 result.mode.as_ref(),
221 result.grant.as_deref(),
222 result.refusal.as_deref(),
223 ) {
224 (true, Some(Mode::Outright), Some(grant), None) => Ok(LiveAnswer::Outright {
225 grant: grant.to_owned(),
226 }),
227 (false, Some(Mode::ByDraft), Some(grant), None) => Ok(LiveAnswer::Held {
228 mode: GrantMode::ByDraft,
229 grant: grant.to_owned(),
230 draft: self.draft(pass, holder, target, grant, &result)?,
231 }),
232 (false, Some(Mode::ByTwo), Some(grant), None) => Ok(LiveAnswer::Held {
233 mode: GrantMode::ByTwo,
234 grant: grant.to_owned(),
235 draft: self.draft(pass, holder, target, grant, &result)?,
236 }),
237 (false, None, None, Some(refusal)) => Ok(LiveAnswer::Refused {
238 refusal: refusal.to_owned(),
239 }),
240 _ => Err(Error::CannotAsk("inconsistent live permission answer")),
241 }
242 }
243}
244
245#[cfg(test)]
246#[path = "live/timing_tests.rs"]
247mod timing_tests;