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