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 SHORT_READ: Duration = Duration::from_secs(1);
462
463fn mcp_read_cap() -> Duration {
470 let tool = crate::tool_bound::budget();
471 if tool > crate::tool_bound::DEFAULT {
472 if let Some(ms) = std::env::var("PACKSET_TIMEOUT_MS")
473 .ok()
474 .and_then(|value| value.trim().parse::<u64>().ok())
475 .filter(|ms| *ms > 0)
476 {
477 return Duration::from_millis(ms).min(tool);
478 }
479 }
480 SHORT_READ
481}
482const HEALTH_BUDGET: Duration = Duration::from_secs(2);
483const STATUS_BUDGET: Duration = Duration::from_secs(2);
484
485thread_local! {
486 static COMMAND_DEADLINE: std::cell::RefCell<Option<SystemTime>> = const { std::cell::RefCell::new(None) };
490 static HELD_DEADLINE: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
493}
494
495pub struct CommandDeadline {
498 prev: Option<SystemTime>,
499}
500
501impl CommandDeadline {
502 #[must_use]
503 pub fn enter(budget: Duration) -> Self {
504 let prev = COMMAND_DEADLINE.with(|cell| *cell.borrow());
505 let proposed = SystemTime::now() + budget;
506 let next = match prev {
507 Some(existing) if existing <= proposed => existing,
508 _ => proposed,
509 };
510 COMMAND_DEADLINE.with(|cell| *cell.borrow_mut() = Some(next));
511 Self { prev }
512 }
513}
514
515impl Drop for CommandDeadline {
516 fn drop(&mut self) {
517 let prev = self.prev.take();
518 COMMAND_DEADLINE.with(|cell| *cell.borrow_mut() = prev);
519 }
520}
521
522struct HeldDeadline {
523 prev: Option<String>,
524 active: bool,
525}
526
527impl HeldDeadline {
528 fn enter(budget: Duration) -> Self {
529 let prev = HELD_DEADLINE.with(|cell| cell.borrow().clone());
530 if prev.is_some() {
531 return Self {
532 prev,
533 active: false,
534 };
535 }
536 let stamp = deadline_stamp(budget);
537 HELD_DEADLINE.with(|cell| *cell.borrow_mut() = Some(stamp));
538 Self { prev, active: true }
539 }
540}
541
542impl Drop for HeldDeadline {
543 fn drop(&mut self) {
544 if self.active {
545 let prev = self.prev.clone();
546 HELD_DEADLINE.with(|cell| *cell.borrow_mut() = prev);
547 }
548 }
549}
550
551fn is_write(method: &str) -> bool {
552 !matches!(method, "GET" | "HEAD")
553}
554
555fn command_remaining() -> Option<Duration> {
556 let when = COMMAND_DEADLINE.with(|cell| *cell.borrow())?;
557 Some(
558 when.duration_since(SystemTime::now())
559 .unwrap_or(Duration::ZERO),
560 )
561}
562
563fn tightened(method: &str, requested: Duration) -> Duration {
569 let mut budget = requested;
570 if let Some(ms) = crate::pack_budget_ms() {
571 budget = budget.min(Duration::from_millis(ms.max(1)));
572 } else if let Some(ms) = std::env::var("PACKSET_TIMEOUT_MS")
573 .ok()
574 .and_then(|v| v.trim().parse::<u64>().ok())
575 .filter(|ms| *ms > 0)
576 {
577 budget = budget.min(Duration::from_millis(ms));
578 }
579 if let Some(left) = crate::wall_left() {
580 budget = budget.min(left);
581 }
582 if !is_write(method) && crate::tool_bound::armed() {
583 budget = budget.min(mcp_read_cap());
584 }
585 if let Some(left) = clock_left() {
586 budget = budget.min(left);
587 }
588 if let Some(left) = command_remaining() {
589 budget = budget.min(left);
590 }
591 budget.max(Duration::from_millis(1))
592}
593
594pub(crate) fn admission_retry_budget(method: &str, per_try: Duration) -> Duration {
596 tightened(method, per_try)
597}
598
599fn deadline_stamp(budget: Duration) -> String {
600 let proposed = SystemTime::now() + budget;
601 let when = match COMMAND_DEADLINE.with(|cell| *cell.borrow()) {
602 Some(command) if command < proposed => command,
603 _ => proposed,
604 };
605 unix_ms(when).to_string()
606}
607
608fn unix_ms(when: SystemTime) -> u64 {
609 when.duration_since(std::time::UNIX_EPOCH)
610 .map(|elapsed| u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX))
611 .unwrap_or(0)
612}
613
614fn current_deadline(method: &str) -> String {
615 HELD_DEADLINE
616 .with(|cell| cell.borrow().clone())
617 .unwrap_or_else(|| deadline_stamp(admission_retry_budget(method, budget())))
618}
619
620fn retry_after_of(header: Option<&str>) -> Option<Duration> {
621 let raw = header?.trim();
622 if raw.is_empty() {
623 return None;
624 }
625 raw.parse::<u64>().ok().map(Duration::from_secs)
626}
627
628#[derive(Clone, Copy, Debug, PartialEq, Eq)]
631pub(crate) enum ShedKind {
632 Read,
633 Write,
634 Deadline,
635}
636
637thread_local! {
638 static SHED: std::cell::RefCell<Vec<(String, ShedKind)>> =
639 const { std::cell::RefCell::new(Vec::new()) };
640}
641
642fn note_shed(line: &str, kind: ShedKind) {
643 SHED.with(|cell| {
644 let mut cell = cell.borrow_mut();
645 cell.retain(|(text, _)| text != line);
646 cell.push((line.to_string(), kind));
647 if cell.len() > 32 {
648 cell.remove(0);
649 }
650 });
651}
652
653pub(crate) fn shed_kind(line: &str) -> Option<ShedKind> {
654 SHED.with(|cell| {
655 cell.borrow()
656 .iter()
657 .rev()
658 .find(|(text, _)| text == line)
659 .map(|(_, kind)| *kind)
660 })
661}
662
663fn held_remaining() -> Option<Duration> {
666 let stamp = HELD_DEADLINE.with(|cell| cell.borrow().clone())?;
667 let ms: u64 = stamp.parse().ok()?;
668 let now = unix_ms(SystemTime::now());
669 Some(Duration::from_millis(ms.saturating_sub(now)))
670}
671
672fn deadline_passed() -> bool {
673 held_remaining().is_some_and(|left| left.is_zero())
674}
675
676fn retry_room(wait: Duration) -> bool {
678 held_remaining().is_some_and(|left| wait < left)
679}
680
681fn fault_line(body: &str, deadline: bool) -> String {
684 let parsed = serde_json::from_str::<Value>(body).ok().and_then(|value| {
685 value
686 .get("error")
687 .and_then(Value::as_str)
688 .map(str::to_string)
689 });
690 if let Some(text) = parsed {
691 let text = text.trim();
692 if let Some(line) = text.lines().next().map(str::trim).filter(|s| !s.is_empty()) {
693 return line.to_string();
694 }
695 }
696 if deadline {
697 return super::PACK_DEADLINE_LINE.to_string();
698 }
699 let raw = body.trim();
700 if !raw.is_empty() && !raw.contains('\n') && raw.len() <= 160 && !raw.starts_with('{') {
701 return raw.to_string();
702 }
703 "degraded".to_string()
704}
705
706fn degrade(body: &str, method: &str, deadline: bool) -> Error {
707 let line = fault_line(body, deadline);
708 let kind = if deadline {
709 ShedKind::Deadline
710 } else if is_write(method) {
711 ShedKind::Write
712 } else {
713 ShedKind::Read
714 };
715 note_shed(&line, kind);
716 Error::Unavailable(line)
717}
718
719static PACK_QUIET: AtomicBool = AtomicBool::new(false);
726static QUIET_HELD: AtomicUsize = AtomicUsize::new(0);
727
728pub struct QuietPack;
730
731impl QuietPack {
732 #[must_use]
735 pub fn enter() -> Self {
736 if QUIET_HELD.fetch_add(1, Ordering::AcqRel) == 0 {
737 PACK_QUIET.store(false, Ordering::Relaxed);
738 }
739 Self
740 }
741}
742
743impl Drop for QuietPack {
744 fn drop(&mut self) {
745 if QUIET_HELD.fetch_sub(1, Ordering::AcqRel) == 1 {
746 PACK_QUIET.store(false, Ordering::Relaxed);
747 }
748 }
749}
750
751fn holding_quiet() -> bool {
752 QUIET_HELD.load(Ordering::Relaxed) > 0
753}
754
755fn pack_quiet() -> bool {
756 holding_quiet() && PACK_QUIET.load(Ordering::Relaxed)
757}
758
759fn mark_pack_quiet() {
760 if holding_quiet() {
761 PACK_QUIET.store(true, Ordering::Relaxed);
762 }
763}
764
765fn tool_bound_spent() -> bool {
769 crate::tool_bound::remaining().is_some_and(|left| left <= Duration::from_millis(50))
770}
771
772fn pack_io(err: std::io::Error) -> std::io::Error {
773 let quiet = io_is_quiet(&err) || err.to_string().contains("the pack did not answer");
774 if quiet && tool_bound_spent() {
775 crate::tool_bound::note("packsetd");
776 crate::tool_bound::mark_failed("packsetd timed out");
777 return std::io::Error::new(std::io::ErrorKind::TimedOut, "packsetd timed out");
778 }
779 err
780}
781
782fn quiet_io() -> std::io::Error {
783 pack_io(std::io::Error::new(
784 std::io::ErrorKind::TimedOut,
785 "the pack did not answer",
786 ))
787}
788
789fn io_is_quiet(err: &std::io::Error) -> bool {
792 matches!(
793 err.kind(),
794 std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
795 )
796}
797
798fn connect(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
799 if pack_quiet() {
800 return Err(quiet_io());
801 }
802 if side_budget_spent() {
803 return Err(budget_spent());
804 }
805 let started = Instant::now();
806 let result = connect_once(addr, timeout);
807 if let Err(err) = &result {
812 if io_is_quiet(err) {
813 mark_pack_quiet();
814 }
815 super::charge_side_wait(started.elapsed());
816 }
817 result
818}
819
820fn side_budget_spent() -> bool {
824 super::side_remaining_ms() == Some(0)
825}
826
827fn budget_spent() -> std::io::Error {
828 std::io::Error::new(
829 std::io::ErrorKind::TimedOut,
830 "the side event's pack budget is spent",
831 )
832}
833
834fn connect_once(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
835 let wait = bounded_wait(timeout, true)?;
836 let stream = TcpStream::connect_timeout(&addr, wait)?;
837 let io_wait = bounded_wait(timeout, false)?;
838 stream.set_read_timeout(Some(io_wait))?;
839 stream.set_write_timeout(Some(io_wait))?;
840 let _ = stream.set_nodelay(true);
841 Ok(stream)
842}
843
844fn bounded_wait(requested: Duration, connect: bool) -> std::io::Result<Duration> {
848 let mut wait = capped_wait(requested);
849 if let Some(deadline) = clock_deadline() {
850 let left = deadline.saturating_duration_since(Instant::now());
851 if left.is_zero() {
852 return Err(pack_io(std::io::Error::new(
853 std::io::ErrorKind::TimedOut,
854 "the pack did not answer",
855 )));
856 }
857 wait = wait.min(left);
858 if connect {
859 wait = wait.min(super::pack_connect_budget());
860 }
861 if wait.is_zero() {
862 return Err(pack_io(std::io::Error::new(
863 std::io::ErrorKind::TimedOut,
864 "the pack did not answer",
865 )));
866 }
867 }
868 Ok(wait)
869}
870
871fn capped_wait(timeout: Duration) -> Duration {
874 let wait = super::cap_pack_wait(timeout);
875 if wait.is_zero() {
876 Duration::from_millis(1)
877 } else {
878 wait
879 }
880}
881
882fn write_all(stream: &mut TcpStream, bytes: &[u8]) -> std::io::Result<()> {
883 let mut sent = 0;
884 while sent < bytes.len() {
885 match stream.write(&bytes[sent..]) {
886 Ok(0) => {
887 return Err(std::io::Error::new(
888 std::io::ErrorKind::WriteZero,
889 "the pack closed the socket",
890 ))
891 }
892 Ok(n) => sent += n,
893 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
894 Err(err) => return Err(pack_io(err)),
895 }
896 }
897 stream.flush()
898}
899
900fn read_response(stream: &mut TcpStream, deadline: Instant) -> std::io::Result<Raw> {
901 let mut buf = Vec::new();
902 let mut tmp = [0u8; 4096];
903 let head_end = loop {
904 if buf.len() > 64 * 1024 {
905 return Err(std::io::Error::new(
906 std::io::ErrorKind::InvalidData,
907 "the pack's headers are too long",
908 ));
909 }
910 arm_read(stream, deadline)?;
911 let n = read_some(stream, &mut tmp)?;
912 buf.extend_from_slice(&tmp[..n]);
913 if let Some(at) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
914 break at;
915 }
916 };
917 let header = String::from_utf8_lossy(&buf[..head_end]).into_owned();
918 let status = header
919 .lines()
920 .next()
921 .and_then(|line| line.split_whitespace().nth(1))
922 .and_then(|code| code.parse().ok())
923 .ok_or_else(|| {
924 std::io::Error::new(
925 std::io::ErrorKind::InvalidData,
926 "the pack's status line is not HTTP",
927 )
928 })?;
929 let mut length = None;
930 let mut close = header.starts_with("HTTP/1.0");
931 let mut retry_after = None;
932 for line in header.lines().skip(1) {
933 let Some((name, value)) = line.split_once(':') else {
934 continue;
935 };
936 if name.eq_ignore_ascii_case("content-length") {
937 length = value.trim().parse().ok();
938 } else if name.eq_ignore_ascii_case("retry-after") {
939 retry_after = retry_after_of(Some(value));
940 } else if name.eq_ignore_ascii_case("connection")
941 && value.trim().eq_ignore_ascii_case("close")
942 {
943 close = true;
944 }
945 }
946 let mut body = buf.split_off(head_end + 4);
947 if let Some(length) = length {
948 while body.len() < length {
949 arm_read(stream, deadline)?;
950 let n = read_some(stream, &mut tmp)?;
951 body.extend_from_slice(&tmp[..n]);
952 if body.len() > length.saturating_add(64 * 1024) {
953 break;
954 }
955 }
956 body.truncate(length);
957 } else {
958 loop {
959 if deadline.saturating_duration_since(Instant::now()).is_zero() {
960 break;
961 }
962 let _ = arm_read(stream, deadline);
963 match stream.read(&mut tmp) {
964 Ok(0) => break,
965 Ok(n) => body.extend_from_slice(&tmp[..n]),
966 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
967 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
968 Err(err) if err.kind() == std::io::ErrorKind::TimedOut => break,
969 Err(_) => break,
970 }
971 }
972 close = true;
973 }
974 Ok(Raw {
976 status,
977 body: String::from_utf8_lossy(&body).into_owned(),
978 keep: !close && !(300..400).contains(&status),
979 retry_after,
980 })
981}
982
983fn arm_read(stream: &TcpStream, deadline: Instant) -> std::io::Result<()> {
984 let remain = deadline.saturating_duration_since(Instant::now());
985 if remain.is_zero() {
986 return Err(pack_io(std::io::Error::new(
987 std::io::ErrorKind::TimedOut,
988 "the pack did not answer",
989 )));
990 }
991 stream.set_read_timeout(Some(remain))?;
992 Ok(())
993}
994
995fn read_some(stream: &mut TcpStream, tmp: &mut [u8]) -> std::io::Result<usize> {
996 loop {
997 match stream.read(tmp) {
998 Ok(0) => {
999 return Err(std::io::Error::new(
1000 std::io::ErrorKind::UnexpectedEof,
1001 "the pack closed the socket",
1002 ))
1003 }
1004 Ok(n) => return Ok(n),
1005 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
1006 Err(err) => return Err(pack_io(err)),
1007 }
1008 }
1009}
1010
1011fn render(
1012 method: &str,
1013 host: &str,
1014 path: &str,
1015 token: Option<&str>,
1016 body: Option<&[u8]>,
1017) -> Vec<u8> {
1018 let deadline = current_deadline(method);
1019 let mut head = format!(
1020 "{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: keep-alive\r\nX-Packset-Deadline: {deadline}\r\n"
1021 );
1022 if let Some(token) = token {
1023 head.push_str("Authorization: Bearer ");
1024 head.push_str(token);
1025 head.push_str("\r\n");
1026 }
1027 let mut out = Vec::new();
1028 if let Some(body) = body {
1029 head.push_str("Content-Type: application/json\r\n");
1030 head.push_str(&format!("Content-Length: {}\r\n\r\n", body.len()));
1031 out.extend(head.into_bytes());
1032 out.extend_from_slice(body);
1033 } else {
1034 head.push_str("\r\n");
1035 out.extend(head.into_bytes());
1036 }
1037 out
1038}
1039
1040struct Wire<'a> {
1041 method: &'a str,
1042 path: &'a str,
1043 query: &'a [(&'a str, &'a str)],
1044 body: Option<&'a [u8]>,
1045 token: Option<&'a str>,
1046}
1047
1048enum Gate {
1049 Send,
1051 Bare,
1053 Busy(Raw),
1055}
1056
1057impl PacksetClient {
1058 pub fn new(base: impl Into<String>) -> Self {
1059 let mut base = base.into();
1060 while base.ends_with('/') {
1061 base.pop();
1062 }
1063 let origin = Url::parse(&base).unwrap_or_else(|_| Url::parse("http://127.0.0.1/").unwrap());
1064 let addr = Url::parse(&base).ok().as_ref().and_then(pin);
1065 Self {
1066 base,
1067 origin,
1068 addr,
1069 workspace: None,
1070 token: Mutex::new(load_token()),
1071 }
1072 }
1073
1074 pub fn from_env() -> Result<Self, Error> {
1076 packset_client::load_seat_env();
1077 let url = std::env::var("PACKSET_URL")
1078 .or_else(|_| std::env::var("INSIDE_MEMORY_URL"))
1079 .ok()
1080 .filter(|url| !url.is_empty());
1081 match url {
1082 Some(url) if url == "off" => Err(Error::NoUrl),
1083 Some(url) => Ok(Self::new(url)),
1084 None => Ok(Self::new(format!(
1085 "http://127.0.0.1:{}",
1086 packset_client::default_port()
1087 ))),
1088 }
1089 }
1090
1091 #[must_use]
1092 pub fn with_workspace(mut self, workspace: impl Into<String>) -> Self {
1093 let workspace = workspace.into();
1094 self.workspace = (!workspace.is_empty()).then_some(workspace);
1095 self
1096 }
1097
1098 pub fn base(&self) -> &str {
1099 &self.base
1100 }
1101
1102 #[must_use]
1104 pub(crate) fn has_token(&self) -> bool {
1105 load_token().is_some()
1106 }
1107
1108 #[must_use]
1111 pub(crate) fn may_carry(&self) -> bool {
1112 load_token().is_some() && !foreign(&self.base)
1113 }
1114
1115 #[must_use]
1118 pub(crate) fn carried(&self) -> bool {
1119 let Some(addr) = self.addr else {
1120 return false;
1121 };
1122 let map = slots().lock().unwrap_or_else(|p| p.into_inner());
1123 map.get(&addr)
1124 .is_some_and(|slot| slot.lock().unwrap_or_else(|p| p.into_inner()).carried)
1125 }
1126
1127 pub fn workspace(&self) -> String {
1128 if let Some(workspace) = &self.workspace {
1129 return workspace.clone();
1130 }
1131 if let Ok(workspace) = std::env::var("PACKSET_WORKSPACE") {
1132 if !workspace.is_empty() {
1133 return workspace;
1134 }
1135 }
1136 let cwd = std::env::var("HOSTPACK_WORKSPACE")
1137 .ok()
1138 .map(|s| s.trim().to_string())
1139 .filter(|s| !s.is_empty())
1140 .map(std::path::PathBuf::from)
1141 .or_else(|| std::env::current_dir().ok())
1142 .unwrap_or_else(|| std::path::PathBuf::from("."));
1143 let abs = cwd.canonicalize().unwrap_or(cwd);
1144 let shown = abs.to_string_lossy().into_owned();
1145 if let Ok(answer) = self.roundtrip(
1146 "GET",
1147 "/v1/identity",
1148 &[("cwd", shown.as_str())],
1149 None,
1150 true,
1151 STATUS_BUDGET,
1152 ) {
1153 if answer.status == 200 {
1154 if let Ok(val) = serde_json::from_str::<Value>(&answer.body) {
1155 if let Some(ws) = val.get("workspace").and_then(|v| v.as_str()) {
1156 if !ws.is_empty() {
1157 return ws.to_string();
1158 }
1159 }
1160 }
1161 }
1162 }
1163 format!("dir:{}", abs.display())
1164 }
1165
1166 pub(crate) fn roundtrip(
1172 &self,
1173 method: &str,
1174 path: &str,
1175 query: &[(&str, &str)],
1176 body: Option<&[u8]>,
1177 bearer: bool,
1178 timeout: Duration,
1179 ) -> Result<Answer, Error> {
1180 if method != "GET" {
1183 crate::forget_atom_listings();
1184 crate::forget_gate_fresh();
1185 forget_kept_answers();
1186 }
1187 let memo_key = answer_memo_key(method, path, query, bearer);
1188 if let Some(answer) = memo_key.as_deref().and_then(kept_answer) {
1189 return Ok(answer);
1190 }
1191 let Some(addr) = self.addr else {
1192 return Err(Error::Bad(format!(
1193 "{} is not a socket this seat can pin",
1194 self.base
1195 )));
1196 };
1197 let token = load_token();
1198 *self.token.lock().unwrap_or_else(|p| p.into_inner()) = token.clone();
1199 let allow = bearer && token.is_some() && !foreign(&self.base);
1200 if crate::tool_bound::armed() {
1201 crate::tool_bound::note("packsetd");
1202 }
1203 let budget = tightened(method, timeout);
1204 let _held = HeldDeadline::enter(budget);
1205 let _clock = PackClock::enter(budget);
1206 let call = Wire {
1207 method,
1208 path,
1209 query,
1210 body,
1211 token: token.as_deref(),
1212 };
1213 let answer = self.exchange(addr, &call, allow, budget)?;
1214 let answer = match answer.status {
1215 408 => return Err(degrade(&answer.body, method, true)),
1216 503 => match answer
1217 .retry_after
1218 .filter(|wait| !deadline_passed() && retry_room(*wait))
1219 {
1220 Some(wait) => {
1221 std::thread::sleep(wait);
1222 if deadline_passed() {
1223 return Err(degrade(&answer.body, method, true));
1224 }
1225 self.exchange(addr, &call, allow, budget)?
1226 }
1227 None => return Err(degrade(&answer.body, method, deadline_passed())),
1228 },
1229 _ => answer,
1230 };
1231 if answer.status == 408 {
1232 return Err(degrade(&answer.body, method, true));
1233 }
1234 if answer.status == 503 {
1235 return Err(degrade(&answer.body, method, false));
1236 }
1237 if deadline_passed() && !(200..300).contains(&answer.status) {
1238 return Err(degrade(&answer.body, method, true));
1239 }
1240 if let Some(key) = &memo_key {
1241 if (200..300).contains(&answer.status) {
1242 keep_answer(key.clone(), &answer);
1243 }
1244 }
1245 Ok(answer)
1246 }
1247
1248 fn exchange(
1249 &self,
1250 addr: SocketAddr,
1251 call: &Wire<'_>,
1252 allow: bool,
1253 timeout: Duration,
1254 ) -> Result<Answer, Error> {
1255 let _budget = PackClock::enter(timeout);
1260 let slot = slot_for(addr);
1261 let mut live = slot.lock().unwrap_or_else(|p| p.into_inner());
1262 let tag = token_tag(call.token);
1263 if live.token_tag != tag {
1264 live.stream = None;
1265 live.proven = false;
1266 live.token_tag = tag;
1267 }
1268 let carried = if allow {
1269 match self.gate(&mut live, addr, timeout)? {
1270 Gate::Bare => None,
1271 Gate::Send => call.token,
1272 Gate::Busy(raw) => {
1273 live.carried = false;
1274 live.proven = false;
1275 return Ok(Answer {
1276 status: raw.status,
1277 body: raw.body,
1278 trusted: false,
1279 retry_after: raw.retry_after,
1280 });
1281 }
1282 }
1283 } else {
1284 None
1285 };
1286 if live.stream.is_none() {
1287 match connect(addr, timeout) {
1288 Ok(stream) => live.stream = Some(stream),
1289 Err(err) => {
1290 live.proven = false;
1291 live.carried = false;
1292 return Err(Error::Io(err));
1293 }
1294 }
1295 live.proven = false;
1299 }
1300 let raw = match send(
1301 &mut live,
1302 &self.origin,
1303 &Wire {
1304 token: carried,
1305 ..*call
1306 },
1307 timeout,
1308 ) {
1309 Ok(raw) => raw,
1310 Err(err) => {
1311 live.stream = None;
1312 live.proven = false;
1313 live.carried = false;
1314 return Err(Error::Io(err));
1315 }
1316 };
1317 let trusted = carried.is_some();
1318 live.carried = trusted;
1319 if trusted && live.stream.is_some() {
1320 live.proven = true;
1321 } else if live.stream.is_none() {
1322 live.proven = false;
1323 }
1324 Ok(Answer {
1325 status: raw.status,
1326 body: raw.body,
1327 trusted,
1328 retry_after: raw.retry_after,
1329 })
1330 }
1331
1332 fn gate(&self, live: &mut Slot, addr: SocketAddr, timeout: Duration) -> Result<Gate, Error> {
1335 if live.proven && live.stream.is_some() {
1336 return Ok(Gate::Send);
1337 }
1338 live.proven = false;
1339 let (Some(id), Some(bogus)) = (nonce(), nonce()) else {
1340 return Err(Error::Io(std::io::Error::other(
1341 "no randomness for the pack proof",
1342 )));
1343 };
1344 let workspace = self.proof_workspace();
1345 let path = format!("/v1/atoms/{}", &id[..32]);
1346 let query = [("workspace", workspace.as_str())];
1347 let mut stage = 0u8;
1348 let mut restarts = 0u8;
1349 loop {
1350 if live.stream.is_none() {
1351 live.stream = Some(connect(addr, timeout).map_err(Error::Io)?);
1352 live.proven = false;
1353 }
1354 let shown = if stage == 0 {
1355 None
1356 } else {
1357 Some(bogus.as_str())
1358 };
1359 let raw = match send(
1360 live,
1361 &self.origin,
1362 &Wire {
1363 method: "GET",
1364 path: &path,
1365 query: &query,
1366 body: None,
1367 token: shown,
1368 },
1369 timeout,
1370 ) {
1371 Ok(raw) => raw,
1372 Err(err) => {
1373 live.stream = None;
1374 live.proven = false;
1375 if io_is_quiet(&err) {
1380 return Err(Error::Io(err));
1381 }
1382 restarts += 1;
1383 let spent = clock_deadline().is_some_and(|deadline| Instant::now() >= deadline);
1384 if spent || restarts > 2 {
1385 return Err(Error::Io(err));
1386 }
1387 stage = 0;
1388 continue;
1389 }
1390 };
1391 if raw.status == 503 || raw.status == 408 {
1393 return Ok(Gate::Busy(raw));
1394 }
1395 if raw.status != 401 {
1396 return Ok(Gate::Bare);
1397 }
1398 if stage == 0 {
1399 stage = 1;
1400 continue;
1401 }
1402 return Ok(Gate::Send);
1403 }
1404 }
1405
1406 fn proof_workspace(&self) -> String {
1407 if let Some(workspace) = &self.workspace {
1408 return workspace.clone();
1409 }
1410 std::env::var("PACKSET_WORKSPACE")
1411 .ok()
1412 .filter(|w| !w.is_empty())
1413 .unwrap_or_else(|| "seat".to_string())
1414 }
1415
1416 fn tried(
1418 &self,
1419 method: &str,
1420 path: &str,
1421 query: &[(&str, &str)],
1422 body: Option<&[u8]>,
1423 timeout: Duration,
1424 ) -> Result<Answer, Error> {
1425 self.roundtrip(method, path, query, body, true, timeout)
1426 }
1427
1428 fn json_ok<T: DeserializeOwned>(&self, path: &str, answer: Answer) -> Result<T, Error> {
1429 if !(200..300).contains(&answer.status) {
1430 return Err(refused(
1431 &format!("{}{path}", self.base),
1432 answer.status,
1433 &answer.body,
1434 ));
1435 }
1436 serde_json::from_str(&answer.body).map_err(Error::from)
1437 }
1438
1439 #[must_use]
1444 pub fn admission_line(&self) -> Option<String> {
1445 let timeout =
1446 Duration::from_millis(300).min(clock_left().unwrap_or(Duration::from_secs(1)));
1447 let answer = self
1448 .exchange_direct("GET", "/stats", &[], None, true, timeout)
1449 .ok()?;
1450 if answer.status == 503 {
1451 return Some("busy, retrying".to_string());
1452 }
1453 if !(200..300).contains(&answer.status) {
1454 return None;
1455 }
1456 let value: Value = serde_json::from_str(&answer.body).ok()?;
1457 value
1458 .get("line")
1459 .and_then(Value::as_str)
1460 .filter(|line| !line.is_empty())
1461 .map(str::to_string)
1462 }
1463
1464 fn exchange_direct(
1467 &self,
1468 method: &str,
1469 path: &str,
1470 query: &[(&str, &str)],
1471 body: Option<&[u8]>,
1472 bearer: bool,
1473 timeout: Duration,
1474 ) -> Result<Answer, Error> {
1475 let Some(addr) = self.addr else {
1476 return Err(Error::Bad(format!(
1477 "{} is not a socket this seat can pin",
1478 self.base
1479 )));
1480 };
1481 let token = load_token();
1482 let allow = bearer && token.is_some() && !foreign(&self.base);
1483 if crate::tool_bound::armed() {
1484 crate::tool_bound::note("packsetd");
1485 }
1486 let budget = admission_retry_budget(method, timeout);
1487 let _held = HeldDeadline::enter(budget);
1488 self.exchange(
1489 addr,
1490 &Wire {
1491 method,
1492 path,
1493 query,
1494 body,
1495 token: token.as_deref(),
1496 },
1497 allow,
1498 timeout,
1499 )
1500 }
1501
1502 pub fn health(&self) -> Result<String, Error> {
1503 let answer = self.tried("GET", "/health", &[], None, HEALTH_BUDGET)?;
1504 if !(200..300).contains(&answer.status) {
1505 return Err(refused(
1506 &format!("{}/health", self.base),
1507 answer.status,
1508 &answer.body,
1509 ));
1510 }
1511 Ok(answer.body)
1512 }
1513
1514 pub fn status(&self, workspace: Option<&str>) -> Result<Value, Error> {
1515 let query: Vec<(&str, &str)> = workspace
1516 .map(|workspace| vec![("workspace", workspace)])
1517 .unwrap_or_default();
1518 let answer = self.tried("GET", "/v1/status", &query, None, STATUS_BUDGET)?;
1519 self.json_ok("/v1/status", answer)
1520 }
1521
1522 pub fn post_atom(&self, atom: &Value) -> Result<Value, Error> {
1523 let bytes = serde_json::to_vec(atom)?;
1524 let answer = self.tried(
1525 "POST",
1526 "/v1/atoms",
1527 &[],
1528 Some(&bytes),
1529 super::REMEMBER_BUDGET,
1530 )?;
1531 self.json_ok("/v1/atoms", answer)
1532 }
1533
1534 pub fn delete_atom(
1535 &self,
1536 workspace: &str,
1537 id: &str,
1538 why: Option<&str>,
1539 ) -> Result<Value, Error> {
1540 let mut body = serde_json::json!({"workspace": workspace, "id": id});
1541 if let Some(accession) = why {
1542 body["why"] = Value::String(accession.to_string());
1543 }
1544 let bytes = serde_json::to_vec(&body)?;
1545 let answer = self.tried(
1546 "POST",
1547 "/v1/atoms/delete",
1548 &[],
1549 Some(&bytes),
1550 super::REMEMBER_BUDGET,
1551 )?;
1552 if answer.status == 404 {
1553 return Err(Error::Bad(format!("no atom {id}")));
1554 }
1555 self.json_ok("/v1/atoms/delete", answer)
1556 }
1557
1558 pub fn atoms_of_kind(&self, workspace: &str, kind: &str) -> Result<Vec<Value>, Error> {
1559 let answer = self.tried(
1560 "GET",
1561 "/v1/atoms",
1562 &[("workspace", workspace), ("kind", kind)],
1563 None,
1564 super::SEARCH_PACK_BUDGET,
1565 )?;
1566 let body: Value = self.json_ok("/v1/atoms", answer)?;
1567 Ok(body
1568 .get("atoms")
1569 .and_then(Value::as_array)
1570 .cloned()
1571 .unwrap_or_default())
1572 }
1573
1574 pub fn atoms_in_set(&self, workspace: &str, set: &str) -> Result<Vec<Value>, Error> {
1575 let answer = self.tried(
1576 "GET",
1577 "/v1/pack",
1578 &[("workspace", workspace), ("set", set)],
1579 None,
1580 super::SEARCH_PACK_BUDGET,
1581 )?;
1582 let body: Value = self.json_ok("/v1/pack", answer)?;
1583 Ok(body
1584 .get("atoms")
1585 .and_then(Value::as_array)
1586 .cloned()
1587 .unwrap_or_default())
1588 }
1589
1590 pub fn sweep(&self, workspace: &str) -> Result<Value, Error> {
1591 let bytes = serde_json::to_vec(&serde_json::json!({"workspace": workspace}))?;
1592 let answer = self.tried(
1593 "POST",
1594 "/v1/sweep",
1595 &[],
1596 Some(&bytes),
1597 super::REMEMBER_BUDGET,
1598 )?;
1599 self.json_ok("/v1/sweep", answer)
1600 }
1601
1602 pub fn grade(&self, workspace: &str, id: &str, recalled: bool) -> Result<Value, Error> {
1603 let bytes = serde_json::to_vec(&serde_json::json!({
1604 "workspace": workspace,
1605 "id": id,
1606 "recalled": recalled,
1607 }))?;
1608 let answer = self.tried(
1609 "POST",
1610 "/v1/grade",
1611 &[],
1612 Some(&bytes),
1613 super::REMEMBER_BUDGET,
1614 )?;
1615 self.json_ok("/v1/grade", answer)
1616 }
1617
1618 pub fn update_atom(&self, workspace: &str, id: &str, fields: &Value) -> Result<Value, Error> {
1620 let bytes = serde_json::to_vec(&serde_json::json!({
1621 "workspace": workspace,
1622 "id": id,
1623 "fields": fields,
1624 }))?;
1625 let answer = self.tried(
1626 "POST",
1627 "/v1/atoms/update",
1628 &[],
1629 Some(&bytes),
1630 super::REMEMBER_BUDGET,
1631 )?;
1632 self.json_ok("/v1/atoms/update", answer)
1633 }
1634
1635 pub fn activate_as(
1636 &self,
1637 workspace: &str,
1638 q: &str,
1639 limit: u32,
1640 fire: bool,
1641 lens: Option<&str>,
1642 ) -> Result<Value, Error> {
1643 let limit = limit.to_string();
1644 let fire = if fire { "1" } else { "0" };
1645 let mut query = vec![
1646 ("workspace", workspace),
1647 ("q", q),
1648 ("limit", limit.as_str()),
1649 ("fire", fire),
1650 ];
1651 if let Some(name) = lens.filter(|n| !n.trim().is_empty()) {
1652 query.push(("as", name));
1653 }
1654 let answer = self.tried(
1655 "GET",
1656 "/v1/activate",
1657 &query,
1658 None,
1659 super::SEARCH_PACK_BUDGET,
1660 )?;
1661 self.json_ok("/v1/activate", answer)
1662 }
1663
1664 pub fn hubs(&self, workspace: &str, limit: usize) -> Result<Value, Error> {
1665 let limit = limit.to_string();
1666 let answer = self.tried(
1667 "GET",
1668 "/v1/hubs",
1669 &[("workspace", workspace), ("limit", limit.as_str())],
1670 None,
1671 super::SEARCH_PACK_BUDGET,
1672 )?;
1673 self.json_ok("/v1/hubs", answer)
1674 }
1675
1676 pub fn consolidate(&self, workspace: &str, apply: bool) -> Result<Value, Error> {
1677 let bytes =
1678 serde_json::to_vec(&serde_json::json!({"workspace": workspace, "apply": apply}))?;
1679 let answer = self.tried(
1680 "POST",
1681 "/v1/consolidate",
1682 &[],
1683 Some(&bytes),
1684 super::REMEMBER_BUDGET,
1685 )?;
1686 self.json_ok("/v1/consolidate", answer)
1687 }
1688
1689 pub fn search_opts(
1690 &self,
1691 workspace: &str,
1692 q: &str,
1693 limit: u32,
1694 as_of: Option<&str>,
1695 rerank: bool,
1696 ) -> Result<Vec<Hit>, Error> {
1697 Ok(self.search_fetched(workspace, q, limit, as_of, rerank)?.0)
1698 }
1699
1700 pub(crate) fn search_fetched(
1701 &self,
1702 workspace: &str,
1703 q: &str,
1704 limit: u32,
1705 as_of: Option<&str>,
1706 rerank: bool,
1707 ) -> Result<(Vec<Hit>, bool), Error> {
1708 let limit_s = limit.to_string();
1709 let mut query = vec![
1710 ("workspace", workspace),
1711 ("q", q),
1712 ("limit", limit_s.as_str()),
1713 ];
1714 if let Some(at) = as_of {
1715 query.push(("as_of", at));
1716 }
1717 if rerank {
1718 query.push(("rerank", "1"));
1719 }
1720 let timeout = if rerank
1725 && !clock_armed()
1726 && std::env::var_os("LJOS_IN_HOOK").is_none()
1727 && !crate::tool_bound::armed()
1728 {
1729 Duration::from_secs(60)
1730 } else if crate::tool_bound::armed() {
1731 mcp_read_cap()
1732 } else {
1733 super::SEARCH_PACK_BUDGET
1734 };
1735 let answer = self.tried("GET", "/v1/search", &query, None, timeout)?;
1736 let trusted = answer.trusted;
1737 let body: Value = self.json_ok("/v1/search", answer)?;
1738 let hits = body.get("hits").cloned().unwrap_or(Value::Array(vec![]));
1739 Ok((serde_json::from_value(hits)?, trusted))
1740 }
1741
1742 pub fn fire(&self, workspace: &str, ids: &[String]) -> Result<Value, Error> {
1743 let bytes = serde_json::to_vec(&serde_json::json!({
1744 "workspace": workspace,
1745 "ids": ids,
1746 "as": "",
1747 }))?;
1748 let answer = self.tried(
1749 "POST",
1750 "/v1/fire",
1751 &[],
1752 Some(&bytes),
1753 super::REMEMBER_BUDGET,
1754 )?;
1755 self.json_ok("/v1/fire", answer)
1756 }
1757}
1758
1759fn refused(url: &str, code: u16, body: &str) -> Error {
1760 let reason = serde_json::from_str::<Value>(body)
1761 .ok()
1762 .and_then(|v| v.get("error").and_then(|r| r.as_str()).map(str::to_string))
1763 .unwrap_or_else(|| body.to_string());
1764 let reason = reason.trim();
1765 if code == 401 {
1766 return Error::Bad(format!(
1767 "{url}: 401: {reason}; this seat's token does not match the writer's, \
1768 so it is another user's writer or PACKSET_HOME names another pack"
1769 ));
1770 }
1771 if reason.is_empty() {
1772 Error::Bad(format!("{url}: status code {code}"))
1773 } else {
1774 Error::Bad(format!("{url}: {code}: {reason}"))
1775 }
1776}
1777
1778fn send(live: &mut Slot, origin: &Url, call: &Wire<'_>, timeout: Duration) -> std::io::Result<Raw> {
1779 if pack_quiet() {
1780 return Err(quiet_io());
1781 }
1782 if side_budget_spent() {
1783 return Err(budget_spent());
1784 }
1785 let started = Instant::now();
1786 let result = send_once(live, origin, call, timeout);
1787 if let Err(err) = &result {
1792 if io_is_quiet(err) {
1793 mark_pack_quiet();
1794 }
1795 super::charge_side_wait(started.elapsed());
1796 }
1797 result
1798}
1799
1800fn send_once(
1801 live: &mut Slot,
1802 origin: &Url,
1803 call: &Wire<'_>,
1804 timeout: Duration,
1805) -> std::io::Result<Raw> {
1806 let stream = live
1807 .stream
1808 .as_mut()
1809 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotConnected, "no pack socket"))?;
1810 let wait = bounded_wait(timeout, false)?;
1811 let deadline = Instant::now() + wait;
1812 stream.set_read_timeout(Some(wait))?;
1813 stream.set_write_timeout(Some(wait))?;
1814 let bytes = render(
1815 call.method,
1816 &host_header(origin),
1817 &target(call.path, call.query),
1818 call.token,
1819 call.body,
1820 );
1821 if let Err(err) = write_all(stream, &bytes) {
1822 live.stream = None;
1823 live.proven = false;
1824 return Err(err);
1825 }
1826 match read_response(stream, deadline) {
1827 Ok(raw) => {
1828 if !raw.keep {
1829 live.stream = None;
1830 live.proven = false;
1831 }
1832 Ok(raw)
1833 }
1834 Err(err) => {
1835 live.stream = None;
1836 live.proven = false;
1837 Err(err)
1838 }
1839 }
1840}
1841
1842#[cfg(test)]
1843mod tests {
1844 use super::{foreign, OwnerOnce, TABLE_READS};
1845
1846 fn reads() -> usize {
1847 TABLE_READS.with(std::cell::Cell::get)
1848 }
1849
1850 #[test]
1854 fn a_search_reads_the_port_owner_once_for_each_url() {
1855 let _g = crate::tests::env_guard();
1856 unsafe { std::env::remove_var("LJOS_IN_HOOK") };
1858 let (url, other) = ("http://127.0.0.1:9", "http://127.0.0.1:10");
1859 let start = reads();
1860 foreign(url);
1861 foreign(url);
1862 assert_eq!(reads() - start, 2, "outside a search each call reads");
1863 {
1864 let _search = OwnerOnce::enter();
1865 let first = foreign(url);
1866 {
1867 let _inner = OwnerOnce::enter();
1868 assert_eq!(foreign(url), first);
1869 }
1870 assert_eq!(foreign(url), first);
1871 foreign(other);
1872 foreign(other);
1873 assert_eq!(reads() - start, 4, "one search reads once a url");
1874 }
1875 foreign(url);
1876 assert_eq!(reads() - start, 5, "a call after the search reads again");
1877 }
1878
1879 fn quiet_pack() -> tempfile::TempDir {
1880 let home = tempfile::tempdir().unwrap();
1881 unsafe {
1882 std::env::remove_var("PACKSET_TOKEN");
1883 std::env::remove_var("PACKSET_TOKEN_FILE");
1884 std::env::remove_var("PACKSET_TIMEOUT_MS");
1885 std::env::remove_var("LJOS_IN_HOOK");
1886 std::env::set_var("PACKSET_HOME", home.path());
1887 }
1888 home
1889 }
1890
1891 fn serve(
1894 on: impl Fn(usize, &str) -> (u16, String, Option<u64>) + Send + 'static,
1895 ) -> (
1896 super::PacksetClient,
1897 std::sync::Arc<std::sync::Mutex<Vec<String>>>,
1898 ) {
1899 use std::io::{Read, Write};
1900 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1901 let addr = listener.local_addr().unwrap();
1902 let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1903 let log = std::sync::Arc::clone(&seen);
1904 std::thread::spawn(move || {
1905 for stream in listener.incoming() {
1906 let Ok(mut sock) = stream else { break };
1907 let _ = sock.set_read_timeout(Some(std::time::Duration::from_secs(2)));
1908 let mut buf = Vec::new();
1909 let mut tmp = [0u8; 2048];
1910 loop {
1911 match sock.read(&mut tmp) {
1912 Ok(0) | Err(_) => break,
1913 Ok(n) => {
1914 buf.extend_from_slice(&tmp[..n]);
1915 if buf.windows(4).any(|w| w == b"\r\n\r\n") {
1916 break;
1917 }
1918 }
1919 }
1920 }
1921 let req = String::from_utf8_lossy(&buf).into_owned();
1922 let n = {
1923 let mut log = log.lock().unwrap();
1924 log.push(req.clone());
1925 log.len() - 1
1926 };
1927 let (code, body, retry) = on(n, &req);
1928 let reason = match code {
1929 200 => "OK",
1930 408 => "Request Timeout",
1931 503 => "Service Unavailable",
1932 _ => "Error",
1933 };
1934 let retry = retry
1935 .map(|secs| format!("Retry-After: {secs}\r\n"))
1936 .unwrap_or_default();
1937 let resp = format!(
1938 "HTTP/1.1 {code} {reason}\r\n{retry}Content-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
1939 body.len()
1940 );
1941 let _ = sock.write_all(resp.as_bytes());
1942 }
1943 });
1944 (super::PacksetClient::new(format!("http://{addr}")), seen)
1945 }
1946
1947 fn deadline_of(req: &str) -> u64 {
1948 req.lines()
1949 .find_map(|line| {
1950 let (name, value) = line.split_once(':')?;
1951 name.eq_ignore_ascii_case("x-packset-deadline")
1952 .then(|| value.trim().parse().ok())
1953 .flatten()
1954 })
1955 .unwrap_or(0)
1956 }
1957
1958 fn ahead_ms(req: &str) -> u64 {
1959 let now = super::unix_ms(std::time::SystemTime::now());
1960 deadline_of(req).saturating_sub(now)
1961 }
1962
1963 #[test]
1964 fn an_mcp_read_deadline_is_about_a_second() {
1965 let _g = crate::tests::env_guard();
1966 let _home = quiet_pack();
1967 let _bound = crate::tool_bound::arm(std::time::Duration::from_secs(30));
1968 let (client, seen) = serve(|_, _| (200, "ok".into(), None));
1969 client.health().unwrap();
1970 let ahead = ahead_ms(&seen.lock().unwrap()[0]);
1971 assert!(
1972 (200..1_500).contains(&ahead),
1973 "mcp read deadline is {ahead} ms ahead"
1974 );
1975 }
1976
1977 #[test]
1980 fn a_raised_mcp_bound_keeps_the_pack_timeout_on_a_read() {
1981 let _g = crate::tests::env_guard();
1982 let _home = quiet_pack();
1983 unsafe {
1984 std::env::set_var("PACKSET_TIMEOUT_MS", "45000");
1985 std::env::set_var("LJOS_MCP_TIMEOUT", "60");
1986 }
1987 let _bound = crate::tool_bound::arm(std::time::Duration::from_secs(60));
1988 let (client, seen) = serve(|_, _| (200, r#"{"hits":[]}"#.into(), None));
1989 client
1990 .search_fetched("seat", "review set", 1, None, false)
1991 .unwrap();
1992 let ahead = ahead_ms(&seen.lock().unwrap()[0]);
1993 assert!(
1994 (40_000..50_000).contains(&ahead),
1995 "raised read deadline is {ahead} ms ahead"
1996 );
1997 }
1998
1999 #[test]
2000 fn a_one_second_clock_caps_the_read_deadline() {
2001 let _g = crate::tests::env_guard();
2002 let _home = quiet_pack();
2003 let _clock = super::PackClock::enter(std::time::Duration::from_secs(1));
2004 let (client, seen) = serve(|_, _| (200, "ok".into(), None));
2005 client.health().unwrap();
2006 let ahead = ahead_ms(&seen.lock().unwrap()[0]);
2007 assert!(
2008 (200..1_500).contains(&ahead),
2009 "clocked read deadline is {ahead} ms ahead"
2010 );
2011 }
2012
2013 #[test]
2014 fn a_200_carries_the_call_deadline() {
2015 let _g = crate::tests::env_guard();
2016 let _home = quiet_pack();
2017 let (client, seen) = serve(|_, req| {
2018 assert!(deadline_of(req) > 0, "{req}");
2019 (200, "ok".into(), None)
2020 });
2021 let body = client.health().unwrap();
2022 assert_eq!(body, "ok");
2023 assert_eq!(seen.lock().unwrap().len(), 1);
2024 let now = super::unix_ms(std::time::SystemTime::now());
2025 let stamp = deadline_of(&seen.lock().unwrap()[0]);
2026 assert!(stamp > now, "deadline {stamp} is not after {now}");
2027 assert!(
2028 stamp < now + 3_000,
2029 "health deadline {stamp} is not about two seconds past {now}"
2030 );
2031 }
2032
2033 #[test]
2034 fn a_503_waits_retry_after_once_and_then_reads() {
2035 let _g = crate::tests::env_guard();
2036 let _home = quiet_pack();
2037 let (client, seen) = serve(|n, req| {
2038 assert!(deadline_of(req) > 0, "{req}");
2039 if n == 0 {
2040 (
2041 503,
2042 r#"{"error":"packsetd is busy: the read queue is full"}"#.into(),
2043 Some(1),
2044 )
2045 } else {
2046 (200, "ok".into(), None)
2047 }
2048 });
2049 let started = std::time::Instant::now();
2050 let body = client.health().unwrap();
2051 let took = started.elapsed();
2052 assert_eq!(body, "ok");
2053 let got = seen.lock().unwrap().clone();
2054 assert_eq!(got.len(), 2);
2055 assert_eq!(deadline_of(&got[0]), deadline_of(&got[1]));
2056 assert!(
2057 took >= std::time::Duration::from_millis(900),
2058 "Retry-After was not waited: {took:?}"
2059 );
2060 assert!(took < std::time::Duration::from_secs(3), "{took:?}");
2061 }
2062
2063 #[test]
2064 fn a_second_503_is_the_error_line_and_stops() {
2065 let _g = crate::tests::env_guard();
2066 let _home = quiet_pack();
2067 let (client, seen) = serve(|_, _| {
2068 (
2069 503,
2070 r#"{"error":"packsetd is busy: the read queue is full"}"#.into(),
2071 Some(0),
2072 )
2073 });
2074 let started = std::time::Instant::now();
2075 let err = client.health().unwrap_err();
2076 let took = started.elapsed();
2077 assert_eq!(
2078 err.to_string(),
2079 "packsetd is busy: the read queue is full",
2080 "{err}"
2081 );
2082 let got = seen.lock().unwrap().clone();
2083 assert_eq!(got.len(), 2, "a 503 is resent once");
2084 assert_eq!(deadline_of(&got[0]), deadline_of(&got[1]));
2085 assert!(took < std::time::Duration::from_millis(500), "{took:?}");
2086 }
2087
2088 #[test]
2089 fn an_expired_deadline_is_the_error_line_and_is_not_retried() {
2090 let _g = crate::tests::env_guard();
2091 let _home = quiet_pack();
2092 let (client, seen) = serve(|_, req| {
2093 assert!(deadline_of(req) > 0, "{req}");
2094 (
2095 408,
2096 r#"{"error":"the deadline passed before this request ran"}"#.into(),
2097 None,
2098 )
2099 });
2100 let err = client.health().unwrap_err();
2101 assert_eq!(
2102 err.to_string(),
2103 "the deadline passed before this request ran",
2104 "{err}"
2105 );
2106 assert_eq!(seen.lock().unwrap().len(), 1);
2107 }
2108
2109 #[test]
2110 fn a_retry_after_past_the_deadline_does_not_wait() {
2111 let _g = crate::tests::env_guard();
2112 let _home = quiet_pack();
2113 let (client, seen) = serve(|_, _| (503, r#"{"error":"busy"}"#.into(), Some(30)));
2114 let started = std::time::Instant::now();
2115 let err = client.health().unwrap_err();
2116 let took = started.elapsed();
2117 assert_eq!(err.to_string(), "busy", "{err}");
2118 assert_eq!(seen.lock().unwrap().len(), 1, "the long wait was taken");
2119 assert!(took < std::time::Duration::from_millis(500), "{took:?}");
2120 }
2121
2122 #[test]
2123 fn stats_line_is_the_admission_line() {
2124 let _g = crate::tests::env_guard();
2125 let _home = quiet_pack();
2126 let (client, seen) = serve(|_, req| {
2127 let line = req.lines().next().unwrap_or("");
2128 if line.contains("GET /stats") {
2129 (200, r#"{"ok":true,"line":"busy, retrying"}"#.into(), None)
2130 } else {
2131 (200, r#"{"line":"ok"}"#.into(), None)
2132 }
2133 });
2134 assert_eq!(client.admission_line().as_deref(), Some("busy, retrying"));
2135 assert_eq!(seen.lock().unwrap().len(), 1);
2136 let (client, seen) = serve(|_, _| (503, r#"{"error":"busy"}"#.into(), Some(1)));
2137 assert_eq!(client.admission_line().as_deref(), Some("busy, retrying"));
2138 assert_eq!(
2139 seen.lock().unwrap().len(),
2140 1,
2141 "stats does not wait out Retry-After"
2142 );
2143 }
2144}