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