Skip to main content

ljos_cli/
pack_link.rs

1//! The only speaker that may carry this seat's packsetd token.
2//!
3//! The token stays in this module. Each request is one byte stream to the
4//! socket address resolved for the writer's URL, with no proxy and no
5//! redirect. The proof runs before the token is written on that stream. A
6//! stream that closes is not remembered: the next call proves again before
7//! the token goes out. packsetd answers one request and closes, so the two
8//! refusals and the call that carries the token are three sockets in a row;
9//! the proof is not kept for a later call.
10
11use std::collections::HashMap;
12use std::io::{Read, Write};
13use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
14use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
15use std::sync::{Arc, Mutex, OnceLock};
16use std::time::{Duration, Instant, SystemTime};
17
18use packset_client::{Error, Hit};
19use serde::de::DeserializeOwned;
20use serde_json::Value;
21use url::Url;
22
23/// The seat's pack writer. The token field is private to this module.
24pub struct PacksetClient {
25    base: String,
26    origin: Url,
27    addr: Option<SocketAddr>,
28    workspace: Option<String>,
29    token: Mutex<Option<String>>,
30}
31
32impl Clone for PacksetClient {
33    fn clone(&self) -> Self {
34        Self {
35            base: self.base.clone(),
36            origin: self.origin.clone(),
37            addr: self.addr,
38            workspace: self.workspace.clone(),
39            token: Mutex::new(self.token.lock().unwrap_or_else(|p| p.into_inner()).clone()),
40        }
41    }
42}
43
44/// One answer. `trusted` means the token was on this answer, after the
45/// proof held.
46#[derive(Debug, Clone)]
47pub(crate) struct Answer {
48    pub status: u16,
49    pub body: String,
50    pub trusted: bool,
51    /// `Retry-After` when the writer sent one, in whole seconds.
52    pub retry_after: Option<Duration>,
53}
54
55/// Answers a hook process already has for a search or a status read, by
56/// request, for [`ANSWER_MEMO`]. A prompt hook asks the same search
57/// twice and the status twice; the second asks are answered here. Only a
58/// hook keeps one, and any write through the client drops it.
59type KeptAnswer = (String, Instant, Answer);
60static KEPT_ANSWERS: Mutex<Vec<KeptAnswer>> = Mutex::new(Vec::new());
61const ANSWER_MEMO: Duration = Duration::from_secs(2);
62
63fn answer_memo_key(
64    method: &str,
65    path: &str,
66    query: &[(&str, &str)],
67    bearer: bool,
68) -> Option<String> {
69    if method != "GET" || !matches!(path, "/v1/search" | "/v1/status") {
70        return None;
71    }
72    std::env::var_os("LJOS_IN_HOOK")?;
73    let mut key = format!("{path}|{bearer}");
74    for (k, v) in query {
75        key.push('|');
76        key.push_str(k);
77        key.push('=');
78        key.push_str(v);
79    }
80    Some(key)
81}
82
83fn kept_answer(key: &str) -> Option<Answer> {
84    let kept = KEPT_ANSWERS.lock().unwrap_or_else(|p| p.into_inner());
85    kept.iter()
86        .find(|(k, at, _)| k == key && at.elapsed() < ANSWER_MEMO)
87        .map(|(_, _, answer)| answer.clone())
88}
89
90fn keep_answer(key: String, answer: &Answer) {
91    let mut kept = KEPT_ANSWERS.lock().unwrap_or_else(|p| p.into_inner());
92    kept.retain(|(k, _, _)| k != &key);
93    kept.push((key, Instant::now(), answer.clone()));
94}
95
96/// Drop every kept answer. Called on any write through the client.
97pub(crate) fn forget_kept_answers() {
98    KEPT_ANSWERS
99        .lock()
100        .unwrap_or_else(|p| p.into_inner())
101        .clear();
102}
103
104struct Slot {
105    stream: Option<TcpStream>,
106    /// The proof held on `stream` and the stream is still the one it held on.
107    proven: bool,
108    token_tag: u64,
109    /// The last answer on this socket carried the token.
110    carried: bool,
111}
112
113fn slots() -> &'static Mutex<HashMap<SocketAddr, Arc<Mutex<Slot>>>> {
114    static SLOTS: OnceLock<Mutex<HashMap<SocketAddr, Arc<Mutex<Slot>>>>> = OnceLock::new();
115    SLOTS.get_or_init(|| Mutex::new(HashMap::new()))
116}
117
118fn slot_for(addr: SocketAddr) -> Arc<Mutex<Slot>> {
119    let mut map = slots().lock().unwrap_or_else(|p| p.into_inner());
120    map.entry(addr)
121        .or_insert_with(|| {
122            Arc::new(Mutex::new(Slot {
123                stream: None,
124                proven: false,
125                token_tag: 0,
126                carried: false,
127            }))
128        })
129        .clone()
130}
131
132fn load_token() -> Option<String> {
133    packset_client::token()
134}
135
136fn token_tag(token: Option<&str>) -> u64 {
137    let Some(token) = token else {
138        return 0;
139    };
140    let mut hash = 0xcbf29ce484222325u64;
141    for byte in token.as_bytes() {
142        hash ^= u64::from(*byte);
143        hash = hash.wrapping_mul(0x100000001b3);
144    }
145    hash
146}
147
148fn nonce() -> Option<String> {
149    use std::io::Read as _;
150    let mut raw = [0u8; 32];
151    std::fs::File::open("/dev/urandom")
152        .and_then(|mut file| file.read_exact(&mut raw))
153        .ok()
154        .map(|()| hex::encode(raw))
155}
156
157fn pin(url: &Url) -> Option<SocketAddr> {
158    let host = url.host_str()?;
159    let port = url.port_or_known_default()?;
160    if !crate::tool_bound::armed() {
161        return (host, port).to_socket_addrs().ok()?.next();
162    }
163    let left = crate::wall_left().unwrap_or(Duration::from_millis(1));
164    if left.is_zero() {
165        crate::tool_bound::note("packsetd");
166        crate::tool_bound::mark_failed("packsetd timed out");
167        return None;
168    }
169    let host = host.to_string();
170    let (tx, rx) = std::sync::mpsc::channel();
171    std::thread::spawn(move || {
172        let found = (host.as_str(), port)
173            .to_socket_addrs()
174            .ok()
175            .and_then(|mut addrs| addrs.next());
176        let _ = tx.send(found);
177    });
178    match rx.recv_timeout(left) {
179        Ok(addr) => addr,
180        Err(_) => {
181            crate::tool_bound::note("packsetd");
182            crate::tool_bound::mark_failed("packsetd timed out");
183            None
184        }
185    }
186}
187
188fn host_header(url: &Url) -> String {
189    match (url.host_str(), url.port()) {
190        (Some(host), Some(port)) if host.contains(':') => format!("[{host}]:{port}"),
191        (Some(host), Some(port)) => format!("{host}:{port}"),
192        (Some(host), None) => host.to_string(),
193        _ => "localhost".to_string(),
194    }
195}
196
197fn budget() -> Duration {
198    let base = crate::pack_budget_ms()
199        .or_else(|| {
200            std::env::var("PACKSET_TIMEOUT_MS")
201                .ok()
202                .and_then(|v| v.trim().parse::<u64>().ok())
203                .filter(|ms| *ms > 0)
204        })
205        .map_or(Duration::from_secs(30), Duration::from_millis);
206    match crate::wall_left() {
207        Some(left) => base.min(left).max(Duration::from_millis(1)),
208        None => base,
209    }
210}
211
212struct Clock {
213    deadline: Instant,
214    depth: u32,
215}
216
217thread_local! {
218    /// This thread's pack budget. A nested call keeps the earlier deadline.
219    /// Another thread has its own: one search must not spend a read that
220    /// is running beside it.
221    static CLOCK: std::cell::RefCell<Option<Clock>> = const { std::cell::RefCell::new(None) };
222}
223
224/// Bound every pack socket call on this thread until this guard drops.
225/// A nested guard on the same thread keeps the earlier deadline, so a
226/// hook stays under 2s and an interactive command under 5s across every
227/// socket of the call. A call on another thread keeps the budget it armed.
228pub struct PackClock {
229    /// Drop has to run on the thread that entered, where the clock lives.
230    _thread: std::marker::PhantomData<*const ()>,
231}
232
233impl PackClock {
234    #[must_use]
235    pub fn enter(total: Duration) -> Self {
236        CLOCK.with(|slot| {
237            let mut slot = slot.borrow_mut();
238            match slot.as_mut() {
239                Some(clock) => clock.depth = clock.depth.saturating_add(1),
240                None => {
241                    *slot = Some(Clock {
242                        deadline: Instant::now() + total,
243                        depth: 1,
244                    });
245                }
246            }
247        });
248        Self {
249            _thread: std::marker::PhantomData,
250        }
251    }
252}
253
254impl Drop for PackClock {
255    fn drop(&mut self) {
256        CLOCK.with(|slot| {
257            let mut slot = slot.borrow_mut();
258            if let Some(clock) = slot.as_mut() {
259                clock.depth = clock.depth.saturating_sub(1);
260                if clock.depth == 0 {
261                    *slot = None;
262                }
263            }
264        });
265    }
266}
267
268fn clock_deadline() -> Option<Instant> {
269    CLOCK.with(|slot| slot.borrow().as_ref().map(|clock| clock.deadline))
270}
271
272pub(crate) fn clock_armed() -> bool {
273    clock_deadline().is_some()
274}
275
276/// Time left on the armed pack clock. `None` when no clock is armed.
277pub(crate) fn clock_left() -> Option<Duration> {
278    clock_deadline().map(|deadline| deadline.saturating_duration_since(Instant::now()))
279}
280
281/// A search or island read in this clock already went quiet, or the clock
282/// is spent. Later reads must not wait it out again.
283pub(crate) fn search_stalled() -> bool {
284    pack_quiet() || clock_deadline().is_some_and(|deadline| Instant::now() >= deadline)
285}
286
287pub(crate) fn error_is_timeout(err: &Error) -> bool {
288    match err {
289        Error::Io(io) => {
290            io_is_quiet(io)
291                || io.to_string().contains("the pack did not answer")
292                || io.to_string().contains("packsetd timed out")
293        }
294        _ => false,
295    }
296}
297
298/// Whether every socket listening on the loopback port of `url` belongs to
299/// another user. Unknown is not foreign.
300///
301/// A hook keeps its first answer for each url. The socket tables are read
302/// in full each time, a hook connects more than once, and it ends within
303/// the runner's cut-off. Elsewhere a search keeps its first answer until
304/// it ends ([`OwnerOnce`]), and any other call reads them again. A writer
305/// that stops leaves its port to whoever binds it next. A long-lived
306/// process that kept its first answer would go on carrying the token to
307/// another user's listener, or never to its own.
308fn foreign(url: &str) -> bool {
309    if std::env::var_os("LJOS_IN_HOOK").is_none() {
310        return SEARCH_OWNERS.with(|kept| match kept.borrow_mut().as_mut() {
311            Some(kept) => match kept.iter().find(|(u, _)| u == url) {
312                Some((_, known)) => *known,
313                None => {
314                    let answer = foreign_read(url);
315                    kept.push((url.to_string(), answer));
316                    answer
317                }
318            },
319            None => foreign_read(url),
320        });
321    }
322    static KNOWN: std::sync::LazyLock<Mutex<Vec<(String, bool)>>> =
323        std::sync::LazyLock::new(|| Mutex::new(Vec::new()));
324    if let Some((_, known)) = KNOWN
325        .lock()
326        .unwrap_or_else(|p| p.into_inner())
327        .iter()
328        .find(|(u, _)| u == url)
329    {
330        return *known;
331    }
332    let answer = foreign_read(url);
333    KNOWN
334        .lock()
335        .unwrap_or_else(|p| p.into_inner())
336        .push((url.to_string(), answer));
337    answer
338}
339
340thread_local! {
341    /// [`foreign`]'s answers for the search on this thread, by url, for
342    /// as long as an [`OwnerOnce`] lasts.
343    static SEARCH_OWNERS: std::cell::RefCell<Option<Vec<(String, bool)>>> =
344        const { std::cell::RefCell::new(None) };
345}
346
347/// Read the pack port's owner once for the requests of one search on this
348/// thread, until the guard drops. Without it, each request of the search
349/// that carries the token reads both tables. A guard inside another keeps
350/// the outer one's answers.
351pub(crate) struct OwnerOnce {
352    outer: bool,
353}
354
355impl OwnerOnce {
356    #[must_use]
357    pub(crate) fn enter() -> Self {
358        let outer = SEARCH_OWNERS.with(|kept| {
359            let mut kept = kept.borrow_mut();
360            let outer = kept.is_none();
361            if outer {
362                *kept = Some(Vec::new());
363            }
364            outer
365        });
366        Self { outer }
367    }
368}
369
370impl Drop for OwnerOnce {
371    fn drop(&mut self) {
372        if self.outer {
373            SEARCH_OWNERS.with(|kept| *kept.borrow_mut() = None);
374        }
375    }
376}
377
378#[cfg(test)]
379thread_local! {
380    /// How many times this thread read the socket tables.
381    static TABLE_READS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
382}
383
384fn foreign_read(url: &str) -> bool {
385    #[cfg(test)]
386    TABLE_READS.with(|reads| reads.set(reads.get() + 1));
387    #[cfg(target_os = "linux")]
388    {
389        use std::os::unix::fs::MetadataExt;
390        let Some(port) = loopback_port(url) else {
391            return false;
392        };
393        let Ok(me) = std::fs::metadata("/proc/self").map(|m| m.uid()) else {
394            return false;
395        };
396        let owners: Vec<u32> = ["/proc/net/tcp", "/proc/net/tcp6"]
397            .iter()
398            .filter_map(|table| std::fs::read_to_string(table).ok())
399            .flat_map(|text| listen_uids(&text, port))
400            .collect();
401        !owners.is_empty() && owners.iter().all(|uid| *uid != me)
402    }
403    #[cfg(not(target_os = "linux"))]
404    {
405        let _ = url;
406        false
407    }
408}
409
410#[cfg(target_os = "linux")]
411fn loopback_port(url: &str) -> Option<u16> {
412    let rest = url
413        .strip_prefix("http://")
414        .or_else(|| url.strip_prefix("https://"))?;
415    let authority = rest.split(['/', '?', '#']).next()?;
416    let (host, port) = match authority.rsplit_once(':') {
417        Some((host, port)) if !port.contains(']') => (host, port.parse().ok()?),
418        _ => (authority, 80),
419    };
420    matches!(host, "127.0.0.1" | "localhost" | "[::1]").then_some(port)
421}
422
423#[cfg(target_os = "linux")]
424fn listen_uids(table: &str, port: u16) -> Vec<u32> {
425    table
426        .lines()
427        .skip(1)
428        .filter_map(|line| {
429            let fields: Vec<&str> = line.split_whitespace().collect();
430            if fields.get(3) != Some(&"0A") {
431                return None;
432            }
433            let hex = fields.get(1)?.rsplit(':').next()?;
434            if u16::from_str_radix(hex, 16).ok()? != port {
435                return None;
436            }
437            fields.get(7)?.parse().ok()
438        })
439        .collect()
440}
441
442fn target(path: &str, query: &[(&str, &str)]) -> String {
443    if query.is_empty() {
444        return path.to_string();
445    }
446    let mut ser = url::form_urlencoded::Serializer::new(String::new());
447    for (key, value) in query {
448        ser.append_pair(key, value);
449    }
450    format!("{path}?{}", ser.finish())
451}
452
453struct Raw {
454    status: u16,
455    body: String,
456    keep: bool,
457    retry_after: Option<Duration>,
458}
459
460/// A sitting or MCP pack read. Longer reads keep their own budgets.
461const SHORT_READ: Duration = Duration::from_secs(1);
462
463/// How long one MCP pack read may take.
464///
465/// The usual read is [`SHORT_READ`], so a silent writer cannot spend the
466/// whole tool budget. A seat that raises `LJOS_MCP_TIMEOUT` above its
467/// thirty-second default and sets `PACKSET_TIMEOUT_MS` is waiting on one
468/// long read. That read uses the shorter of the two.
469fn mcp_read_cap() -> Duration {
470    let tool = crate::tool_bound::budget();
471    if tool > crate::tool_bound::DEFAULT {
472        if let Some(ms) = std::env::var("PACKSET_TIMEOUT_MS")
473            .ok()
474            .and_then(|value| value.trim().parse::<u64>().ok())
475            .filter(|ms| *ms > 0)
476        {
477            return Duration::from_millis(ms).min(tool);
478        }
479    }
480    SHORT_READ
481}
482const HEALTH_BUDGET: Duration = Duration::from_secs(2);
483const STATUS_BUDGET: Duration = Duration::from_secs(2);
484
485thread_local! {
486    /// The command's admission deadline, as a wall time. A nested guard
487    /// keeps the sooner of the two, so a search inside a longer command
488    /// stays short. The socket clock is a separate bound.
489    static COMMAND_DEADLINE: std::cell::RefCell<Option<SystemTime>> = const { std::cell::RefCell::new(None) };
490    /// The `X-Packset-Deadline` for the request in flight, frozen when the
491    /// call starts so a retry does not push the deadline out.
492    static HELD_DEADLINE: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
493}
494
495/// How long this command is willing to wait for the pack. Search and
496/// recall pass a few seconds. Remember passes twenty. Import passes a minute.
497pub struct CommandDeadline {
498    prev: Option<SystemTime>,
499}
500
501impl CommandDeadline {
502    #[must_use]
503    pub fn enter(budget: Duration) -> Self {
504        let prev = COMMAND_DEADLINE.with(|cell| *cell.borrow());
505        let proposed = SystemTime::now() + budget;
506        let next = match prev {
507            Some(existing) if existing <= proposed => existing,
508            _ => proposed,
509        };
510        COMMAND_DEADLINE.with(|cell| *cell.borrow_mut() = Some(next));
511        Self { prev }
512    }
513}
514
515impl Drop for CommandDeadline {
516    fn drop(&mut self) {
517        let prev = self.prev.take();
518        COMMAND_DEADLINE.with(|cell| *cell.borrow_mut() = prev);
519    }
520}
521
522struct HeldDeadline {
523    prev: Option<String>,
524    active: bool,
525}
526
527impl HeldDeadline {
528    fn enter(budget: Duration) -> Self {
529        let prev = HELD_DEADLINE.with(|cell| cell.borrow().clone());
530        if prev.is_some() {
531            return Self {
532                prev,
533                active: false,
534            };
535        }
536        let stamp = deadline_stamp(budget);
537        HELD_DEADLINE.with(|cell| *cell.borrow_mut() = Some(stamp));
538        Self { prev, active: true }
539    }
540}
541
542impl Drop for HeldDeadline {
543    fn drop(&mut self) {
544        if self.active {
545            let prev = self.prev.clone();
546            HELD_DEADLINE.with(|cell| *cell.borrow_mut() = prev);
547        }
548    }
549}
550
551fn is_write(method: &str) -> bool {
552    !matches!(method, "GET" | "HEAD")
553}
554
555fn command_remaining() -> Option<Duration> {
556    let when = COMMAND_DEADLINE.with(|cell| *cell.borrow())?;
557    Some(
558        when.duration_since(SystemTime::now())
559            .unwrap_or(Duration::ZERO),
560    )
561}
562
563/// How long this call may run, and the offset on `X-Packset-Deadline`.
564/// The caller's budget wins. A thread timeout, `PACKSET_TIMEOUT_MS`, the
565/// wall clock, and a command or pack clock shorten it. An MCP read is
566/// about a second unless the seat raised the tool bound and the pack
567/// timeout. A sitting read is about a second because its clock is.
568fn tightened(method: &str, requested: Duration) -> Duration {
569    let mut budget = requested;
570    if let Some(ms) = crate::pack_budget_ms() {
571        budget = budget.min(Duration::from_millis(ms.max(1)));
572    } else if let Some(ms) = std::env::var("PACKSET_TIMEOUT_MS")
573        .ok()
574        .and_then(|v| v.trim().parse::<u64>().ok())
575        .filter(|ms| *ms > 0)
576    {
577        budget = budget.min(Duration::from_millis(ms));
578    }
579    if let Some(left) = crate::wall_left() {
580        budget = budget.min(left);
581    }
582    if !is_write(method) && crate::tool_bound::armed() {
583        budget = budget.min(mcp_read_cap());
584    }
585    if let Some(left) = clock_left() {
586        budget = budget.min(left);
587    }
588    if let Some(left) = command_remaining() {
589        budget = budget.min(left);
590    }
591    budget.max(Duration::from_millis(1))
592}
593
594/// How long one call may take. [`tightened`].
595pub(crate) fn admission_retry_budget(method: &str, per_try: Duration) -> Duration {
596    tightened(method, per_try)
597}
598
599fn deadline_stamp(budget: Duration) -> String {
600    let proposed = SystemTime::now() + budget;
601    let when = match COMMAND_DEADLINE.with(|cell| *cell.borrow()) {
602        Some(command) if command < proposed => command,
603        _ => proposed,
604    };
605    unix_ms(when).to_string()
606}
607
608fn unix_ms(when: SystemTime) -> u64 {
609    when.duration_since(std::time::UNIX_EPOCH)
610        .map(|elapsed| u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX))
611        .unwrap_or(0)
612}
613
614fn current_deadline(method: &str) -> String {
615    HELD_DEADLINE
616        .with(|cell| cell.borrow().clone())
617        .unwrap_or_else(|| deadline_stamp(admission_retry_budget(method, budget())))
618}
619
620fn retry_after_of(header: Option<&str>) -> Option<Duration> {
621    let raw = header?.trim();
622    if raw.is_empty() {
623        return None;
624    }
625    raw.parse::<u64>().ok().map(Duration::from_secs)
626}
627
628/// Read or write shed, or a deadline that passed. The line is what the
629/// command prints.
630#[derive(Clone, Copy, Debug, PartialEq, Eq)]
631pub(crate) enum ShedKind {
632    Read,
633    Write,
634    Deadline,
635}
636
637thread_local! {
638    static SHED: std::cell::RefCell<Vec<(String, ShedKind)>> =
639        const { std::cell::RefCell::new(Vec::new()) };
640}
641
642fn note_shed(line: &str, kind: ShedKind) {
643    SHED.with(|cell| {
644        let mut cell = cell.borrow_mut();
645        cell.retain(|(text, _)| text != line);
646        cell.push((line.to_string(), kind));
647        if cell.len() > 32 {
648            cell.remove(0);
649        }
650    });
651}
652
653pub(crate) fn shed_kind(line: &str) -> Option<ShedKind> {
654    SHED.with(|cell| {
655        cell.borrow()
656            .iter()
657            .rev()
658            .find(|(text, _)| text == line)
659            .map(|(_, kind)| *kind)
660    })
661}
662
663/// Time left until the frozen `X-Packset-Deadline`. `None` when no call
664/// is in flight.
665fn held_remaining() -> Option<Duration> {
666    let stamp = HELD_DEADLINE.with(|cell| cell.borrow().clone())?;
667    let ms: u64 = stamp.parse().ok()?;
668    let now = unix_ms(SystemTime::now());
669    Some(Duration::from_millis(ms.saturating_sub(now)))
670}
671
672fn deadline_passed() -> bool {
673    held_remaining().is_some_and(|left| left.is_zero())
674}
675
676/// `wait` finishes before the frozen deadline, so the resend still has time.
677fn retry_room(wait: Duration) -> bool {
678    held_remaining().is_some_and(|left| wait < left)
679}
680
681/// The JSON `error` field, as one line. A deadline with no field uses the
682/// deadline sentence. Anything else with no field is `degraded`.
683fn fault_line(body: &str, deadline: bool) -> String {
684    let parsed = serde_json::from_str::<Value>(body).ok().and_then(|value| {
685        value
686            .get("error")
687            .and_then(Value::as_str)
688            .map(str::to_string)
689    });
690    if let Some(text) = parsed {
691        let text = text.trim();
692        if let Some(line) = text.lines().next().map(str::trim).filter(|s| !s.is_empty()) {
693            return line.to_string();
694        }
695    }
696    if deadline {
697        return super::PACK_DEADLINE_LINE.to_string();
698    }
699    let raw = body.trim();
700    if !raw.is_empty() && !raw.contains('\n') && raw.len() <= 160 && !raw.starts_with('{') {
701        return raw.to_string();
702    }
703    "degraded".to_string()
704}
705
706fn degrade(body: &str, method: &str, deadline: bool) -> Error {
707    let line = fault_line(body, deadline);
708    let kind = if deadline {
709        ShedKind::Deadline
710    } else if is_write(method) {
711        ShedKind::Write
712    } else {
713        ShedKind::Read
714    };
715    note_shed(&line, kind);
716    Error::Unavailable(line)
717}
718
719/// The pack accepted a connection and then stayed silent, or the connect
720/// itself never finished. While a [`QuietPack`] guard is alive, the first
721/// such read still waits out its budget and a later read returns before it
722/// opens another socket. A consensus reading asks several times. Without
723/// the stop, each ask waited the whole budget and the command sat for the
724/// sum. Dropping the guard clears it, so a later call can try the pack again.
725static PACK_QUIET: AtomicBool = AtomicBool::new(false);
726static QUIET_HELD: AtomicUsize = AtomicUsize::new(0);
727
728/// Remember one quiet pack for the reads this guard covers.
729pub struct QuietPack;
730
731impl QuietPack {
732    /// Start remembering a silent pack. The first guard of a pile drops a
733    /// quiet read already seen, so the pile starts with a fresh try.
734    #[must_use]
735    pub fn enter() -> Self {
736        if QUIET_HELD.fetch_add(1, Ordering::AcqRel) == 0 {
737            PACK_QUIET.store(false, Ordering::Relaxed);
738        }
739        Self
740    }
741}
742
743impl Drop for QuietPack {
744    fn drop(&mut self) {
745        if QUIET_HELD.fetch_sub(1, Ordering::AcqRel) == 1 {
746            PACK_QUIET.store(false, Ordering::Relaxed);
747        }
748    }
749}
750
751fn holding_quiet() -> bool {
752    QUIET_HELD.load(Ordering::Relaxed) > 0
753}
754
755fn pack_quiet() -> bool {
756    holding_quiet() && PACK_QUIET.load(Ordering::Relaxed)
757}
758
759fn mark_pack_quiet() {
760    if holding_quiet() {
761        PACK_QUIET.store(true, Ordering::Relaxed);
762    }
763}
764
765/// The tool call's own bound is spent. A shorter read, one atom inside
766/// its own budget, times out while the call still has time and the caller
767/// carries on. That read is not the call failing.
768fn tool_bound_spent() -> bool {
769    crate::tool_bound::remaining().is_some_and(|left| left <= Duration::from_millis(50))
770}
771
772fn pack_io(err: std::io::Error) -> std::io::Error {
773    let quiet = io_is_quiet(&err) || err.to_string().contains("the pack did not answer");
774    if quiet && tool_bound_spent() {
775        crate::tool_bound::note("packsetd");
776        crate::tool_bound::mark_failed("packsetd timed out");
777        return std::io::Error::new(std::io::ErrorKind::TimedOut, "packsetd timed out");
778    }
779    err
780}
781
782fn quiet_io() -> std::io::Error {
783    pack_io(std::io::Error::new(
784        std::io::ErrorKind::TimedOut,
785        "the pack did not answer",
786    ))
787}
788
789/// A read or a connect that ended because the socket stayed silent.
790/// A refused connect is not this: the writer may still be starting.
791fn io_is_quiet(err: &std::io::Error) -> bool {
792    matches!(
793        err.kind(),
794        std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
795    )
796}
797
798fn connect(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
799    if pack_quiet() {
800        return Err(quiet_io());
801    }
802    if side_budget_spent() {
803        return Err(budget_spent());
804    }
805    let started = Instant::now();
806    let result = connect_once(addr, timeout);
807    // A connect that returns a socket is the pack accepting. Time spent
808    // there is not a quiet wait. A connect that fails spends the budget.
809    // A connect that never finishes is remembered, so the next read does
810    // not wait the budget out again.
811    if let Err(err) = &result {
812        if io_is_quiet(err) {
813            mark_pack_quiet();
814        }
815        super::charge_side_wait(started.elapsed());
816    }
817    result
818}
819
820/// A side event's pack budget is gone. Opening a socket for a one
821/// millisecond timer still blocks, and a prompt makes several of those
822/// reads, so the call returns before the connect.
823fn side_budget_spent() -> bool {
824    super::side_remaining_ms() == Some(0)
825}
826
827fn budget_spent() -> std::io::Error {
828    std::io::Error::new(
829        std::io::ErrorKind::TimedOut,
830        "the side event's pack budget is spent",
831    )
832}
833
834fn connect_once(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
835    let wait = bounded_wait(timeout, true)?;
836    let stream = TcpStream::connect_timeout(&addr, wait)?;
837    let io_wait = bounded_wait(timeout, false)?;
838    stream.set_read_timeout(Some(io_wait))?;
839    stream.set_write_timeout(Some(io_wait))?;
840    let _ = stream.set_nodelay(true);
841    Ok(stream)
842}
843
844/// The wait a socket call may use. With a [`PackClock`], a connect is
845/// capped by [`super::pack_connect_budget`] and every call stops at the
846/// clock. Past the clock the call returns before it opens a socket.
847fn bounded_wait(requested: Duration, connect: bool) -> std::io::Result<Duration> {
848    let mut wait = capped_wait(requested);
849    if let Some(deadline) = clock_deadline() {
850        let left = deadline.saturating_duration_since(Instant::now());
851        if left.is_zero() {
852            return Err(pack_io(std::io::Error::new(
853                std::io::ErrorKind::TimedOut,
854                "the pack did not answer",
855            )));
856        }
857        wait = wait.min(left);
858        if connect {
859            wait = wait.min(super::pack_connect_budget());
860        }
861        if wait.is_zero() {
862            return Err(pack_io(std::io::Error::new(
863                std::io::ErrorKind::TimedOut,
864                "the pack did not answer",
865            )));
866        }
867    }
868    Ok(wait)
869}
870
871/// A side-event bound shrinks this wait. A zero bound is one millisecond,
872/// so a later read fails at once instead of taking the default.
873fn capped_wait(timeout: Duration) -> Duration {
874    let wait = super::cap_pack_wait(timeout);
875    if wait.is_zero() {
876        Duration::from_millis(1)
877    } else {
878        wait
879    }
880}
881
882fn write_all(stream: &mut TcpStream, bytes: &[u8]) -> std::io::Result<()> {
883    let mut sent = 0;
884    while sent < bytes.len() {
885        match stream.write(&bytes[sent..]) {
886            Ok(0) => {
887                return Err(std::io::Error::new(
888                    std::io::ErrorKind::WriteZero,
889                    "the pack closed the socket",
890                ))
891            }
892            Ok(n) => sent += n,
893            Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
894            Err(err) => return Err(pack_io(err)),
895        }
896    }
897    stream.flush()
898}
899
900fn read_response(stream: &mut TcpStream, deadline: Instant) -> std::io::Result<Raw> {
901    let mut buf = Vec::new();
902    let mut tmp = [0u8; 4096];
903    let head_end = loop {
904        if buf.len() > 64 * 1024 {
905            return Err(std::io::Error::new(
906                std::io::ErrorKind::InvalidData,
907                "the pack's headers are too long",
908            ));
909        }
910        arm_read(stream, deadline)?;
911        let n = read_some(stream, &mut tmp)?;
912        buf.extend_from_slice(&tmp[..n]);
913        if let Some(at) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
914            break at;
915        }
916    };
917    let header = String::from_utf8_lossy(&buf[..head_end]).into_owned();
918    let status = header
919        .lines()
920        .next()
921        .and_then(|line| line.split_whitespace().nth(1))
922        .and_then(|code| code.parse().ok())
923        .ok_or_else(|| {
924            std::io::Error::new(
925                std::io::ErrorKind::InvalidData,
926                "the pack's status line is not HTTP",
927            )
928        })?;
929    let mut length = None;
930    let mut close = header.starts_with("HTTP/1.0");
931    let mut retry_after = None;
932    for line in header.lines().skip(1) {
933        let Some((name, value)) = line.split_once(':') else {
934            continue;
935        };
936        if name.eq_ignore_ascii_case("content-length") {
937            length = value.trim().parse().ok();
938        } else if name.eq_ignore_ascii_case("retry-after") {
939            retry_after = retry_after_of(Some(value));
940        } else if name.eq_ignore_ascii_case("connection")
941            && value.trim().eq_ignore_ascii_case("close")
942        {
943            close = true;
944        }
945    }
946    let mut body = buf.split_off(head_end + 4);
947    if let Some(length) = length {
948        while body.len() < length {
949            arm_read(stream, deadline)?;
950            let n = read_some(stream, &mut tmp)?;
951            body.extend_from_slice(&tmp[..n]);
952            if body.len() > length.saturating_add(64 * 1024) {
953                break;
954            }
955        }
956        body.truncate(length);
957    } else {
958        loop {
959            if deadline.saturating_duration_since(Instant::now()).is_zero() {
960                break;
961            }
962            let _ = arm_read(stream, deadline);
963            match stream.read(&mut tmp) {
964                Ok(0) => break,
965                Ok(n) => body.extend_from_slice(&tmp[..n]),
966                Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
967                Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
968                Err(err) if err.kind() == std::io::ErrorKind::TimedOut => break,
969                Err(_) => break,
970            }
971        }
972        close = true;
973    }
974    // A redirect is the answer. The token must not follow it.
975    Ok(Raw {
976        status,
977        body: String::from_utf8_lossy(&body).into_owned(),
978        keep: !close && !(300..400).contains(&status),
979        retry_after,
980    })
981}
982
983fn arm_read(stream: &TcpStream, deadline: Instant) -> std::io::Result<()> {
984    let remain = deadline.saturating_duration_since(Instant::now());
985    if remain.is_zero() {
986        return Err(pack_io(std::io::Error::new(
987            std::io::ErrorKind::TimedOut,
988            "the pack did not answer",
989        )));
990    }
991    stream.set_read_timeout(Some(remain))?;
992    Ok(())
993}
994
995fn read_some(stream: &mut TcpStream, tmp: &mut [u8]) -> std::io::Result<usize> {
996    loop {
997        match stream.read(tmp) {
998            Ok(0) => {
999                return Err(std::io::Error::new(
1000                    std::io::ErrorKind::UnexpectedEof,
1001                    "the pack closed the socket",
1002                ))
1003            }
1004            Ok(n) => return Ok(n),
1005            Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
1006            Err(err) => return Err(pack_io(err)),
1007        }
1008    }
1009}
1010
1011fn render(
1012    method: &str,
1013    host: &str,
1014    path: &str,
1015    token: Option<&str>,
1016    body: Option<&[u8]>,
1017) -> Vec<u8> {
1018    let deadline = current_deadline(method);
1019    let mut head = format!(
1020        "{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: keep-alive\r\nX-Packset-Deadline: {deadline}\r\n"
1021    );
1022    if let Some(token) = token {
1023        head.push_str("Authorization: Bearer ");
1024        head.push_str(token);
1025        head.push_str("\r\n");
1026    }
1027    let mut out = Vec::new();
1028    if let Some(body) = body {
1029        head.push_str("Content-Type: application/json\r\n");
1030        head.push_str(&format!("Content-Length: {}\r\n\r\n", body.len()));
1031        out.extend(head.into_bytes());
1032        out.extend_from_slice(body);
1033    } else {
1034        head.push_str("\r\n");
1035        out.extend(head.into_bytes());
1036    }
1037    out
1038}
1039
1040struct Wire<'a> {
1041    method: &'a str,
1042    path: &'a str,
1043    query: &'a [(&'a str, &'a str)],
1044    body: Option<&'a [u8]>,
1045    token: Option<&'a str>,
1046}
1047
1048enum Gate {
1049    /// The proof held. The call may carry the token.
1050    Send,
1051    /// The listener did not ask for the token.
1052    Bare,
1053    /// The writer is busy. The call is not sent.
1054    Busy(Raw),
1055}
1056
1057impl PacksetClient {
1058    pub fn new(base: impl Into<String>) -> Self {
1059        let mut base = base.into();
1060        while base.ends_with('/') {
1061            base.pop();
1062        }
1063        let origin = Url::parse(&base).unwrap_or_else(|_| Url::parse("http://127.0.0.1/").unwrap());
1064        let addr = Url::parse(&base).ok().as_ref().and_then(pin);
1065        Self {
1066            base,
1067            origin,
1068            addr,
1069            workspace: None,
1070            token: Mutex::new(load_token()),
1071        }
1072    }
1073
1074    /// The writer the seat talks to. `PACKSET_URL=off` is no pack.
1075    pub fn from_env() -> Result<Self, Error> {
1076        packset_client::load_seat_env();
1077        let url = std::env::var("PACKSET_URL")
1078            .or_else(|_| std::env::var("INSIDE_MEMORY_URL"))
1079            .ok()
1080            .filter(|url| !url.is_empty());
1081        match url {
1082            Some(url) if url == "off" => Err(Error::NoUrl),
1083            Some(url) => Ok(Self::new(url)),
1084            None => Ok(Self::new(format!(
1085                "http://127.0.0.1:{}",
1086                packset_client::default_port()
1087            ))),
1088        }
1089    }
1090
1091    #[must_use]
1092    pub fn with_workspace(mut self, workspace: impl Into<String>) -> Self {
1093        let workspace = workspace.into();
1094        self.workspace = (!workspace.is_empty()).then_some(workspace);
1095        self
1096    }
1097
1098    pub fn base(&self) -> &str {
1099        &self.base
1100    }
1101
1102    /// Whether this seat has a token to show. The token itself stays here.
1103    #[must_use]
1104    pub(crate) fn has_token(&self) -> bool {
1105        load_token().is_some()
1106    }
1107
1108    /// Whether a call may carry the token: the seat has one, and another
1109    /// user does not hold the loopback port.
1110    #[must_use]
1111    pub(crate) fn may_carry(&self) -> bool {
1112        load_token().is_some() && !foreign(&self.base)
1113    }
1114
1115    /// Whether the last answer on this socket carried the token. A closed
1116    /// socket does not keep this for the next call: that call proves again.
1117    #[must_use]
1118    pub(crate) fn carried(&self) -> bool {
1119        let Some(addr) = self.addr else {
1120            return false;
1121        };
1122        let map = slots().lock().unwrap_or_else(|p| p.into_inner());
1123        map.get(&addr)
1124            .is_some_and(|slot| slot.lock().unwrap_or_else(|p| p.into_inner()).carried)
1125    }
1126
1127    pub fn workspace(&self) -> String {
1128        if let Some(workspace) = &self.workspace {
1129            return workspace.clone();
1130        }
1131        if let Ok(workspace) = std::env::var("PACKSET_WORKSPACE") {
1132            if !workspace.is_empty() {
1133                return workspace;
1134            }
1135        }
1136        let cwd = std::env::var("HOSTPACK_WORKSPACE")
1137            .ok()
1138            .map(|s| s.trim().to_string())
1139            .filter(|s| !s.is_empty())
1140            .map(std::path::PathBuf::from)
1141            .or_else(|| std::env::current_dir().ok())
1142            .unwrap_or_else(|| std::path::PathBuf::from("."));
1143        let abs = cwd.canonicalize().unwrap_or(cwd);
1144        let shown = abs.to_string_lossy().into_owned();
1145        if let Ok(answer) = self.roundtrip(
1146            "GET",
1147            "/v1/identity",
1148            &[("cwd", shown.as_str())],
1149            None,
1150            true,
1151            STATUS_BUDGET,
1152        ) {
1153            if answer.status == 200 {
1154                if let Ok(val) = serde_json::from_str::<Value>(&answer.body) {
1155                    if let Some(ws) = val.get("workspace").and_then(|v| v.as_str()) {
1156                        if !ws.is_empty() {
1157                            return ws.to_string();
1158                        }
1159                    }
1160                }
1161            }
1162        }
1163        format!("dir:{}", abs.display())
1164    }
1165
1166    /// One call. `bearer` asks to carry the token; it is left off when
1167    /// the proof does not hold or another user owns the port. A 3xx is
1168    /// returned as the answer and not followed. A 503 waits `Retry-After`
1169    /// once and is sent again with the same deadline. A second 503, or a
1170    /// deadline that has passed, is the JSON `error` field and stops.
1171    pub(crate) fn roundtrip(
1172        &self,
1173        method: &str,
1174        path: &str,
1175        query: &[(&str, &str)],
1176        body: Option<&[u8]>,
1177        bearer: bool,
1178        timeout: Duration,
1179    ) -> Result<Answer, Error> {
1180        // A write changes what a listing would say: the memo this process
1181        // keeps and the gate's freshness mark both go.
1182        if method != "GET" {
1183            crate::forget_atom_listings();
1184            crate::forget_gate_fresh();
1185            forget_kept_answers();
1186        }
1187        let memo_key = answer_memo_key(method, path, query, bearer);
1188        if let Some(answer) = memo_key.as_deref().and_then(kept_answer) {
1189            return Ok(answer);
1190        }
1191        let Some(addr) = self.addr else {
1192            return Err(Error::Bad(format!(
1193                "{} is not a socket this seat can pin",
1194                self.base
1195            )));
1196        };
1197        let token = load_token();
1198        *self.token.lock().unwrap_or_else(|p| p.into_inner()) = token.clone();
1199        let allow = bearer && token.is_some() && !foreign(&self.base);
1200        if crate::tool_bound::armed() {
1201            crate::tool_bound::note("packsetd");
1202        }
1203        let budget = tightened(method, timeout);
1204        let _held = HeldDeadline::enter(budget);
1205        let _clock = PackClock::enter(budget);
1206        let call = Wire {
1207            method,
1208            path,
1209            query,
1210            body,
1211            token: token.as_deref(),
1212        };
1213        let answer = self.exchange(addr, &call, allow, budget)?;
1214        let answer = match answer.status {
1215            408 => return Err(degrade(&answer.body, method, true)),
1216            503 => match answer
1217                .retry_after
1218                .filter(|wait| !deadline_passed() && retry_room(*wait))
1219            {
1220                Some(wait) => {
1221                    std::thread::sleep(wait);
1222                    if deadline_passed() {
1223                        return Err(degrade(&answer.body, method, true));
1224                    }
1225                    self.exchange(addr, &call, allow, budget)?
1226                }
1227                None => return Err(degrade(&answer.body, method, deadline_passed())),
1228            },
1229            _ => answer,
1230        };
1231        if answer.status == 408 {
1232            return Err(degrade(&answer.body, method, true));
1233        }
1234        if answer.status == 503 {
1235            return Err(degrade(&answer.body, method, false));
1236        }
1237        if deadline_passed() && !(200..300).contains(&answer.status) {
1238            return Err(degrade(&answer.body, method, true));
1239        }
1240        if let Some(key) = &memo_key {
1241            if (200..300).contains(&answer.status) {
1242                keep_answer(key.clone(), &answer);
1243            }
1244        }
1245        Ok(answer)
1246    }
1247
1248    fn exchange(
1249        &self,
1250        addr: SocketAddr,
1251        call: &Wire<'_>,
1252        allow: bool,
1253        timeout: Duration,
1254    ) -> Result<Answer, Error> {
1255        // The proof and the call share this budget. A nested guard keeps
1256        // the command's earlier deadline, so a hook stays under 2s and an
1257        // interactive command under 5s across every socket of the call.
1258        // A restart does not start the budget over.
1259        let _budget = PackClock::enter(timeout);
1260        let slot = slot_for(addr);
1261        let mut live = slot.lock().unwrap_or_else(|p| p.into_inner());
1262        let tag = token_tag(call.token);
1263        if live.token_tag != tag {
1264            live.stream = None;
1265            live.proven = false;
1266            live.token_tag = tag;
1267        }
1268        let carried = if allow {
1269            match self.gate(&mut live, addr, timeout)? {
1270                Gate::Bare => None,
1271                Gate::Send => call.token,
1272                Gate::Busy(raw) => {
1273                    live.carried = false;
1274                    live.proven = false;
1275                    return Ok(Answer {
1276                        status: raw.status,
1277                        body: raw.body,
1278                        trusted: false,
1279                        retry_after: raw.retry_after,
1280                    });
1281                }
1282            }
1283        } else {
1284            None
1285        };
1286        if live.stream.is_none() {
1287            match connect(addr, timeout) {
1288                Ok(stream) => live.stream = Some(stream),
1289                Err(err) => {
1290                    live.proven = false;
1291                    live.carried = false;
1292                    return Err(Error::Io(err));
1293                }
1294            }
1295            // The probes ran on sockets the writer closed. This socket is
1296            // the one the token rides, the third step of the same proof,
1297            // not a later call.
1298            live.proven = false;
1299        }
1300        let raw = match send(
1301            &mut live,
1302            &self.origin,
1303            &Wire {
1304                token: carried,
1305                ..*call
1306            },
1307            timeout,
1308        ) {
1309            Ok(raw) => raw,
1310            Err(err) => {
1311                live.stream = None;
1312                live.proven = false;
1313                live.carried = false;
1314                return Err(Error::Io(err));
1315            }
1316        };
1317        let trusted = carried.is_some();
1318        live.carried = trusted;
1319        if trusted && live.stream.is_some() {
1320            live.proven = true;
1321        } else if live.stream.is_none() {
1322            live.proven = false;
1323        }
1324        Ok(Answer {
1325            status: raw.status,
1326            body: raw.body,
1327            trusted,
1328            retry_after: raw.retry_after,
1329        })
1330    }
1331
1332    /// Two refusals on this socket, then the call may carry the token.
1333    /// A busy writer (503) is returned as that answer; the call is not sent.
1334    fn gate(&self, live: &mut Slot, addr: SocketAddr, timeout: Duration) -> Result<Gate, Error> {
1335        if live.proven && live.stream.is_some() {
1336            return Ok(Gate::Send);
1337        }
1338        live.proven = false;
1339        let (Some(id), Some(bogus)) = (nonce(), nonce()) else {
1340            return Err(Error::Io(std::io::Error::other(
1341                "no randomness for the pack proof",
1342            )));
1343        };
1344        let workspace = self.proof_workspace();
1345        let path = format!("/v1/atoms/{}", &id[..32]);
1346        let query = [("workspace", workspace.as_str())];
1347        let mut stage = 0u8;
1348        let mut restarts = 0u8;
1349        loop {
1350            if live.stream.is_none() {
1351                live.stream = Some(connect(addr, timeout).map_err(Error::Io)?);
1352                live.proven = false;
1353            }
1354            let shown = if stage == 0 {
1355                None
1356            } else {
1357                Some(bogus.as_str())
1358            };
1359            let raw = match send(
1360                live,
1361                &self.origin,
1362                &Wire {
1363                    method: "GET",
1364                    path: &path,
1365                    query: &query,
1366                    body: None,
1367                    token: shown,
1368                },
1369                timeout,
1370            ) {
1371                Ok(raw) => raw,
1372                Err(err) => {
1373                    live.stream = None;
1374                    live.proven = false;
1375                    // A silent writer is the answer. Another socket would
1376                    // wait the budget out again, and three of those is a
1377                    // minute and a half. A closed socket may be tried
1378                    // again, inside the same deadline.
1379                    if io_is_quiet(&err) {
1380                        return Err(Error::Io(err));
1381                    }
1382                    restarts += 1;
1383                    let spent = clock_deadline().is_some_and(|deadline| Instant::now() >= deadline);
1384                    if spent || restarts > 2 {
1385                        return Err(Error::Io(err));
1386                    }
1387                    stage = 0;
1388                    continue;
1389                }
1390            };
1391            // A shed or a passed deadline is the answer. The call is not sent.
1392            if raw.status == 503 || raw.status == 408 {
1393                return Ok(Gate::Busy(raw));
1394            }
1395            if raw.status != 401 {
1396                return Ok(Gate::Bare);
1397            }
1398            if stage == 0 {
1399                stage = 1;
1400                continue;
1401            }
1402            return Ok(Gate::Send);
1403        }
1404    }
1405
1406    fn proof_workspace(&self) -> String {
1407        if let Some(workspace) = &self.workspace {
1408            return workspace.clone();
1409        }
1410        std::env::var("PACKSET_WORKSPACE")
1411            .ok()
1412            .filter(|w| !w.is_empty())
1413            .unwrap_or_else(|| "seat".to_string())
1414    }
1415
1416    /// [`Self::roundtrip`] with the token when the proof holds.
1417    fn tried(
1418        &self,
1419        method: &str,
1420        path: &str,
1421        query: &[(&str, &str)],
1422        body: Option<&[u8]>,
1423        timeout: Duration,
1424    ) -> Result<Answer, Error> {
1425        self.roundtrip(method, path, query, body, true, timeout)
1426    }
1427
1428    fn json_ok<T: DeserializeOwned>(&self, path: &str, answer: Answer) -> Result<T, Error> {
1429        if !(200..300).contains(&answer.status) {
1430            return Err(refused(
1431                &format!("{}{path}", self.base),
1432                answer.status,
1433                &answer.body,
1434            ));
1435        }
1436        serde_json::from_str(&answer.body).map_err(Error::from)
1437    }
1438
1439    /// The `line` field of `GET /stats`: `ok`, or `busy, retrying` when
1440    /// the writer is shedding. One try, so doctor does not spend the
1441    /// read budget on a writer that has no `/stats`. `None` when the
1442    /// writer does not answer, or the body has no line.
1443    #[must_use]
1444    pub fn admission_line(&self) -> Option<String> {
1445        let timeout =
1446            Duration::from_millis(300).min(clock_left().unwrap_or(Duration::from_secs(1)));
1447        let answer = self
1448            .exchange_direct("GET", "/stats", &[], None, true, timeout)
1449            .ok()?;
1450        if answer.status == 503 {
1451            return Some("busy, retrying".to_string());
1452        }
1453        if !(200..300).contains(&answer.status) {
1454            return None;
1455        }
1456        let value: Value = serde_json::from_str(&answer.body).ok()?;
1457        value
1458            .get("line")
1459            .and_then(Value::as_str)
1460            .filter(|line| !line.is_empty())
1461            .map(str::to_string)
1462    }
1463
1464    /// One exchange, with the deadline header and no retry. `/stats` uses
1465    /// this so a busy writer is one answer, not a wait.
1466    fn exchange_direct(
1467        &self,
1468        method: &str,
1469        path: &str,
1470        query: &[(&str, &str)],
1471        body: Option<&[u8]>,
1472        bearer: bool,
1473        timeout: Duration,
1474    ) -> Result<Answer, Error> {
1475        let Some(addr) = self.addr else {
1476            return Err(Error::Bad(format!(
1477                "{} is not a socket this seat can pin",
1478                self.base
1479            )));
1480        };
1481        let token = load_token();
1482        let allow = bearer && token.is_some() && !foreign(&self.base);
1483        if crate::tool_bound::armed() {
1484            crate::tool_bound::note("packsetd");
1485        }
1486        let budget = admission_retry_budget(method, timeout);
1487        let _held = HeldDeadline::enter(budget);
1488        self.exchange(
1489            addr,
1490            &Wire {
1491                method,
1492                path,
1493                query,
1494                body,
1495                token: token.as_deref(),
1496            },
1497            allow,
1498            timeout,
1499        )
1500    }
1501
1502    pub fn health(&self) -> Result<String, Error> {
1503        let answer = self.tried("GET", "/health", &[], None, HEALTH_BUDGET)?;
1504        if !(200..300).contains(&answer.status) {
1505            return Err(refused(
1506                &format!("{}/health", self.base),
1507                answer.status,
1508                &answer.body,
1509            ));
1510        }
1511        Ok(answer.body)
1512    }
1513
1514    pub fn status(&self, workspace: Option<&str>) -> Result<Value, Error> {
1515        let query: Vec<(&str, &str)> = workspace
1516            .map(|workspace| vec![("workspace", workspace)])
1517            .unwrap_or_default();
1518        let answer = self.tried("GET", "/v1/status", &query, None, STATUS_BUDGET)?;
1519        self.json_ok("/v1/status", answer)
1520    }
1521
1522    pub fn post_atom(&self, atom: &Value) -> Result<Value, Error> {
1523        let bytes = serde_json::to_vec(atom)?;
1524        let answer = self.tried(
1525            "POST",
1526            "/v1/atoms",
1527            &[],
1528            Some(&bytes),
1529            super::REMEMBER_BUDGET,
1530        )?;
1531        self.json_ok("/v1/atoms", answer)
1532    }
1533
1534    pub fn delete_atom(
1535        &self,
1536        workspace: &str,
1537        id: &str,
1538        why: Option<&str>,
1539    ) -> Result<Value, Error> {
1540        let mut body = serde_json::json!({"workspace": workspace, "id": id});
1541        if let Some(accession) = why {
1542            body["why"] = Value::String(accession.to_string());
1543        }
1544        let bytes = serde_json::to_vec(&body)?;
1545        let answer = self.tried(
1546            "POST",
1547            "/v1/atoms/delete",
1548            &[],
1549            Some(&bytes),
1550            super::REMEMBER_BUDGET,
1551        )?;
1552        if answer.status == 404 {
1553            return Err(Error::Bad(format!("no atom {id}")));
1554        }
1555        self.json_ok("/v1/atoms/delete", answer)
1556    }
1557
1558    pub fn atoms_of_kind(&self, workspace: &str, kind: &str) -> Result<Vec<Value>, Error> {
1559        let answer = self.tried(
1560            "GET",
1561            "/v1/atoms",
1562            &[("workspace", workspace), ("kind", kind)],
1563            None,
1564            super::SEARCH_PACK_BUDGET,
1565        )?;
1566        let body: Value = self.json_ok("/v1/atoms", answer)?;
1567        Ok(body
1568            .get("atoms")
1569            .and_then(Value::as_array)
1570            .cloned()
1571            .unwrap_or_default())
1572    }
1573
1574    pub fn atoms_in_set(&self, workspace: &str, set: &str) -> Result<Vec<Value>, Error> {
1575        let answer = self.tried(
1576            "GET",
1577            "/v1/pack",
1578            &[("workspace", workspace), ("set", set)],
1579            None,
1580            super::SEARCH_PACK_BUDGET,
1581        )?;
1582        let body: Value = self.json_ok("/v1/pack", answer)?;
1583        Ok(body
1584            .get("atoms")
1585            .and_then(Value::as_array)
1586            .cloned()
1587            .unwrap_or_default())
1588    }
1589
1590    pub fn sweep(&self, workspace: &str) -> Result<Value, Error> {
1591        let bytes = serde_json::to_vec(&serde_json::json!({"workspace": workspace}))?;
1592        let answer = self.tried(
1593            "POST",
1594            "/v1/sweep",
1595            &[],
1596            Some(&bytes),
1597            super::REMEMBER_BUDGET,
1598        )?;
1599        self.json_ok("/v1/sweep", answer)
1600    }
1601
1602    pub fn grade(&self, workspace: &str, id: &str, recalled: bool) -> Result<Value, Error> {
1603        let bytes = serde_json::to_vec(&serde_json::json!({
1604            "workspace": workspace,
1605            "id": id,
1606            "recalled": recalled,
1607        }))?;
1608        let answer = self.tried(
1609            "POST",
1610            "/v1/grade",
1611            &[],
1612            Some(&bytes),
1613            super::REMEMBER_BUDGET,
1614        )?;
1615        self.json_ok("/v1/grade", answer)
1616    }
1617
1618    /// Merge `fields` into one current atom, under its id.
1619    pub fn update_atom(&self, workspace: &str, id: &str, fields: &Value) -> Result<Value, Error> {
1620        let bytes = serde_json::to_vec(&serde_json::json!({
1621            "workspace": workspace,
1622            "id": id,
1623            "fields": fields,
1624        }))?;
1625        let answer = self.tried(
1626            "POST",
1627            "/v1/atoms/update",
1628            &[],
1629            Some(&bytes),
1630            super::REMEMBER_BUDGET,
1631        )?;
1632        self.json_ok("/v1/atoms/update", answer)
1633    }
1634
1635    pub fn activate_as(
1636        &self,
1637        workspace: &str,
1638        q: &str,
1639        limit: u32,
1640        fire: bool,
1641        lens: Option<&str>,
1642    ) -> Result<Value, Error> {
1643        let limit = limit.to_string();
1644        let fire = if fire { "1" } else { "0" };
1645        let mut query = vec![
1646            ("workspace", workspace),
1647            ("q", q),
1648            ("limit", limit.as_str()),
1649            ("fire", fire),
1650        ];
1651        if let Some(name) = lens.filter(|n| !n.trim().is_empty()) {
1652            query.push(("as", name));
1653        }
1654        let answer = self.tried(
1655            "GET",
1656            "/v1/activate",
1657            &query,
1658            None,
1659            super::SEARCH_PACK_BUDGET,
1660        )?;
1661        self.json_ok("/v1/activate", answer)
1662    }
1663
1664    pub fn hubs(&self, workspace: &str, limit: usize) -> Result<Value, Error> {
1665        let limit = limit.to_string();
1666        let answer = self.tried(
1667            "GET",
1668            "/v1/hubs",
1669            &[("workspace", workspace), ("limit", limit.as_str())],
1670            None,
1671            super::SEARCH_PACK_BUDGET,
1672        )?;
1673        self.json_ok("/v1/hubs", answer)
1674    }
1675
1676    pub fn consolidate(&self, workspace: &str, apply: bool) -> Result<Value, Error> {
1677        let bytes =
1678            serde_json::to_vec(&serde_json::json!({"workspace": workspace, "apply": apply}))?;
1679        let answer = self.tried(
1680            "POST",
1681            "/v1/consolidate",
1682            &[],
1683            Some(&bytes),
1684            super::REMEMBER_BUDGET,
1685        )?;
1686        self.json_ok("/v1/consolidate", answer)
1687    }
1688
1689    pub fn search_opts(
1690        &self,
1691        workspace: &str,
1692        q: &str,
1693        limit: u32,
1694        as_of: Option<&str>,
1695        rerank: bool,
1696    ) -> Result<Vec<Hit>, Error> {
1697        Ok(self.search_fetched(workspace, q, limit, as_of, rerank)?.0)
1698    }
1699
1700    pub(crate) fn search_fetched(
1701        &self,
1702        workspace: &str,
1703        q: &str,
1704        limit: u32,
1705        as_of: Option<&str>,
1706        rerank: bool,
1707    ) -> Result<(Vec<Hit>, bool), Error> {
1708        let limit_s = limit.to_string();
1709        let mut query = vec![
1710            ("workspace", workspace),
1711            ("q", q),
1712            ("limit", limit_s.as_str()),
1713        ];
1714        if let Some(at) = as_of {
1715            query.push(("as_of", at));
1716        }
1717        if rerank {
1718            query.push(("rerank", "1"));
1719        }
1720        // A rerank outside a clock, a hook, or an MCP call may load the
1721        // model, so it gets a minute. A sitting clock and an MCP read stay
1722        // near one second, unless the seat raised the tool bound and the
1723        // pack timeout. Any other search is the short search budget.
1724        let timeout = if rerank
1725            && !clock_armed()
1726            && std::env::var_os("LJOS_IN_HOOK").is_none()
1727            && !crate::tool_bound::armed()
1728        {
1729            Duration::from_secs(60)
1730        } else if crate::tool_bound::armed() {
1731            mcp_read_cap()
1732        } else {
1733            super::SEARCH_PACK_BUDGET
1734        };
1735        let answer = self.tried("GET", "/v1/search", &query, None, timeout)?;
1736        let trusted = answer.trusted;
1737        let body: Value = self.json_ok("/v1/search", answer)?;
1738        let hits = body.get("hits").cloned().unwrap_or(Value::Array(vec![]));
1739        Ok((serde_json::from_value(hits)?, trusted))
1740    }
1741
1742    pub fn fire(&self, workspace: &str, ids: &[String]) -> Result<Value, Error> {
1743        let bytes = serde_json::to_vec(&serde_json::json!({
1744            "workspace": workspace,
1745            "ids": ids,
1746            "as": "",
1747        }))?;
1748        let answer = self.tried(
1749            "POST",
1750            "/v1/fire",
1751            &[],
1752            Some(&bytes),
1753            super::REMEMBER_BUDGET,
1754        )?;
1755        self.json_ok("/v1/fire", answer)
1756    }
1757}
1758
1759fn refused(url: &str, code: u16, body: &str) -> Error {
1760    let reason = serde_json::from_str::<Value>(body)
1761        .ok()
1762        .and_then(|v| v.get("error").and_then(|r| r.as_str()).map(str::to_string))
1763        .unwrap_or_else(|| body.to_string());
1764    let reason = reason.trim();
1765    if code == 401 {
1766        return Error::Bad(format!(
1767            "{url}: 401: {reason}; this seat's token does not match the writer's, \
1768             so it is another user's writer or PACKSET_HOME names another pack"
1769        ));
1770    }
1771    if reason.is_empty() {
1772        Error::Bad(format!("{url}: status code {code}"))
1773    } else {
1774        Error::Bad(format!("{url}: {code}: {reason}"))
1775    }
1776}
1777
1778fn send(live: &mut Slot, origin: &Url, call: &Wire<'_>, timeout: Duration) -> std::io::Result<Raw> {
1779    if pack_quiet() {
1780        return Err(quiet_io());
1781    }
1782    if side_budget_spent() {
1783        return Err(budget_spent());
1784    }
1785    let started = Instant::now();
1786    let result = send_once(live, origin, call, timeout);
1787    // An answer is not charged. A read that ends without one is a quiet
1788    // socket and spends the budget, so the next read does not open another.
1789    // Outside a hook the side budget is not armed. The quiet flag is that
1790    // same stop for a command, which asks the pack more than once.
1791    if let Err(err) = &result {
1792        if io_is_quiet(err) {
1793            mark_pack_quiet();
1794        }
1795        super::charge_side_wait(started.elapsed());
1796    }
1797    result
1798}
1799
1800fn send_once(
1801    live: &mut Slot,
1802    origin: &Url,
1803    call: &Wire<'_>,
1804    timeout: Duration,
1805) -> std::io::Result<Raw> {
1806    let stream = live
1807        .stream
1808        .as_mut()
1809        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotConnected, "no pack socket"))?;
1810    let wait = bounded_wait(timeout, false)?;
1811    let deadline = Instant::now() + wait;
1812    stream.set_read_timeout(Some(wait))?;
1813    stream.set_write_timeout(Some(wait))?;
1814    let bytes = render(
1815        call.method,
1816        &host_header(origin),
1817        &target(call.path, call.query),
1818        call.token,
1819        call.body,
1820    );
1821    if let Err(err) = write_all(stream, &bytes) {
1822        live.stream = None;
1823        live.proven = false;
1824        return Err(err);
1825    }
1826    match read_response(stream, deadline) {
1827        Ok(raw) => {
1828            if !raw.keep {
1829                live.stream = None;
1830                live.proven = false;
1831            }
1832            Ok(raw)
1833        }
1834        Err(err) => {
1835            live.stream = None;
1836            live.proven = false;
1837            Err(err)
1838        }
1839    }
1840}
1841
1842#[cfg(test)]
1843mod tests {
1844    use super::{foreign, OwnerOnce, TABLE_READS};
1845
1846    fn reads() -> usize {
1847        TABLE_READS.with(std::cell::Cell::get)
1848    }
1849
1850    /// The requests of one search share one read of the socket tables for
1851    /// each url. A guard inside it keeps that read, and a call after the
1852    /// search reads them again.
1853    #[test]
1854    fn a_search_reads_the_port_owner_once_for_each_url() {
1855        let _g = crate::tests::env_guard();
1856        // SAFETY: under the lock every environment-reading test takes.
1857        unsafe { std::env::remove_var("LJOS_IN_HOOK") };
1858        let (url, other) = ("http://127.0.0.1:9", "http://127.0.0.1:10");
1859        let start = reads();
1860        foreign(url);
1861        foreign(url);
1862        assert_eq!(reads() - start, 2, "outside a search each call reads");
1863        {
1864            let _search = OwnerOnce::enter();
1865            let first = foreign(url);
1866            {
1867                let _inner = OwnerOnce::enter();
1868                assert_eq!(foreign(url), first);
1869            }
1870            assert_eq!(foreign(url), first);
1871            foreign(other);
1872            foreign(other);
1873            assert_eq!(reads() - start, 4, "one search reads once a url");
1874        }
1875        foreign(url);
1876        assert_eq!(reads() - start, 5, "a call after the search reads again");
1877    }
1878
1879    fn quiet_pack() -> tempfile::TempDir {
1880        let home = tempfile::tempdir().unwrap();
1881        unsafe {
1882            std::env::remove_var("PACKSET_TOKEN");
1883            std::env::remove_var("PACKSET_TOKEN_FILE");
1884            std::env::remove_var("PACKSET_TIMEOUT_MS");
1885            std::env::remove_var("LJOS_IN_HOOK");
1886            std::env::set_var("PACKSET_HOME", home.path());
1887        }
1888        home
1889    }
1890
1891    /// `on` sees the request text and how many requests came before this one.
1892    /// It answers with a status, a body, and `Retry-After` seconds when set.
1893    fn serve(
1894        on: impl Fn(usize, &str) -> (u16, String, Option<u64>) + Send + 'static,
1895    ) -> (
1896        super::PacksetClient,
1897        std::sync::Arc<std::sync::Mutex<Vec<String>>>,
1898    ) {
1899        use std::io::{Read, Write};
1900        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1901        let addr = listener.local_addr().unwrap();
1902        let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1903        let log = std::sync::Arc::clone(&seen);
1904        std::thread::spawn(move || {
1905            for stream in listener.incoming() {
1906                let Ok(mut sock) = stream else { break };
1907                let _ = sock.set_read_timeout(Some(std::time::Duration::from_secs(2)));
1908                let mut buf = Vec::new();
1909                let mut tmp = [0u8; 2048];
1910                loop {
1911                    match sock.read(&mut tmp) {
1912                        Ok(0) | Err(_) => break,
1913                        Ok(n) => {
1914                            buf.extend_from_slice(&tmp[..n]);
1915                            if buf.windows(4).any(|w| w == b"\r\n\r\n") {
1916                                break;
1917                            }
1918                        }
1919                    }
1920                }
1921                let req = String::from_utf8_lossy(&buf).into_owned();
1922                let n = {
1923                    let mut log = log.lock().unwrap();
1924                    log.push(req.clone());
1925                    log.len() - 1
1926                };
1927                let (code, body, retry) = on(n, &req);
1928                let reason = match code {
1929                    200 => "OK",
1930                    408 => "Request Timeout",
1931                    503 => "Service Unavailable",
1932                    _ => "Error",
1933                };
1934                let retry = retry
1935                    .map(|secs| format!("Retry-After: {secs}\r\n"))
1936                    .unwrap_or_default();
1937                let resp = format!(
1938                    "HTTP/1.1 {code} {reason}\r\n{retry}Content-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
1939                    body.len()
1940                );
1941                let _ = sock.write_all(resp.as_bytes());
1942            }
1943        });
1944        (super::PacksetClient::new(format!("http://{addr}")), seen)
1945    }
1946
1947    fn deadline_of(req: &str) -> u64 {
1948        req.lines()
1949            .find_map(|line| {
1950                let (name, value) = line.split_once(':')?;
1951                name.eq_ignore_ascii_case("x-packset-deadline")
1952                    .then(|| value.trim().parse().ok())
1953                    .flatten()
1954            })
1955            .unwrap_or(0)
1956    }
1957
1958    fn ahead_ms(req: &str) -> u64 {
1959        let now = super::unix_ms(std::time::SystemTime::now());
1960        deadline_of(req).saturating_sub(now)
1961    }
1962
1963    #[test]
1964    fn an_mcp_read_deadline_is_about_a_second() {
1965        let _g = crate::tests::env_guard();
1966        let _home = quiet_pack();
1967        let _bound = crate::tool_bound::arm(std::time::Duration::from_secs(30));
1968        let (client, seen) = serve(|_, _| (200, "ok".into(), None));
1969        client.health().unwrap();
1970        let ahead = ahead_ms(&seen.lock().unwrap()[0]);
1971        assert!(
1972            (200..1_500).contains(&ahead),
1973            "mcp read deadline is {ahead} ms ahead"
1974        );
1975    }
1976
1977    /// A seat that raises the tool bound and the pack timeout is waiting
1978    /// on one long read. The deadline is that pack timeout, not a second.
1979    #[test]
1980    fn a_raised_mcp_bound_keeps_the_pack_timeout_on_a_read() {
1981        let _g = crate::tests::env_guard();
1982        let _home = quiet_pack();
1983        unsafe {
1984            std::env::set_var("PACKSET_TIMEOUT_MS", "45000");
1985            std::env::set_var("LJOS_MCP_TIMEOUT", "60");
1986        }
1987        let _bound = crate::tool_bound::arm(std::time::Duration::from_secs(60));
1988        let (client, seen) = serve(|_, _| (200, r#"{"hits":[]}"#.into(), None));
1989        client
1990            .search_fetched("seat", "review set", 1, None, false)
1991            .unwrap();
1992        let ahead = ahead_ms(&seen.lock().unwrap()[0]);
1993        assert!(
1994            (40_000..50_000).contains(&ahead),
1995            "raised read deadline is {ahead} ms ahead"
1996        );
1997    }
1998
1999    #[test]
2000    fn a_one_second_clock_caps_the_read_deadline() {
2001        let _g = crate::tests::env_guard();
2002        let _home = quiet_pack();
2003        let _clock = super::PackClock::enter(std::time::Duration::from_secs(1));
2004        let (client, seen) = serve(|_, _| (200, "ok".into(), None));
2005        client.health().unwrap();
2006        let ahead = ahead_ms(&seen.lock().unwrap()[0]);
2007        assert!(
2008            (200..1_500).contains(&ahead),
2009            "clocked read deadline is {ahead} ms ahead"
2010        );
2011    }
2012
2013    #[test]
2014    fn a_200_carries_the_call_deadline() {
2015        let _g = crate::tests::env_guard();
2016        let _home = quiet_pack();
2017        let (client, seen) = serve(|_, req| {
2018            assert!(deadline_of(req) > 0, "{req}");
2019            (200, "ok".into(), None)
2020        });
2021        let body = client.health().unwrap();
2022        assert_eq!(body, "ok");
2023        assert_eq!(seen.lock().unwrap().len(), 1);
2024        let now = super::unix_ms(std::time::SystemTime::now());
2025        let stamp = deadline_of(&seen.lock().unwrap()[0]);
2026        assert!(stamp > now, "deadline {stamp} is not after {now}");
2027        assert!(
2028            stamp < now + 3_000,
2029            "health deadline {stamp} is not about two seconds past {now}"
2030        );
2031    }
2032
2033    #[test]
2034    fn a_503_waits_retry_after_once_and_then_reads() {
2035        let _g = crate::tests::env_guard();
2036        let _home = quiet_pack();
2037        let (client, seen) = serve(|n, req| {
2038            assert!(deadline_of(req) > 0, "{req}");
2039            if n == 0 {
2040                (
2041                    503,
2042                    r#"{"error":"packsetd is busy: the read queue is full"}"#.into(),
2043                    Some(1),
2044                )
2045            } else {
2046                (200, "ok".into(), None)
2047            }
2048        });
2049        let started = std::time::Instant::now();
2050        let body = client.health().unwrap();
2051        let took = started.elapsed();
2052        assert_eq!(body, "ok");
2053        let got = seen.lock().unwrap().clone();
2054        assert_eq!(got.len(), 2);
2055        assert_eq!(deadline_of(&got[0]), deadline_of(&got[1]));
2056        assert!(
2057            took >= std::time::Duration::from_millis(900),
2058            "Retry-After was not waited: {took:?}"
2059        );
2060        assert!(took < std::time::Duration::from_secs(3), "{took:?}");
2061    }
2062
2063    #[test]
2064    fn a_second_503_is_the_error_line_and_stops() {
2065        let _g = crate::tests::env_guard();
2066        let _home = quiet_pack();
2067        let (client, seen) = serve(|_, _| {
2068            (
2069                503,
2070                r#"{"error":"packsetd is busy: the read queue is full"}"#.into(),
2071                Some(0),
2072            )
2073        });
2074        let started = std::time::Instant::now();
2075        let err = client.health().unwrap_err();
2076        let took = started.elapsed();
2077        assert_eq!(
2078            err.to_string(),
2079            "packsetd is busy: the read queue is full",
2080            "{err}"
2081        );
2082        let got = seen.lock().unwrap().clone();
2083        assert_eq!(got.len(), 2, "a 503 is resent once");
2084        assert_eq!(deadline_of(&got[0]), deadline_of(&got[1]));
2085        assert!(took < std::time::Duration::from_millis(500), "{took:?}");
2086    }
2087
2088    #[test]
2089    fn an_expired_deadline_is_the_error_line_and_is_not_retried() {
2090        let _g = crate::tests::env_guard();
2091        let _home = quiet_pack();
2092        let (client, seen) = serve(|_, req| {
2093            assert!(deadline_of(req) > 0, "{req}");
2094            (
2095                408,
2096                r#"{"error":"the deadline passed before this request ran"}"#.into(),
2097                None,
2098            )
2099        });
2100        let err = client.health().unwrap_err();
2101        assert_eq!(
2102            err.to_string(),
2103            "the deadline passed before this request ran",
2104            "{err}"
2105        );
2106        assert_eq!(seen.lock().unwrap().len(), 1);
2107    }
2108
2109    #[test]
2110    fn a_retry_after_past_the_deadline_does_not_wait() {
2111        let _g = crate::tests::env_guard();
2112        let _home = quiet_pack();
2113        let (client, seen) = serve(|_, _| (503, r#"{"error":"busy"}"#.into(), Some(30)));
2114        let started = std::time::Instant::now();
2115        let err = client.health().unwrap_err();
2116        let took = started.elapsed();
2117        assert_eq!(err.to_string(), "busy", "{err}");
2118        assert_eq!(seen.lock().unwrap().len(), 1, "the long wait was taken");
2119        assert!(took < std::time::Duration::from_millis(500), "{took:?}");
2120    }
2121
2122    #[test]
2123    fn stats_line_is_the_admission_line() {
2124        let _g = crate::tests::env_guard();
2125        let _home = quiet_pack();
2126        let (client, seen) = serve(|_, req| {
2127            let line = req.lines().next().unwrap_or("");
2128            if line.contains("GET /stats") {
2129                (200, r#"{"ok":true,"line":"busy, retrying"}"#.into(), None)
2130            } else {
2131                (200, r#"{"line":"ok"}"#.into(), None)
2132            }
2133        });
2134        assert_eq!(client.admission_line().as_deref(), Some("busy, retrying"));
2135        assert_eq!(seen.lock().unwrap().len(), 1);
2136        let (client, seen) = serve(|_, _| (503, r#"{"error":"busy"}"#.into(), Some(1)));
2137        assert_eq!(client.admission_line().as_deref(), Some("busy, retrying"));
2138        assert_eq!(
2139            seen.lock().unwrap().len(),
2140            1,
2141            "stats does not wait out Retry-After"
2142        );
2143    }
2144}