1use crate::error::*;
2use crate::host_identity::{HostConnectError, HostExpectation, HostProcess};
3use crate::private::PrivateSecret;
4use crate::record::{try_read_record, ServiceRecord, PROTOCOL_MAJOR};
5use crate::suite::{Suite, SuiteError};
6use crate::transport::{connect, connect_expecting, Conn};
7use rightkit_sidecar::{read_json_line, write_json_line, FrameError};
8use serde_json::{json, Value};
9use std::io::{BufReader, Read, Write};
10use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
11use std::sync::{mpsc, Arc, Mutex};
12use std::time::{Duration, Instant};
13
14const LIMIT: usize = 16 * 1024 * 1024;
15
16pub const DEFAULT_DEADLINE: Duration = Duration::from_secs(120);
19
20struct Watchdog {
24 fired: Arc<AtomicBool>,
25 _disarm: mpsc::Sender<()>,
26}
27
28impl Watchdog {
29 fn arm(after: Option<Duration>, shutdown: Arc<dyn Fn() + Send + Sync>) -> Watchdog {
30 let fired = Arc::new(AtomicBool::new(false));
31 let (tx, rx) = mpsc::channel::<()>();
32 if let Some(d) = after {
33 let f = fired.clone();
34 std::thread::spawn(move || {
35 if let Err(mpsc::RecvTimeoutError::Timeout) = rx.recv_timeout(d) {
36 f.store(true, Ordering::SeqCst);
37 shutdown();
38 }
39 });
40 }
41 Watchdog { fired, _disarm: tx }
42 }
43 fn fired(&self) -> bool {
44 self.fired.load(Ordering::SeqCst)
45 }
46}
47
48fn timed_out(what: &str, d: Option<Duration>) -> ServiceError {
49 ServiceError::new(
50 ERROR_TIMEOUT,
51 format!(
52 "{what} exceeded the {} ms deadline; connection aborted",
53 d.map(|d| d.as_millis()).unwrap_or(0)
54 ),
55 )
56}
57
58fn lock_until<T>(m: &Mutex<T>, deadline: Option<Instant>) -> Option<std::sync::MutexGuard<'_, T>> {
61 use std::sync::TryLockError;
62 let Some(deadline) = deadline else {
63 return Some(m.lock().unwrap_or_else(|e| e.into_inner()));
64 };
65 let mut pause = Duration::from_micros(200);
66 loop {
67 match m.try_lock() {
68 Ok(g) => return Some(g),
69 Err(TryLockError::Poisoned(e)) => return Some(e.into_inner()),
70 Err(TryLockError::WouldBlock) => {}
71 }
72 let left = deadline.saturating_duration_since(Instant::now());
73 if left.is_zero() {
74 return None;
75 }
76 std::thread::sleep(pause.min(left));
77 pause = (pause * 2).min(Duration::from_millis(5));
78 }
79}
80
81struct Io {
82 reader: BufReader<Box<dyn Read + Send>>,
83 writer: Box<dyn Write + Send>,
84 buf: Vec<u8>,
85}
86
87#[derive(Debug, Clone)]
106pub struct ClientConfig {
107 service: String,
108 app: String,
109 suite: Option<Suite>,
110 deadline: Option<Duration>,
111 ignore_env_overrides: bool,
112 secret: Option<PrivateSecret>,
113}
114
115impl ClientConfig {
116 pub fn new(service: impl Into<String>, app: impl Into<String>) -> Self {
120 Self {
121 service: service.into(),
122 app: app.into(),
123 suite: None,
124 deadline: Some(DEFAULT_DEADLINE),
125 ignore_env_overrides: false,
126 secret: None,
127 }
128 }
129
130 pub fn secret(mut self, secret: PrivateSecret) -> Self {
134 self.secret = Some(secret);
135 self
136 }
137
138 pub fn suite(mut self, suite: Suite) -> Self {
140 self.suite = Some(suite);
141 self
142 }
143
144 pub fn deadline(mut self, deadline: Option<Duration>) -> Self {
146 self.deadline = deadline;
147 self
148 }
149
150 pub fn ignore_env_overrides(mut self, ignore: bool) -> Self {
155 self.ignore_env_overrides = ignore;
156 self
157 }
158
159 pub fn service(&self) -> &str {
160 &self.service
161 }
162
163 pub fn app(&self) -> &str {
164 &self.app
165 }
166
167 pub fn ignores_env_overrides(&self) -> bool {
169 self.ignore_env_overrides
170 }
171
172 #[allow(deprecated)] pub fn resolve_suite(&self) -> Result<Suite, SuiteError> {
175 match (&self.suite, self.ignore_env_overrides) {
176 (Some(suite), true) if suite.env_derived() => Err(SuiteError::Insecure(format!(
177 "the suite {} was derived from the environment, which this client ignores; \
178 pass Suite::at(..) or leave the suite unset",
179 suite.root().display()
180 ))),
181 (Some(suite), _) => Ok(suite.clone()),
182 (None, ignore) => Suite::resolve_ignoring_env_overrides(ignore),
183 }
184 }
185}
186
187fn suite_error(e: SuiteError) -> ServiceError {
188 ServiceError::new(ERROR_SUITE_UNRESOLVED, e.to_string())
189}
190
191pub struct Client {
196 io: Mutex<Io>,
197 next_id: AtomicU64,
198 dead: AtomicBool,
199 hello: Value,
200 record: ServiceRecord,
201 shutdown: Arc<dyn Fn() + Send + Sync>,
202 deadline_ms: AtomicU64,
204 host: Option<HostProcess>,
206 secret: Option<PrivateSecret>,
208}
209
210fn with_secret(mut frame: Value, secret: Option<&PrivateSecret>) -> Value {
212 if let Some(secret) = secret {
213 frame["secret"] = Value::String(secret.expose().to_string());
214 }
215 frame
216}
217
218fn closed() -> ServiceError {
219 ServiceError::retryable(
220 ERROR_CONNECTION_CLOSED,
221 "connection closed before a response arrived",
222 )
223}
224
225impl Client {
226 pub fn connect(suite: &Suite, service: &str, app: &str) -> Result<Client, ServiceError> {
236 Self::connect_with(suite, service, app, Some(DEFAULT_DEADLINE))
237 }
238
239 pub fn connect_with(
243 suite: &Suite,
244 service: &str,
245 app: &str,
246 deadline: Option<Duration>,
247 ) -> Result<Client, ServiceError> {
248 Self::connect_bounded(suite, service, app, deadline, None, None)
249 .map_err(ConnectError::into_service_error)
250 }
251
252 pub fn connect_config(config: &ClientConfig) -> Result<Client, ServiceError> {
256 let suite = config.resolve_suite().map_err(suite_error)?;
257 Self::connect_bounded(
258 &suite,
259 &config.service,
260 &config.app,
261 config.deadline,
262 None,
263 config.secret.clone(),
264 )
265 .map_err(ConnectError::into_service_error)
266 }
267
268 pub fn connect_expecting(
291 config: &ClientConfig,
292 expect: &HostExpectation,
293 ) -> Result<Client, ConnectError> {
294 let suite = config
295 .resolve_suite()
296 .map_err(|e| ConnectError::Service(suite_error(e)))?;
297 Self::connect_bounded(
298 &suite,
299 &config.service,
300 &config.app,
301 config.deadline,
302 Some(expect.clone()),
303 config.secret.clone(),
304 )
305 }
306
307 fn connect_bounded(
311 suite: &Suite,
312 service: &str,
313 app: &str,
314 deadline: Option<Duration>,
315 expect: Option<HostExpectation>,
316 secret: Option<PrivateSecret>,
317 ) -> Result<Client, ConnectError> {
318 let Some(d) = deadline else {
319 return Self::handshake(suite, service, app, None, expect.as_ref(), secret);
320 };
321 let (tx, rx) = mpsc::channel();
322 let (s, sv, ap) = (suite.clone(), service.to_string(), app.to_string());
323 std::thread::spawn(move || {
324 let _ = tx.send(Self::handshake(
325 &s,
326 &sv,
327 &ap,
328 Some(d),
329 expect.as_ref(),
330 secret,
331 ));
332 });
333 match rx.recv_timeout(d + Duration::from_millis(250)) {
334 Ok(r) => r,
335 Err(_) => Err(ConnectError::Service(timed_out("connect", deadline))),
336 }
337 }
338
339 fn handshake(
340 suite: &Suite,
341 service: &str,
342 app: &str,
343 deadline: Option<Duration>,
344 expect: Option<&HostExpectation>,
345 secret: Option<PrivateSecret>,
346 ) -> Result<Client, ConnectError> {
347 let started = Instant::now();
348 let record = match try_read_record(suite, service) {
351 Ok(Some(record)) => record,
352 Ok(None) => {
353 return Err(ServiceError::retryable(
354 ERROR_UNAVAILABLE,
355 format!("no service.json for {service}"),
356 )
357 .into())
358 }
359 Err(e) => {
360 return Err(ServiceError::retryable(
361 ERROR_UNAVAILABLE,
362 format!("could not read service.json for {service} (try again): {e}"),
363 )
364 .into())
365 }
366 };
367 if record.protocol_major != PROTOCOL_MAJOR {
368 return Err(ServiceError::new(
369 "protocol_mismatch",
370 format!("host speaks protocol {}", record.protocol_major),
371 )
372 .into());
373 }
374 let unreachable_host = |e: std::io::Error| {
375 ServiceError::retryable(
376 ERROR_UNAVAILABLE,
377 format!("{service} host unreachable: {e}"),
378 )
379 };
380 let (conn, host) = match expect {
382 None => (connect(&record.endpoint).map_err(unreachable_host)?, None),
383 Some(expect) => match connect_expecting(&record.endpoint, expect) {
384 Ok((conn, host)) => (conn, Some(host)),
385 Err(HostConnectError::Io(e)) => return Err(unreachable_host(e).into()),
386 Err(HostConnectError::Identity(e)) => return Err(ConnectError::HostIdentity(e)),
387 },
388 };
389 let Conn {
390 reader,
391 writer,
392 shutdown,
393 ..
394 } = conn;
395 let mut io = Io {
396 reader: BufReader::new(reader),
397 writer,
398 buf: Vec::new(),
399 };
400 let wd = Watchdog::arm(
401 deadline.map(|d| d.saturating_sub(started.elapsed())),
402 shutdown.clone(),
403 );
404 let hello_res = (|| {
405 write_json_line(
406 &mut io.writer,
407 &with_secret(
408 json!({"type": "hello", "app": app, "protocol_major": PROTOCOL_MAJOR}),
409 secret.as_ref(),
410 ),
411 LIMIT,
412 )
413 .map_err(|e| ServiceError::new("io", e.to_string()))?;
414 io.writer.flush()?;
415 read_json_line::<_, Value>(&mut io.reader, &mut io.buf, LIMIT)
416 .map_err(|e| ServiceError::new("io", e.to_string()))?
417 .ok_or_else(closed)
418 })();
419 let hello = match hello_res {
420 Ok(h) => h,
421 Err(e) => {
422 let t = wd.fired();
423 (shutdown)();
424 let e = if t { timed_out("hello", deadline) } else { e };
425 return Err(e.into());
426 }
427 };
428 drop(wd);
429 if hello["ok"] != true {
430 return Err(ServiceError::from_value(&hello["error"]).into());
431 }
432 Ok(Client {
433 io: Mutex::new(io),
434 next_id: AtomicU64::new(1),
435 dead: AtomicBool::new(false),
436 hello,
437 record,
438 shutdown,
439 deadline_ms: AtomicU64::new(deadline.map(|d| d.as_millis() as u64).unwrap_or(0)),
440 host,
441 secret,
442 })
443 }
444
445 pub fn verified_host(&self) -> Option<&HostProcess> {
449 self.host.as_ref()
450 }
451
452 pub fn set_deadline(&self, deadline: Option<Duration>) {
454 self.deadline_ms.store(
455 deadline.map(|d| (d.as_millis() as u64).max(1)).unwrap_or(0),
456 Ordering::SeqCst,
457 );
458 }
459
460 pub fn record(&self) -> &ServiceRecord {
461 &self.record
462 }
463 pub fn hello(&self) -> &Value {
465 &self.hello
466 }
467
468 pub fn abort(&self) {
471 self.dead.store(true, Ordering::SeqCst);
472 (self.shutdown)();
473 }
474
475 pub fn call(&self, method: &str, params: Value) -> Result<Value, ServiceError> {
476 self.call_events(method, params, &mut |_| {})
477 }
478
479 pub fn call_events(
484 &self,
485 method: &str,
486 params: Value,
487 on_event: &mut dyn FnMut(&Value),
488 ) -> Result<Value, ServiceError> {
489 let deadline = match self.deadline_ms.load(Ordering::SeqCst) {
490 0 => None,
491 ms => Some(Instant::now() + Duration::from_millis(ms)),
492 };
493 self.call_events_until(method, params, deadline, on_event)
494 }
495
496 pub fn call_events_until(
501 &self,
502 method: &str,
503 params: Value,
504 deadline: Option<Instant>,
505 on_event: &mut dyn FnMut(&Value),
506 ) -> Result<Value, ServiceError> {
507 let mut frame = self.call_frame_until(method, params, false, deadline, on_event)?;
508 Ok(frame["result"].take())
509 }
510
511 fn call_frame_until(
514 &self,
515 method: &str,
516 params: Value,
517 chunked_ok: bool,
518 deadline: Option<Instant>,
519 on_event: &mut dyn FnMut(&Value),
520 ) -> Result<Value, ServiceError> {
521 let started = Instant::now();
522 let total = deadline.map(|d| d.saturating_duration_since(started));
523 if self.dead.load(Ordering::SeqCst) {
524 return Err(closed());
525 }
526 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
527 let Some(mut io) = lock_until(&self.io, deadline) else {
528 return Err(timed_out(
529 &format!("request `{method}` waiting for the connection"),
530 total,
531 ));
532 };
533 if self.dead.load(Ordering::SeqCst) {
534 return Err(closed());
535 }
536 let wd = Watchdog::arm(
537 deadline.map(|d| d.saturating_duration_since(Instant::now())),
538 self.shutdown.clone(),
539 );
540 let res = (|| {
541 let mut frame =
542 json!({"type": "request", "id": id, "method": method, "params": params});
543 if chunked_ok {
544 frame["chunked_ok"] = Value::Bool(true);
545 }
546 write_json_line(
547 &mut io.writer,
548 &with_secret(frame, self.secret.as_ref()),
549 LIMIT,
550 )
551 .map_err(|e| match e {
552 FrameError::TooLarge { .. } => ServiceError::new(
553 ERROR_REQUEST_TOO_LARGE,
554 "request exceeds the frame limit (Client::call_large sends it in pieces)",
555 ),
556 _ => closed(),
557 })?;
558 io.writer.flush().map_err(|_| closed())?;
559 loop {
560 let Io { reader, buf, .. } = &mut *io;
561 match read_json_line::<_, Value>(reader, buf, LIMIT) {
562 Ok(Some(v)) if v["id"].as_u64() == Some(id) => {
563 if v["type"] == "event" {
564 on_event(&v["event"]);
565 } else if v["type"] == "response" {
566 return if v["ok"] == true {
567 Ok(v)
568 } else {
569 Err(ServiceError::from_value(&v["error"]))
570 };
571 }
572 }
573 Ok(Some(_)) => {} Ok(None) | Err(FrameError::Io(_)) => return Err(closed()),
575 Err(FrameError::TooLarge { .. }) => {
579 return Err(ServiceError::new(
580 ERROR_RESPONSE_TOO_LARGE,
581 "the host's reply exceeds the client's frame limit",
582 ));
583 }
584 Err(_) => {}
585 }
586 }
587 })();
588 if wd.fired() && res.is_err() {
589 self.dead.store(true, Ordering::SeqCst);
590 return Err(timed_out(&format!("request `{method}`"), total));
591 }
592 if matches!(&res, Err(e) if e.code == ERROR_CONNECTION_CLOSED) {
593 self.dead.store(true, Ordering::SeqCst);
594 }
595 res
596 }
597}
598
599impl Client {
600 fn call_step(
602 &self,
603 method: &str,
604 params: Value,
605 chunked_ok: bool,
606 on_event: &mut dyn FnMut(&Value),
607 ) -> Result<Value, ServiceError> {
608 let deadline = match self.deadline_ms.load(Ordering::SeqCst) {
609 0 => None,
610 ms => Some(Instant::now() + Duration::from_millis(ms)),
611 };
612 self.call_frame_until(method, params, chunked_ok, deadline, on_event)
613 }
614
615 pub fn call_large(
636 &self,
637 method: &str,
638 params: Value,
639 on_event: &mut dyn FnMut(&Value),
640 ) -> Result<Value, ServiceError> {
641 use crate::chunk::{boundaries, CHUNK_TEXT_BYTES, METHOD_FETCH, METHOD_UPLOAD};
642 let advertised = |key: &str| {
643 self.hello[key]
644 .as_u64()
645 .and_then(|n| usize::try_from(n).ok())
646 };
647 let max_message = advertised("max_message").unwrap_or(0);
648 let frame_limit = advertised("frame_limit").unwrap_or(LIMIT).min(LIMIT);
649 let chunk = advertised("chunk_bytes")
650 .unwrap_or(CHUNK_TEXT_BYTES)
651 .clamp(16, CHUNK_TEXT_BYTES);
652 let text = serde_json::to_string(¶ms)
653 .map_err(|e| ServiceError::new("bad_request", e.to_string()))?;
654 let single_max = if max_message == 0 {
655 frame_limit - (64 * 1024).min(frame_limit / 2)
659 } else {
660 frame_limit / 2
661 };
662 let mut frame = if text.len() <= single_max {
663 drop(text);
664 self.call_step(method, params, max_message > 0, &mut *on_event)?
665 } else {
666 if max_message == 0 || text.len() > max_message {
667 return Err(ServiceError::new(
668 ERROR_REQUEST_TOO_LARGE,
669 if max_message == 0 {
670 "the request exceeds the frame limit and the host does not chunk"
671 .to_string()
672 } else {
673 format!(
674 "the request is {} bytes, over the host's {max_message}-byte message limit",
675 text.len()
676 )
677 },
678 ));
679 }
680 drop(params);
681 let ends = boundaries(&text, chunk);
682 let upload_id = format!("u{}", self.next_id.fetch_add(1, Ordering::SeqCst));
683 let mut start = 0;
684 let mut answer = Value::Null;
685 for (seq, end) in ends.iter().enumerate() {
686 let last = seq + 1 == ends.len();
687 let mut piece = json!({
688 "id": upload_id, "seq": seq, "last": last, "text": &text[start..*end],
689 });
690 if seq == 0 {
691 piece["method"] = Value::String(method.to_string());
692 }
693 start = *end;
694 let frame = self.call_step(METHOD_UPLOAD, piece, last, &mut *on_event)?;
695 if last {
696 answer = frame;
697 }
698 }
699 answer
700 };
701 let held = frame.get_mut("chunked").map(Value::take);
702 let Some(held) = held else {
703 return Ok(frame["result"].take());
704 };
705 let bad = |why: &str| ServiceError::new(ERROR_BAD_CHUNK, why.to_string());
707 let id = held["id"]
708 .as_str()
709 .ok_or_else(|| bad("the chunked result has no id"))?
710 .to_string();
711 let bytes = held["bytes"]
712 .as_u64()
713 .and_then(|n| usize::try_from(n).ok())
714 .ok_or_else(|| bad("the chunked result has no size"))?;
715 let count = held["count"]
716 .as_u64()
717 .ok_or_else(|| bad("the chunked result has no count"))?;
718 if bytes > max_message || count > bytes.max(1) as u64 {
719 return Err(bad(
720 "the chunked result is larger than the host's own message limit",
721 ));
722 }
723 let mut joined = String::with_capacity(bytes);
724 for seq in 0..count {
725 let request = json!({"id": id, "seq": seq});
726 let piece = self.call_step(METHOD_FETCH, request, false, &mut *on_event)?;
727 let part = piece["result"]["text"]
728 .as_str()
729 .ok_or_else(|| bad("a result piece has no text"))?;
730 if joined.len() + part.len() > bytes {
731 return Err(bad("the result pieces add up to more than announced"));
732 }
733 joined.push_str(part);
734 }
735 if joined.len() != bytes {
736 return Err(bad("the result pieces add up to less than announced"));
737 }
738 serde_json::from_str(&joined)
739 .map_err(|e| bad(&format!("the assembled result is not JSON: {e}")))
740 }
741}
742
743#[derive(Debug, Clone, Copy)]
745pub struct RetryPolicy {
746 pub max_attempts: u32,
747 pub backoff: Duration,
748 pub give_up_after: Duration,
751 pub request_deadline: Option<Duration>,
753 pub idempotent: bool,
759}
760
761impl Default for RetryPolicy {
762 fn default() -> Self {
763 Self {
764 max_attempts: 8,
765 backoff: Duration::from_millis(150),
766 give_up_after: Duration::from_secs(120),
767 request_deadline: Some(DEFAULT_DEADLINE),
768 idempotent: false,
769 }
770 }
771}
772
773fn run_attempt(
776 c: &Client,
777 method: &str,
778 params: &Value,
779 stop: Option<&(dyn Fn() -> bool + Sync)>,
780 on_event: &mut dyn FnMut(&Value),
781) -> Result<Value, ServiceError> {
782 let Some(stop) = stop else {
783 return c.call_large(method, params.clone(), on_event);
784 };
785 let done = AtomicBool::new(false);
786 std::thread::scope(|scope| {
787 scope.spawn(|| {
788 while !done.load(Ordering::SeqCst) {
789 if stop() {
790 c.abort();
791 break;
792 }
793 std::thread::sleep(Duration::from_millis(25));
794 }
795 });
796 let result = c.call_large(method, params.clone(), on_event);
797 done.store(true, Ordering::SeqCst);
798 result
799 })
800}
801
802fn sleep_unless_stopped(d: Duration, stop: Option<&(dyn Fn() -> bool + Sync)>) {
805 let Some(stop) = stop else {
806 std::thread::sleep(d);
807 return;
808 };
809 let end = Instant::now() + d;
810 loop {
811 if stop() {
812 return;
813 }
814 let now = Instant::now();
815 if now >= end {
816 return;
817 }
818 std::thread::sleep((end - now).min(Duration::from_millis(25)));
819 }
820}
821
822impl Client {
823 pub fn call_retrying(
827 suite: &Suite,
828 service: &str,
829 app: &str,
830 method: &str,
831 params: Value,
832 policy: RetryPolicy,
833 on_event: &mut dyn FnMut(&Value),
834 ) -> Result<Value, ServiceError> {
835 Self::retry_loop(
836 &|deadline| Self::connect_bounded(suite, service, app, deadline, None, None),
837 method,
838 params,
839 policy,
840 None,
841 on_event,
842 )
843 }
844
845 pub fn call_retrying_expecting(
851 config: &ClientConfig,
852 expect: &HostExpectation,
853 method: &str,
854 params: Value,
855 policy: RetryPolicy,
856 on_event: &mut dyn FnMut(&Value),
857 ) -> Result<Value, ServiceError> {
858 let suite = config.resolve_suite().map_err(suite_error)?;
859 Self::retry_loop(
860 &|deadline| {
861 Self::connect_bounded(
862 &suite,
863 &config.service,
864 &config.app,
865 deadline,
866 Some(expect.clone()),
867 config.secret.clone(),
868 )
869 },
870 method,
871 params,
872 policy,
873 None,
874 on_event,
875 )
876 }
877
878 pub fn call_retrying_over<F>(
885 connect: F,
886 method: &str,
887 params: Value,
888 policy: RetryPolicy,
889 on_event: &mut dyn FnMut(&Value),
890 ) -> Result<Value, ServiceError>
891 where
892 F: Fn(Option<Duration>) -> Result<Client, ConnectError>,
893 {
894 Self::retry_loop(&connect, method, params, policy, None, on_event)
895 }
896
897 pub fn call_retrying_until<F>(
904 connect: F,
905 method: &str,
906 params: Value,
907 policy: RetryPolicy,
908 stop: &(dyn Fn() -> bool + Sync),
909 on_event: &mut dyn FnMut(&Value),
910 ) -> Result<Value, ServiceError>
911 where
912 F: Fn(Option<Duration>) -> Result<Client, ConnectError>,
913 {
914 Self::retry_loop(&connect, method, params, policy, Some(stop), on_event)
915 }
916
917 fn retry_loop(
918 connect: &dyn Fn(Option<Duration>) -> Result<Client, ConnectError>,
919 method: &str,
920 params: Value,
921 policy: RetryPolicy,
922 stop: Option<&(dyn Fn() -> bool + Sync)>,
923 on_event: &mut dyn FnMut(&Value),
924 ) -> Result<Value, ServiceError> {
925 let started = Instant::now();
926 let stopped = || stop.is_some_and(|f| f());
927 let mut last = ServiceError::retryable(ERROR_UNAVAILABLE, "no attempt made");
928 for attempt in 0..policy.max_attempts.max(1) {
929 if attempt > 0 {
930 if stopped() || started.elapsed() > policy.give_up_after {
931 break;
932 }
933 sleep_unless_stopped(policy.backoff * attempt.min(8), stop);
934 if stopped() {
935 break;
936 }
937 }
938 let c = match connect(policy.request_deadline) {
939 Ok(c) => c,
940 Err(ConnectError::Service(e)) if e.retryable => {
941 last = e; continue;
943 }
944 Err(e) => return Err(e.into_service_error()),
945 };
946 match run_attempt(&c, method, ¶ms, stop, on_event) {
947 Ok(v) => return Ok(v),
948 Err(e)
951 if (e.retryable && (e.code == ERROR_HOST_HANDOFF || policy.idempotent))
952 || (e.code == ERROR_TIMEOUT && policy.idempotent) =>
953 {
954 last = e
955 }
956 Err(e) => return Err(e),
957 }
958 }
959 Err(last)
960 }
961}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966
967 #[test]
968 fn defaults_are_the_unhardened_resolve_with_the_default_deadline() {
969 let cfg = ClientConfig::new("svc", "app");
970 assert_eq!((cfg.service(), cfg.app()), ("svc", "app"));
971 assert!(!cfg.ignores_env_overrides());
972 assert_eq!(cfg.deadline, Some(DEFAULT_DEADLINE));
973 }
974
975 #[test]
976 fn an_explicit_suite_is_used_as_is_unless_it_came_from_the_environment_and_the_switch_is_on() {
977 let dir = tempfile::tempdir().unwrap();
978 let explicit = Suite::at(dir.path()).unwrap();
979 for ignore in [false, true] {
980 let cfg = ClientConfig::new("svc", "app")
981 .suite(explicit.clone())
982 .ignore_env_overrides(ignore);
983 assert_eq!(cfg.resolve_suite().unwrap().root(), explicit.root());
984 }
985 #[cfg(feature = "test-mode")]
987 if let Ok(from_env) = Suite::resolve() {
988 let lenient = ClientConfig::new("svc", "app").suite(from_env);
989 assert!(lenient.resolve_suite().is_ok());
990 let strict = lenient.ignore_env_overrides(true);
991 assert!(matches!(
992 strict.resolve_suite(),
993 Err(SuiteError::Insecure(_))
994 ));
995 }
996 }
997
998 #[test]
999 fn a_missing_record_is_a_retryable_service_error_not_an_identity_error() {
1000 let dir = tempfile::tempdir().unwrap();
1001 let cfg = ClientConfig::new("nobody-home", "app").suite(Suite::at(dir.path()).unwrap());
1002 let err = Client::connect_expecting(&cfg, &HostExpectation::executable("/x"))
1003 .err()
1004 .expect("there is no host");
1005 match err {
1006 ConnectError::Service(e) => {
1007 assert_eq!(e.code, ERROR_UNAVAILABLE);
1008 assert!(e.retryable);
1009 }
1010 other => panic!("expected a service error, got {other:?}"),
1011 }
1012 }
1013}