1use 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
23pub 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#[derive(Debug, Clone)]
47pub(crate) struct Answer {
48 pub status: u16,
49 pub body: String,
50 pub trusted: bool,
51 pub retry_after: Option<Duration>,
53}
54
55type 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
96pub(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 proven: bool,
108 token_tag: u64,
109 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 static CLOCK: std::cell::RefCell<Option<Clock>> = const { std::cell::RefCell::new(None) };
222}
223
224pub struct PackClock {
229 _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
276pub(crate) fn clock_left() -> Option<Duration> {
278 clock_deadline().map(|deadline| deadline.saturating_duration_since(Instant::now()))
279}
280
281pub(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
298fn 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 static SEARCH_OWNERS: std::cell::RefCell<Option<Vec<(String, bool)>>> =
344 const { std::cell::RefCell::new(None) };
345}
346
347pub(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 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
460const BUSY_GROWTH: u32 = 3;
462const BUSY_WAIT_CAP: Duration = Duration::from_secs(5);
463
464thread_local! {
465 static COMMAND_DEADLINE: std::cell::RefCell<Option<SystemTime>> = const { std::cell::RefCell::new(None) };
469 static HELD_DEADLINE: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
472}
473
474pub struct CommandDeadline {
477 prev: Option<SystemTime>,
478}
479
480impl CommandDeadline {
481 #[must_use]
482 pub fn enter(budget: Duration) -> Self {
483 let prev = COMMAND_DEADLINE.with(|cell| *cell.borrow());
484 let proposed = SystemTime::now() + budget;
485 let next = match prev {
486 Some(existing) if existing <= proposed => existing,
487 _ => proposed,
488 };
489 COMMAND_DEADLINE.with(|cell| *cell.borrow_mut() = Some(next));
490 Self { prev }
491 }
492}
493
494impl Drop for CommandDeadline {
495 fn drop(&mut self) {
496 let prev = self.prev.take();
497 COMMAND_DEADLINE.with(|cell| *cell.borrow_mut() = prev);
498 }
499}
500
501struct HeldDeadline {
502 prev: Option<String>,
503 active: bool,
504}
505
506impl HeldDeadline {
507 fn enter(budget: Duration) -> Self {
508 let prev = HELD_DEADLINE.with(|cell| cell.borrow().clone());
509 if prev.is_some() {
510 return Self {
511 prev,
512 active: false,
513 };
514 }
515 let stamp = deadline_stamp(budget);
516 HELD_DEADLINE.with(|cell| *cell.borrow_mut() = Some(stamp));
517 Self { prev, active: true }
518 }
519}
520
521impl Drop for HeldDeadline {
522 fn drop(&mut self) {
523 if self.active {
524 let prev = self.prev.clone();
525 HELD_DEADLINE.with(|cell| *cell.borrow_mut() = prev);
526 }
527 }
528}
529
530fn is_write(method: &str) -> bool {
531 !matches!(method, "GET" | "HEAD")
532}
533
534fn command_remaining() -> Option<Duration> {
535 let when = COMMAND_DEADLINE.with(|cell| *cell.borrow())?;
536 Some(
537 when.duration_since(SystemTime::now())
538 .unwrap_or(Duration::ZERO),
539 )
540}
541
542pub(crate) fn admission_retry_budget(method: &str, per_try: Duration) -> Duration {
546 let fallback = if is_write(method) {
547 super::REMEMBER_BUDGET
548 } else {
549 super::SEARCH_PACK_BUDGET
550 };
551 let room = match (command_remaining(), clock_left()) {
552 (Some(cmd), Some(clock)) => cmd.min(clock),
553 (Some(cmd), None) => cmd,
554 (None, Some(clock)) => clock,
555 (None, None) => fallback,
556 };
557 let room = if is_write(method) {
558 room
559 } else {
560 room.min(super::SEARCH_PACK_BUDGET)
561 };
562 per_try.min(room).max(Duration::from_millis(1))
563}
564
565fn deadline_stamp(budget: Duration) -> String {
566 let proposed = SystemTime::now() + budget;
567 let when = match COMMAND_DEADLINE.with(|cell| *cell.borrow()) {
568 Some(command) if command < proposed => command,
569 _ => proposed,
570 };
571 unix_ms(when).to_string()
572}
573
574fn unix_ms(when: SystemTime) -> u64 {
575 when.duration_since(std::time::UNIX_EPOCH)
576 .map(|elapsed| u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX))
577 .unwrap_or(0)
578}
579
580fn current_deadline(method: &str) -> String {
581 HELD_DEADLINE
582 .with(|cell| cell.borrow().clone())
583 .unwrap_or_else(|| deadline_stamp(admission_retry_budget(method, budget())))
584}
585
586struct Pause {
587 sleep: Duration,
588 charge: Duration,
589}
590
591fn planned_wait(backoff: Duration, retry_after: Option<Duration>) -> Duration {
592 match retry_after {
593 Some(after) if after > backoff => after,
594 _ => backoff,
595 }
596}
597
598fn jittered(wait: Duration, salt: u64) -> Duration {
601 let ms = u64::try_from(wait.as_millis()).unwrap_or(u64::MAX).max(1);
602 let half = ms / 2;
603 let extra = if half == 0 { 0 } else { salt % (half + 1) };
604 Duration::from_millis(half + extra)
605}
606
607fn pause_for(
611 backoff: Duration,
612 retry_after: Option<Duration>,
613 waited: Duration,
614 budget: Duration,
615 salt: u64,
616) -> Option<Pause> {
617 let charge = planned_wait(backoff, retry_after);
618 if waited.saturating_add(charge) >= budget {
619 return None;
620 }
621 let room = budget.saturating_sub(waited).max(Duration::from_millis(1));
622 Some(Pause {
623 sleep: jittered(charge, salt)
624 .min(room)
625 .max(Duration::from_millis(1)),
626 charge,
627 })
628}
629
630fn retry_after_of(header: Option<&str>) -> Option<Duration> {
631 let raw = header?.trim();
632 if raw.is_empty() {
633 return None;
634 }
635 raw.parse::<u64>().ok().map(Duration::from_secs)
636}
637
638fn jitter_salt(asked: u32) -> u64 {
639 let nanos = SystemTime::now()
640 .duration_since(std::time::UNIX_EPOCH)
641 .map(|elapsed| u64::from(elapsed.subsec_nanos()))
642 .unwrap_or(0);
643 nanos ^ u64::from(asked)
644}
645
646fn shed_error(method: &str) -> Error {
647 let line = if is_write(method) {
648 super::PACK_SHED_LINE
649 } else {
650 super::PACK_READ_BUSY_LINE
651 };
652 Error::Unavailable(line.to_string())
653}
654
655fn deadline_error() -> Error {
656 Error::Unavailable(super::PACK_DEADLINE_LINE.to_string())
657}
658
659#[cfg(test)]
660pub(crate) fn busy_attempts(budget: Duration) -> u32 {
661 let mut waited = Duration::ZERO;
662 let mut wait = super::BUSY_WAIT;
663 let mut attempts = 1u32;
664 while pause_for(wait, None, waited, budget, 0).is_some() {
665 waited = waited.saturating_add(wait);
666 wait = (wait * BUSY_GROWTH).min(BUSY_WAIT_CAP);
667 attempts = attempts.saturating_add(1);
668 }
669 attempts
670}
671
672static PACK_QUIET: AtomicBool = AtomicBool::new(false);
679static QUIET_HELD: AtomicUsize = AtomicUsize::new(0);
680
681pub struct QuietPack;
683
684impl QuietPack {
685 #[must_use]
688 pub fn enter() -> Self {
689 if QUIET_HELD.fetch_add(1, Ordering::AcqRel) == 0 {
690 PACK_QUIET.store(false, Ordering::Relaxed);
691 }
692 Self
693 }
694}
695
696impl Drop for QuietPack {
697 fn drop(&mut self) {
698 if QUIET_HELD.fetch_sub(1, Ordering::AcqRel) == 1 {
699 PACK_QUIET.store(false, Ordering::Relaxed);
700 }
701 }
702}
703
704fn holding_quiet() -> bool {
705 QUIET_HELD.load(Ordering::Relaxed) > 0
706}
707
708fn pack_quiet() -> bool {
709 holding_quiet() && PACK_QUIET.load(Ordering::Relaxed)
710}
711
712fn mark_pack_quiet() {
713 if holding_quiet() {
714 PACK_QUIET.store(true, Ordering::Relaxed);
715 }
716}
717
718fn tool_bound_spent() -> bool {
722 crate::tool_bound::remaining().is_some_and(|left| left <= Duration::from_millis(50))
723}
724
725fn pack_io(err: std::io::Error) -> std::io::Error {
726 let quiet = io_is_quiet(&err) || err.to_string().contains("the pack did not answer");
727 if quiet && tool_bound_spent() {
728 crate::tool_bound::note("packsetd");
729 crate::tool_bound::mark_failed("packsetd timed out");
730 return std::io::Error::new(std::io::ErrorKind::TimedOut, "packsetd timed out");
731 }
732 err
733}
734
735fn quiet_io() -> std::io::Error {
736 pack_io(std::io::Error::new(
737 std::io::ErrorKind::TimedOut,
738 "the pack did not answer",
739 ))
740}
741
742fn io_is_quiet(err: &std::io::Error) -> bool {
745 matches!(
746 err.kind(),
747 std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
748 )
749}
750
751fn connect(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
752 if pack_quiet() {
753 return Err(quiet_io());
754 }
755 if side_budget_spent() {
756 return Err(budget_spent());
757 }
758 let started = Instant::now();
759 let result = connect_once(addr, timeout);
760 if let Err(err) = &result {
765 if io_is_quiet(err) {
766 mark_pack_quiet();
767 }
768 super::charge_side_wait(started.elapsed());
769 }
770 result
771}
772
773fn side_budget_spent() -> bool {
777 super::side_remaining_ms() == Some(0)
778}
779
780fn budget_spent() -> std::io::Error {
781 std::io::Error::new(
782 std::io::ErrorKind::TimedOut,
783 "the side event's pack budget is spent",
784 )
785}
786
787fn connect_once(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
788 let wait = bounded_wait(timeout, true)?;
789 let stream = TcpStream::connect_timeout(&addr, wait)?;
790 let io_wait = bounded_wait(timeout, false)?;
791 stream.set_read_timeout(Some(io_wait))?;
792 stream.set_write_timeout(Some(io_wait))?;
793 let _ = stream.set_nodelay(true);
794 Ok(stream)
795}
796
797fn bounded_wait(requested: Duration, connect: bool) -> std::io::Result<Duration> {
801 let mut wait = capped_wait(requested);
802 if let Some(deadline) = clock_deadline() {
803 let left = deadline.saturating_duration_since(Instant::now());
804 if left.is_zero() {
805 return Err(pack_io(std::io::Error::new(
806 std::io::ErrorKind::TimedOut,
807 "the pack did not answer",
808 )));
809 }
810 wait = wait.min(left);
811 if connect {
812 wait = wait.min(super::pack_connect_budget());
813 }
814 if wait.is_zero() {
815 return Err(pack_io(std::io::Error::new(
816 std::io::ErrorKind::TimedOut,
817 "the pack did not answer",
818 )));
819 }
820 }
821 Ok(wait)
822}
823
824fn capped_wait(timeout: Duration) -> Duration {
827 let wait = super::cap_pack_wait(timeout);
828 if wait.is_zero() {
829 Duration::from_millis(1)
830 } else {
831 wait
832 }
833}
834
835fn write_all(stream: &mut TcpStream, bytes: &[u8]) -> std::io::Result<()> {
836 let mut sent = 0;
837 while sent < bytes.len() {
838 match stream.write(&bytes[sent..]) {
839 Ok(0) => {
840 return Err(std::io::Error::new(
841 std::io::ErrorKind::WriteZero,
842 "the pack closed the socket",
843 ))
844 }
845 Ok(n) => sent += n,
846 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
847 Err(err) => return Err(pack_io(err)),
848 }
849 }
850 stream.flush()
851}
852
853fn read_response(stream: &mut TcpStream, deadline: Instant) -> std::io::Result<Raw> {
854 let mut buf = Vec::new();
855 let mut tmp = [0u8; 4096];
856 let head_end = loop {
857 if buf.len() > 64 * 1024 {
858 return Err(std::io::Error::new(
859 std::io::ErrorKind::InvalidData,
860 "the pack's headers are too long",
861 ));
862 }
863 arm_read(stream, deadline)?;
864 let n = read_some(stream, &mut tmp)?;
865 buf.extend_from_slice(&tmp[..n]);
866 if let Some(at) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
867 break at;
868 }
869 };
870 let header = String::from_utf8_lossy(&buf[..head_end]).into_owned();
871 let status = header
872 .lines()
873 .next()
874 .and_then(|line| line.split_whitespace().nth(1))
875 .and_then(|code| code.parse().ok())
876 .ok_or_else(|| {
877 std::io::Error::new(
878 std::io::ErrorKind::InvalidData,
879 "the pack's status line is not HTTP",
880 )
881 })?;
882 let mut length = None;
883 let mut close = header.starts_with("HTTP/1.0");
884 let mut retry_after = None;
885 for line in header.lines().skip(1) {
886 let Some((name, value)) = line.split_once(':') else {
887 continue;
888 };
889 if name.eq_ignore_ascii_case("content-length") {
890 length = value.trim().parse().ok();
891 } else if name.eq_ignore_ascii_case("retry-after") {
892 retry_after = retry_after_of(Some(value));
893 } else if name.eq_ignore_ascii_case("connection")
894 && value.trim().eq_ignore_ascii_case("close")
895 {
896 close = true;
897 }
898 }
899 let mut body = buf.split_off(head_end + 4);
900 if let Some(length) = length {
901 while body.len() < length {
902 arm_read(stream, deadline)?;
903 let n = read_some(stream, &mut tmp)?;
904 body.extend_from_slice(&tmp[..n]);
905 if body.len() > length.saturating_add(64 * 1024) {
906 break;
907 }
908 }
909 body.truncate(length);
910 } else {
911 loop {
912 if deadline.saturating_duration_since(Instant::now()).is_zero() {
913 break;
914 }
915 let _ = arm_read(stream, deadline);
916 match stream.read(&mut tmp) {
917 Ok(0) => break,
918 Ok(n) => body.extend_from_slice(&tmp[..n]),
919 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
920 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
921 Err(err) if err.kind() == std::io::ErrorKind::TimedOut => break,
922 Err(_) => break,
923 }
924 }
925 close = true;
926 }
927 Ok(Raw {
929 status,
930 body: String::from_utf8_lossy(&body).into_owned(),
931 keep: !close && !(300..400).contains(&status),
932 retry_after,
933 })
934}
935
936fn arm_read(stream: &TcpStream, deadline: Instant) -> std::io::Result<()> {
937 let remain = deadline.saturating_duration_since(Instant::now());
938 if remain.is_zero() {
939 return Err(pack_io(std::io::Error::new(
940 std::io::ErrorKind::TimedOut,
941 "the pack did not answer",
942 )));
943 }
944 stream.set_read_timeout(Some(remain))?;
945 Ok(())
946}
947
948fn read_some(stream: &mut TcpStream, tmp: &mut [u8]) -> std::io::Result<usize> {
949 loop {
950 match stream.read(tmp) {
951 Ok(0) => {
952 return Err(std::io::Error::new(
953 std::io::ErrorKind::UnexpectedEof,
954 "the pack closed the socket",
955 ))
956 }
957 Ok(n) => return Ok(n),
958 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
959 Err(err) => return Err(pack_io(err)),
960 }
961 }
962}
963
964fn render(
965 method: &str,
966 host: &str,
967 path: &str,
968 token: Option<&str>,
969 body: Option<&[u8]>,
970) -> Vec<u8> {
971 let deadline = current_deadline(method);
972 let mut head = format!(
973 "{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: keep-alive\r\nX-Packset-Deadline: {deadline}\r\n"
974 );
975 if let Some(token) = token {
976 head.push_str("Authorization: Bearer ");
977 head.push_str(token);
978 head.push_str("\r\n");
979 }
980 let mut out = Vec::new();
981 if let Some(body) = body {
982 head.push_str("Content-Type: application/json\r\n");
983 head.push_str(&format!("Content-Length: {}\r\n\r\n", body.len()));
984 out.extend(head.into_bytes());
985 out.extend_from_slice(body);
986 } else {
987 head.push_str("\r\n");
988 out.extend(head.into_bytes());
989 }
990 out
991}
992
993struct Wire<'a> {
994 method: &'a str,
995 path: &'a str,
996 query: &'a [(&'a str, &'a str)],
997 body: Option<&'a [u8]>,
998 token: Option<&'a str>,
999}
1000
1001enum Gate {
1002 Send,
1004 Bare,
1006 Busy(Raw),
1008}
1009
1010impl PacksetClient {
1011 pub fn new(base: impl Into<String>) -> Self {
1012 let mut base = base.into();
1013 while base.ends_with('/') {
1014 base.pop();
1015 }
1016 let origin = Url::parse(&base).unwrap_or_else(|_| Url::parse("http://127.0.0.1/").unwrap());
1017 let addr = Url::parse(&base).ok().as_ref().and_then(pin);
1018 Self {
1019 base,
1020 origin,
1021 addr,
1022 workspace: None,
1023 token: Mutex::new(load_token()),
1024 }
1025 }
1026
1027 pub fn from_env() -> Result<Self, Error> {
1029 packset_client::load_seat_env();
1030 let url = std::env::var("PACKSET_URL")
1031 .or_else(|_| std::env::var("INSIDE_MEMORY_URL"))
1032 .ok()
1033 .filter(|url| !url.is_empty());
1034 match url {
1035 Some(url) if url == "off" => Err(Error::NoUrl),
1036 Some(url) => Ok(Self::new(url)),
1037 None => Ok(Self::new(format!(
1038 "http://127.0.0.1:{}",
1039 packset_client::default_port()
1040 ))),
1041 }
1042 }
1043
1044 #[must_use]
1045 pub fn with_workspace(mut self, workspace: impl Into<String>) -> Self {
1046 let workspace = workspace.into();
1047 self.workspace = (!workspace.is_empty()).then_some(workspace);
1048 self
1049 }
1050
1051 pub fn base(&self) -> &str {
1052 &self.base
1053 }
1054
1055 #[must_use]
1057 pub(crate) fn has_token(&self) -> bool {
1058 load_token().is_some()
1059 }
1060
1061 #[must_use]
1064 pub(crate) fn may_carry(&self) -> bool {
1065 load_token().is_some() && !foreign(&self.base)
1066 }
1067
1068 #[must_use]
1071 pub(crate) fn carried(&self) -> bool {
1072 let Some(addr) = self.addr else {
1073 return false;
1074 };
1075 let map = slots().lock().unwrap_or_else(|p| p.into_inner());
1076 map.get(&addr)
1077 .is_some_and(|slot| slot.lock().unwrap_or_else(|p| p.into_inner()).carried)
1078 }
1079
1080 pub fn workspace(&self) -> String {
1081 if let Some(workspace) = &self.workspace {
1082 return workspace.clone();
1083 }
1084 if let Ok(workspace) = std::env::var("PACKSET_WORKSPACE") {
1085 if !workspace.is_empty() {
1086 return workspace;
1087 }
1088 }
1089 let cwd = std::env::var("HOSTPACK_WORKSPACE")
1090 .ok()
1091 .map(|s| s.trim().to_string())
1092 .filter(|s| !s.is_empty())
1093 .map(std::path::PathBuf::from)
1094 .or_else(|| std::env::current_dir().ok())
1095 .unwrap_or_else(|| std::path::PathBuf::from("."));
1096 let abs = cwd.canonicalize().unwrap_or(cwd);
1097 let shown = abs.to_string_lossy().into_owned();
1098 if let Ok(answer) = self.roundtrip(
1099 "GET",
1100 "/v1/identity",
1101 &[("cwd", shown.as_str())],
1102 None,
1103 true,
1104 budget(),
1105 ) {
1106 if answer.status == 200 {
1107 if let Ok(val) = serde_json::from_str::<Value>(&answer.body) {
1108 if let Some(ws) = val.get("workspace").and_then(|v| v.as_str()) {
1109 if !ws.is_empty() {
1110 return ws.to_string();
1111 }
1112 }
1113 }
1114 }
1115 }
1116 format!("dir:{}", abs.display())
1117 }
1118
1119 pub(crate) fn roundtrip(
1126 &self,
1127 method: &str,
1128 path: &str,
1129 query: &[(&str, &str)],
1130 body: Option<&[u8]>,
1131 bearer: bool,
1132 timeout: Duration,
1133 ) -> Result<Answer, Error> {
1134 if method != "GET" {
1137 crate::forget_atom_listings();
1138 crate::forget_gate_fresh();
1139 forget_kept_answers();
1140 }
1141 let memo_key = answer_memo_key(method, path, query, bearer);
1142 if let Some(answer) = memo_key.as_deref().and_then(kept_answer) {
1143 return Ok(answer);
1144 }
1145 let Some(addr) = self.addr else {
1146 return Err(Error::Bad(format!(
1147 "{} is not a socket this seat can pin",
1148 self.base
1149 )));
1150 };
1151 let token = load_token();
1152 *self.token.lock().unwrap_or_else(|p| p.into_inner()) = token.clone();
1153 let allow = bearer && token.is_some() && !foreign(&self.base);
1154 if crate::tool_bound::armed() {
1155 crate::tool_bound::note("packsetd");
1156 }
1157 let budget = admission_retry_budget(method, timeout);
1158 let _held = HeldDeadline::enter(budget);
1159 let mut waited = Duration::ZERO;
1160 let mut wait = super::BUSY_WAIT;
1161 let mut asked = 0u32;
1162 loop {
1163 let answer = self.exchange(
1164 addr,
1165 &Wire {
1166 method,
1167 path,
1168 query,
1169 body,
1170 token: token.as_deref(),
1171 },
1172 allow,
1173 timeout,
1174 )?;
1175 if answer.status == 408 {
1176 return Err(deadline_error());
1177 }
1178 if answer.status != 503 {
1179 if let Some(key) = &memo_key {
1180 if (200..300).contains(&answer.status) {
1181 keep_answer(key.clone(), &answer);
1182 }
1183 }
1184 return Ok(answer);
1185 }
1186 asked = asked.saturating_add(1);
1187 match pause_for(wait, answer.retry_after, waited, budget, jitter_salt(asked)) {
1188 Some(pause) => {
1189 std::thread::sleep(pause.sleep);
1190 waited = waited.saturating_add(pause.charge);
1191 wait = (wait * BUSY_GROWTH).min(BUSY_WAIT_CAP);
1192 }
1193 None => return Err(shed_error(method)),
1194 }
1195 }
1196 }
1197
1198 fn exchange(
1199 &self,
1200 addr: SocketAddr,
1201 call: &Wire<'_>,
1202 allow: bool,
1203 timeout: Duration,
1204 ) -> Result<Answer, Error> {
1205 let _budget = PackClock::enter(timeout);
1210 let slot = slot_for(addr);
1211 let mut live = slot.lock().unwrap_or_else(|p| p.into_inner());
1212 let tag = token_tag(call.token);
1213 if live.token_tag != tag {
1214 live.stream = None;
1215 live.proven = false;
1216 live.token_tag = tag;
1217 }
1218 let carried = if allow {
1219 match self.gate(&mut live, addr, timeout)? {
1220 Gate::Bare => None,
1221 Gate::Send => call.token,
1222 Gate::Busy(raw) => {
1223 live.carried = false;
1224 live.proven = false;
1225 return Ok(Answer {
1226 status: raw.status,
1227 body: raw.body,
1228 trusted: false,
1229 retry_after: raw.retry_after,
1230 });
1231 }
1232 }
1233 } else {
1234 None
1235 };
1236 if live.stream.is_none() {
1237 match connect(addr, timeout) {
1238 Ok(stream) => live.stream = Some(stream),
1239 Err(err) => {
1240 live.proven = false;
1241 live.carried = false;
1242 return Err(Error::Io(err));
1243 }
1244 }
1245 live.proven = false;
1249 }
1250 let raw = match send(
1251 &mut live,
1252 &self.origin,
1253 &Wire {
1254 token: carried,
1255 ..*call
1256 },
1257 timeout,
1258 ) {
1259 Ok(raw) => raw,
1260 Err(err) => {
1261 live.stream = None;
1262 live.proven = false;
1263 live.carried = false;
1264 return Err(Error::Io(err));
1265 }
1266 };
1267 let trusted = carried.is_some();
1268 live.carried = trusted;
1269 if trusted && live.stream.is_some() {
1270 live.proven = true;
1271 } else if live.stream.is_none() {
1272 live.proven = false;
1273 }
1274 Ok(Answer {
1275 status: raw.status,
1276 body: raw.body,
1277 trusted,
1278 retry_after: raw.retry_after,
1279 })
1280 }
1281
1282 fn gate(&self, live: &mut Slot, addr: SocketAddr, timeout: Duration) -> Result<Gate, Error> {
1285 if live.proven && live.stream.is_some() {
1286 return Ok(Gate::Send);
1287 }
1288 live.proven = false;
1289 let (Some(id), Some(bogus)) = (nonce(), nonce()) else {
1290 return Err(Error::Io(std::io::Error::other(
1291 "no randomness for the pack proof",
1292 )));
1293 };
1294 let workspace = self.proof_workspace();
1295 let path = format!("/v1/atoms/{}", &id[..32]);
1296 let query = [("workspace", workspace.as_str())];
1297 let mut stage = 0u8;
1298 let mut restarts = 0u8;
1299 loop {
1300 if live.stream.is_none() {
1301 live.stream = Some(connect(addr, timeout).map_err(Error::Io)?);
1302 live.proven = false;
1303 }
1304 let shown = if stage == 0 {
1305 None
1306 } else {
1307 Some(bogus.as_str())
1308 };
1309 let raw = match send(
1310 live,
1311 &self.origin,
1312 &Wire {
1313 method: "GET",
1314 path: &path,
1315 query: &query,
1316 body: None,
1317 token: shown,
1318 },
1319 timeout,
1320 ) {
1321 Ok(raw) => raw,
1322 Err(err) => {
1323 live.stream = None;
1324 live.proven = false;
1325 if io_is_quiet(&err) {
1330 return Err(Error::Io(err));
1331 }
1332 restarts += 1;
1333 let spent = clock_deadline().is_some_and(|deadline| Instant::now() >= deadline);
1334 if spent || restarts > 2 {
1335 return Err(Error::Io(err));
1336 }
1337 stage = 0;
1338 continue;
1339 }
1340 };
1341 if raw.status == 503 || raw.status == 408 {
1343 return Ok(Gate::Busy(raw));
1344 }
1345 if raw.status != 401 {
1346 return Ok(Gate::Bare);
1347 }
1348 if stage == 0 {
1349 stage = 1;
1350 continue;
1351 }
1352 return Ok(Gate::Send);
1353 }
1354 }
1355
1356 fn proof_workspace(&self) -> String {
1357 if let Some(workspace) = &self.workspace {
1358 return workspace.clone();
1359 }
1360 std::env::var("PACKSET_WORKSPACE")
1361 .ok()
1362 .filter(|w| !w.is_empty())
1363 .unwrap_or_else(|| "seat".to_string())
1364 }
1365
1366 fn tried(
1368 &self,
1369 method: &str,
1370 path: &str,
1371 query: &[(&str, &str)],
1372 body: Option<&[u8]>,
1373 timeout: Duration,
1374 ) -> Result<Answer, Error> {
1375 self.roundtrip(method, path, query, body, true, timeout)
1376 }
1377
1378 fn json_ok<T: DeserializeOwned>(&self, path: &str, answer: Answer) -> Result<T, Error> {
1379 if !(200..300).contains(&answer.status) {
1380 return Err(refused(
1381 &format!("{}{path}", self.base),
1382 answer.status,
1383 &answer.body,
1384 ));
1385 }
1386 serde_json::from_str(&answer.body).map_err(Error::from)
1387 }
1388
1389 #[must_use]
1394 pub fn admission_line(&self) -> Option<String> {
1395 let timeout =
1396 Duration::from_millis(300).min(clock_left().unwrap_or(Duration::from_secs(1)));
1397 let answer = self
1398 .exchange_direct("GET", "/stats", &[], None, true, timeout)
1399 .ok()?;
1400 if answer.status == 503 {
1401 return Some("busy, retrying".to_string());
1402 }
1403 if !(200..300).contains(&answer.status) {
1404 return None;
1405 }
1406 let value: Value = serde_json::from_str(&answer.body).ok()?;
1407 value
1408 .get("line")
1409 .and_then(Value::as_str)
1410 .filter(|line| !line.is_empty())
1411 .map(str::to_string)
1412 }
1413
1414 fn exchange_direct(
1417 &self,
1418 method: &str,
1419 path: &str,
1420 query: &[(&str, &str)],
1421 body: Option<&[u8]>,
1422 bearer: bool,
1423 timeout: Duration,
1424 ) -> Result<Answer, Error> {
1425 let Some(addr) = self.addr else {
1426 return Err(Error::Bad(format!(
1427 "{} is not a socket this seat can pin",
1428 self.base
1429 )));
1430 };
1431 let token = load_token();
1432 let allow = bearer && token.is_some() && !foreign(&self.base);
1433 if crate::tool_bound::armed() {
1434 crate::tool_bound::note("packsetd");
1435 }
1436 let budget = admission_retry_budget(method, timeout);
1437 let _held = HeldDeadline::enter(budget);
1438 self.exchange(
1439 addr,
1440 &Wire {
1441 method,
1442 path,
1443 query,
1444 body,
1445 token: token.as_deref(),
1446 },
1447 allow,
1448 timeout,
1449 )
1450 }
1451
1452 pub fn health(&self) -> Result<String, Error> {
1453 let answer = self.tried("GET", "/health", &[], None, budget())?;
1454 if !(200..300).contains(&answer.status) {
1455 return Err(refused(
1456 &format!("{}/health", self.base),
1457 answer.status,
1458 &answer.body,
1459 ));
1460 }
1461 Ok(answer.body)
1462 }
1463
1464 pub fn status(&self, workspace: Option<&str>) -> Result<Value, Error> {
1465 let query: Vec<(&str, &str)> = workspace
1466 .map(|workspace| vec![("workspace", workspace)])
1467 .unwrap_or_default();
1468 let answer = self.tried("GET", "/v1/status", &query, None, budget())?;
1469 self.json_ok("/v1/status", answer)
1470 }
1471
1472 pub fn post_atom(&self, atom: &Value) -> Result<Value, Error> {
1473 let bytes = serde_json::to_vec(atom)?;
1474 let answer = self.tried("POST", "/v1/atoms", &[], Some(&bytes), budget())?;
1475 self.json_ok("/v1/atoms", answer)
1476 }
1477
1478 pub fn delete_atom(
1479 &self,
1480 workspace: &str,
1481 id: &str,
1482 why: Option<&str>,
1483 ) -> Result<Value, Error> {
1484 let mut body = serde_json::json!({"workspace": workspace, "id": id});
1485 if let Some(accession) = why {
1486 body["why"] = Value::String(accession.to_string());
1487 }
1488 let bytes = serde_json::to_vec(&body)?;
1489 let answer = self.tried("POST", "/v1/atoms/delete", &[], Some(&bytes), budget())?;
1490 if answer.status == 404 {
1491 return Err(Error::Bad(format!("no atom {id}")));
1492 }
1493 self.json_ok("/v1/atoms/delete", answer)
1494 }
1495
1496 pub fn atoms_of_kind(&self, workspace: &str, kind: &str) -> Result<Vec<Value>, Error> {
1497 let answer = self.tried(
1498 "GET",
1499 "/v1/atoms",
1500 &[("workspace", workspace), ("kind", kind)],
1501 None,
1502 budget(),
1503 )?;
1504 let body: Value = self.json_ok("/v1/atoms", answer)?;
1505 Ok(body
1506 .get("atoms")
1507 .and_then(Value::as_array)
1508 .cloned()
1509 .unwrap_or_default())
1510 }
1511
1512 pub fn atoms_in_set(&self, workspace: &str, set: &str) -> Result<Vec<Value>, Error> {
1513 let answer = self.tried(
1514 "GET",
1515 "/v1/pack",
1516 &[("workspace", workspace), ("set", set)],
1517 None,
1518 budget(),
1519 )?;
1520 let body: Value = self.json_ok("/v1/pack", answer)?;
1521 Ok(body
1522 .get("atoms")
1523 .and_then(Value::as_array)
1524 .cloned()
1525 .unwrap_or_default())
1526 }
1527
1528 pub fn sweep(&self, workspace: &str) -> Result<Value, Error> {
1529 let bytes = serde_json::to_vec(&serde_json::json!({"workspace": workspace}))?;
1530 let answer = self.tried("POST", "/v1/sweep", &[], Some(&bytes), budget())?;
1531 self.json_ok("/v1/sweep", answer)
1532 }
1533
1534 pub fn grade(&self, workspace: &str, id: &str, recalled: bool) -> Result<Value, Error> {
1535 let bytes = serde_json::to_vec(&serde_json::json!({
1536 "workspace": workspace,
1537 "id": id,
1538 "recalled": recalled,
1539 }))?;
1540 let answer = self.tried("POST", "/v1/grade", &[], Some(&bytes), budget())?;
1541 self.json_ok("/v1/grade", answer)
1542 }
1543
1544 pub fn update_atom(&self, workspace: &str, id: &str, fields: &Value) -> Result<Value, Error> {
1546 let bytes = serde_json::to_vec(&serde_json::json!({
1547 "workspace": workspace,
1548 "id": id,
1549 "fields": fields,
1550 }))?;
1551 let answer = self.tried("POST", "/v1/atoms/update", &[], Some(&bytes), budget())?;
1552 self.json_ok("/v1/atoms/update", answer)
1553 }
1554
1555 pub fn activate_as(
1556 &self,
1557 workspace: &str,
1558 q: &str,
1559 limit: u32,
1560 fire: bool,
1561 lens: Option<&str>,
1562 ) -> Result<Value, Error> {
1563 let limit = limit.to_string();
1564 let fire = if fire { "1" } else { "0" };
1565 let mut query = vec![
1566 ("workspace", workspace),
1567 ("q", q),
1568 ("limit", limit.as_str()),
1569 ("fire", fire),
1570 ];
1571 if let Some(name) = lens.filter(|n| !n.trim().is_empty()) {
1572 query.push(("as", name));
1573 }
1574 let answer = self.tried("GET", "/v1/activate", &query, None, budget())?;
1575 self.json_ok("/v1/activate", answer)
1576 }
1577
1578 pub fn hubs(&self, workspace: &str, limit: usize) -> Result<Value, Error> {
1579 let limit = limit.to_string();
1580 let answer = self.tried(
1581 "GET",
1582 "/v1/hubs",
1583 &[("workspace", workspace), ("limit", limit.as_str())],
1584 None,
1585 budget(),
1586 )?;
1587 self.json_ok("/v1/hubs", answer)
1588 }
1589
1590 pub fn consolidate(&self, workspace: &str, apply: bool) -> Result<Value, Error> {
1591 let bytes =
1592 serde_json::to_vec(&serde_json::json!({"workspace": workspace, "apply": apply}))?;
1593 let answer = self.tried("POST", "/v1/consolidate", &[], Some(&bytes), budget())?;
1594 self.json_ok("/v1/consolidate", answer)
1595 }
1596
1597 pub fn search_opts(
1598 &self,
1599 workspace: &str,
1600 q: &str,
1601 limit: u32,
1602 as_of: Option<&str>,
1603 rerank: bool,
1604 ) -> Result<Vec<Hit>, Error> {
1605 Ok(self.search_fetched(workspace, q, limit, as_of, rerank)?.0)
1606 }
1607
1608 pub(crate) fn search_fetched(
1609 &self,
1610 workspace: &str,
1611 q: &str,
1612 limit: u32,
1613 as_of: Option<&str>,
1614 rerank: bool,
1615 ) -> Result<(Vec<Hit>, bool), Error> {
1616 let limit_s = limit.to_string();
1617 let mut query = vec![
1618 ("workspace", workspace),
1619 ("q", q),
1620 ("limit", limit_s.as_str()),
1621 ];
1622 if let Some(at) = as_of {
1623 query.push(("as_of", at));
1624 }
1625 if rerank {
1626 query.push(("rerank", "1"));
1627 }
1628 let timeout = if rerank && !clock_armed() && std::env::var_os("LJOS_IN_HOOK").is_none() {
1631 Duration::from_secs(60).max(budget())
1632 } else {
1633 budget()
1634 };
1635 let answer = self.tried("GET", "/v1/search", &query, None, timeout)?;
1636 let trusted = answer.trusted;
1637 let body: Value = self.json_ok("/v1/search", answer)?;
1638 let hits = body.get("hits").cloned().unwrap_or(Value::Array(vec![]));
1639 Ok((serde_json::from_value(hits)?, trusted))
1640 }
1641
1642 pub fn fire(&self, workspace: &str, ids: &[String]) -> Result<Value, Error> {
1643 let bytes = serde_json::to_vec(&serde_json::json!({
1644 "workspace": workspace,
1645 "ids": ids,
1646 "as": "",
1647 }))?;
1648 let answer = self.tried("POST", "/v1/fire", &[], Some(&bytes), budget())?;
1649 self.json_ok("/v1/fire", answer)
1650 }
1651}
1652
1653fn refused(url: &str, code: u16, body: &str) -> Error {
1654 let reason = serde_json::from_str::<Value>(body)
1655 .ok()
1656 .and_then(|v| v.get("error").and_then(|r| r.as_str()).map(str::to_string))
1657 .unwrap_or_else(|| body.to_string());
1658 let reason = reason.trim();
1659 if code == 401 {
1660 return Error::Bad(format!(
1661 "{url}: 401: {reason}; this seat's token does not match the writer's, \
1662 so it is another user's writer or PACKSET_HOME names another pack"
1663 ));
1664 }
1665 if reason.is_empty() {
1666 Error::Bad(format!("{url}: status code {code}"))
1667 } else {
1668 Error::Bad(format!("{url}: {code}: {reason}"))
1669 }
1670}
1671
1672fn send(live: &mut Slot, origin: &Url, call: &Wire<'_>, timeout: Duration) -> std::io::Result<Raw> {
1673 if pack_quiet() {
1674 return Err(quiet_io());
1675 }
1676 if side_budget_spent() {
1677 return Err(budget_spent());
1678 }
1679 let started = Instant::now();
1680 let result = send_once(live, origin, call, timeout);
1681 if let Err(err) = &result {
1686 if io_is_quiet(err) {
1687 mark_pack_quiet();
1688 }
1689 super::charge_side_wait(started.elapsed());
1690 }
1691 result
1692}
1693
1694fn send_once(
1695 live: &mut Slot,
1696 origin: &Url,
1697 call: &Wire<'_>,
1698 timeout: Duration,
1699) -> std::io::Result<Raw> {
1700 let stream = live
1701 .stream
1702 .as_mut()
1703 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotConnected, "no pack socket"))?;
1704 let wait = bounded_wait(timeout, false)?;
1705 let deadline = Instant::now() + wait;
1706 stream.set_read_timeout(Some(wait))?;
1707 stream.set_write_timeout(Some(wait))?;
1708 let bytes = render(
1709 call.method,
1710 &host_header(origin),
1711 &target(call.path, call.query),
1712 call.token,
1713 call.body,
1714 );
1715 if let Err(err) = write_all(stream, &bytes) {
1716 live.stream = None;
1717 live.proven = false;
1718 return Err(err);
1719 }
1720 match read_response(stream, deadline) {
1721 Ok(raw) => {
1722 if !raw.keep {
1723 live.stream = None;
1724 live.proven = false;
1725 }
1726 Ok(raw)
1727 }
1728 Err(err) => {
1729 live.stream = None;
1730 live.proven = false;
1731 Err(err)
1732 }
1733 }
1734}
1735
1736#[cfg(test)]
1737mod tests {
1738 use super::{foreign, OwnerOnce, TABLE_READS};
1739
1740 fn reads() -> usize {
1741 TABLE_READS.with(std::cell::Cell::get)
1742 }
1743
1744 #[test]
1748 fn a_search_reads_the_port_owner_once_for_each_url() {
1749 let _g = crate::tests::env_guard();
1750 unsafe { std::env::remove_var("LJOS_IN_HOOK") };
1752 let (url, other) = ("http://127.0.0.1:9", "http://127.0.0.1:10");
1753 let start = reads();
1754 foreign(url);
1755 foreign(url);
1756 assert_eq!(reads() - start, 2, "outside a search each call reads");
1757 {
1758 let _search = OwnerOnce::enter();
1759 let first = foreign(url);
1760 {
1761 let _inner = OwnerOnce::enter();
1762 assert_eq!(foreign(url), first);
1763 }
1764 assert_eq!(foreign(url), first);
1765 foreign(other);
1766 foreign(other);
1767 assert_eq!(reads() - start, 4, "one search reads once a url");
1768 }
1769 foreign(url);
1770 assert_eq!(reads() - start, 5, "a call after the search reads again");
1771 }
1772
1773 fn quiet_pack() -> tempfile::TempDir {
1774 let home = tempfile::tempdir().unwrap();
1775 unsafe {
1776 std::env::remove_var("PACKSET_TOKEN");
1777 std::env::remove_var("PACKSET_TOKEN_FILE");
1778 std::env::remove_var("PACKSET_TIMEOUT_MS");
1779 std::env::remove_var("LJOS_IN_HOOK");
1780 std::env::set_var("PACKSET_HOME", home.path());
1781 }
1782 home
1783 }
1784
1785 fn serve(
1788 on: impl Fn(usize, &str) -> (u16, String, Option<u64>) + Send + 'static,
1789 ) -> (
1790 super::PacksetClient,
1791 std::sync::Arc<std::sync::Mutex<Vec<String>>>,
1792 ) {
1793 use std::io::{Read, Write};
1794 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1795 let addr = listener.local_addr().unwrap();
1796 let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1797 let log = std::sync::Arc::clone(&seen);
1798 std::thread::spawn(move || {
1799 for stream in listener.incoming() {
1800 let Ok(mut sock) = stream else { break };
1801 let _ = sock.set_read_timeout(Some(std::time::Duration::from_secs(2)));
1802 let mut buf = Vec::new();
1803 let mut tmp = [0u8; 2048];
1804 loop {
1805 match sock.read(&mut tmp) {
1806 Ok(0) | Err(_) => break,
1807 Ok(n) => {
1808 buf.extend_from_slice(&tmp[..n]);
1809 if buf.windows(4).any(|w| w == b"\r\n\r\n") {
1810 break;
1811 }
1812 }
1813 }
1814 }
1815 let req = String::from_utf8_lossy(&buf).into_owned();
1816 let n = {
1817 let mut log = log.lock().unwrap();
1818 log.push(req.clone());
1819 log.len() - 1
1820 };
1821 let (code, body, retry) = on(n, &req);
1822 let reason = match code {
1823 200 => "OK",
1824 408 => "Request Timeout",
1825 503 => "Service Unavailable",
1826 _ => "Error",
1827 };
1828 let retry = retry
1829 .map(|secs| format!("Retry-After: {secs}\r\n"))
1830 .unwrap_or_default();
1831 let resp = format!(
1832 "HTTP/1.1 {code} {reason}\r\n{retry}Content-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
1833 body.len()
1834 );
1835 let _ = sock.write_all(resp.as_bytes());
1836 }
1837 });
1838 (super::PacksetClient::new(format!("http://{addr}")), seen)
1839 }
1840
1841 fn deadline_of(req: &str) -> u64 {
1842 req.lines()
1843 .find_map(|line| {
1844 let (name, value) = line.split_once(':')?;
1845 name.eq_ignore_ascii_case("x-packset-deadline")
1846 .then(|| value.trim().parse().ok())
1847 .flatten()
1848 })
1849 .unwrap_or(0)
1850 }
1851
1852 #[test]
1853 fn a_read_waits_out_retry_after_and_then_reads() {
1854 let _g = crate::tests::env_guard();
1855 let _home = quiet_pack();
1856 let (client, seen) = serve(|n, req| {
1857 assert!(deadline_of(req) > 0, "{req}");
1858 if n == 0 {
1859 (
1860 503,
1861 r#"{"error":"packsetd is busy: the read queue is full"}"#.into(),
1862 Some(1),
1863 )
1864 } else {
1865 (200, "ok".into(), None)
1866 }
1867 });
1868 let started = std::time::Instant::now();
1869 let body = client.health().unwrap();
1870 let took = started.elapsed();
1871 assert_eq!(body, "ok");
1872 assert_eq!(seen.lock().unwrap().len(), 2);
1873 assert!(
1874 took >= std::time::Duration::from_millis(400),
1875 "Retry-After was not waited: {took:?}"
1876 );
1877 assert!(took < std::time::Duration::from_secs(3), "{took:?}");
1878 let first = &seen.lock().unwrap()[0];
1879 let now = super::unix_ms(std::time::SystemTime::now());
1880 let stamp = deadline_of(first);
1881 assert!(stamp > now, "deadline {stamp} is not after {now}");
1882 assert!(stamp < now + 5_000, "deadline {stamp} is not a short read");
1883 }
1884
1885 #[test]
1886 fn a_retry_after_past_the_read_budget_fails_in_one_line() {
1887 let _g = crate::tests::env_guard();
1888 let _home = quiet_pack();
1889 let (client, seen) = serve(|_, _| (503, r#"{"error":"busy"}"#.into(), Some(30)));
1890 let started = std::time::Instant::now();
1891 let err = client.health().unwrap_err();
1892 let took = started.elapsed();
1893 assert_eq!(err.to_string(), crate::PACK_READ_BUSY_LINE, "{err}");
1894 assert_eq!(seen.lock().unwrap().len(), 1, "the long wait was taken");
1895 assert!(took < std::time::Duration::from_millis(500), "{took:?}");
1896 }
1897
1898 #[test]
1899 fn a_passed_deadline_is_one_line_and_is_not_retried() {
1900 let _g = crate::tests::env_guard();
1901 let _home = quiet_pack();
1902 let (client, seen) = serve(|_, req| {
1903 assert!(deadline_of(req) > 0, "{req}");
1904 (
1905 408,
1906 r#"{"error":"the deadline passed before this request ran"}"#.into(),
1907 None,
1908 )
1909 });
1910 let err = client.health().unwrap_err();
1911 assert_eq!(err.to_string(), crate::PACK_DEADLINE_LINE, "{err}");
1912 assert_eq!(seen.lock().unwrap().len(), 1);
1913 }
1914
1915 #[test]
1916 fn a_shed_write_names_the_later_retry() {
1917 let _g = crate::tests::env_guard();
1918 let _home = quiet_pack();
1919 let (client, seen) = serve(|_, _| (503, r#"{"error":"busy"}"#.into(), Some(1)));
1920 let started = std::time::Instant::now();
1921 let err = {
1922 let _deadline = super::CommandDeadline::enter(std::time::Duration::from_millis(200));
1923 client
1924 .post_atom(&serde_json::json!({"text": "one write", "workspace": "seat"}))
1925 .unwrap_err()
1926 };
1927 let took = started.elapsed();
1928 assert_eq!(err.to_string(), crate::PACK_SHED_LINE, "{err}");
1929 assert_eq!(seen.lock().unwrap().len(), 1);
1930 assert!(took < std::time::Duration::from_millis(500), "{took:?}");
1931 }
1932
1933 #[test]
1934 fn stats_line_is_the_admission_line() {
1935 let _g = crate::tests::env_guard();
1936 let _home = quiet_pack();
1937 let (client, seen) = serve(|_, req| {
1938 let line = req.lines().next().unwrap_or("");
1939 if line.contains("GET /stats") {
1940 (200, r#"{"ok":true,"line":"busy, retrying"}"#.into(), None)
1941 } else {
1942 (200, r#"{"line":"ok"}"#.into(), None)
1943 }
1944 });
1945 assert_eq!(client.admission_line().as_deref(), Some("busy, retrying"));
1946 assert_eq!(seen.lock().unwrap().len(), 1);
1947 let (client, seen) = serve(|_, _| (503, r#"{"error":"busy"}"#.into(), Some(1)));
1948 assert_eq!(client.admission_line().as_deref(), Some("busy, retrying"));
1949 assert_eq!(
1950 seen.lock().unwrap().len(),
1951 1,
1952 "stats does not wait out Retry-After"
1953 );
1954 }
1955}