1use std::error::Error as _;
9use std::sync::Arc;
10use std::time::{Duration, Instant};
11
12use serde::{Deserialize, Serialize};
13use tokio::sync::mpsc;
14use tokio::sync::Mutex as AsyncMutex;
15use tokio_stream::wrappers::ReceiverStream;
16use tonic::metadata::{AsciiMetadataKey, AsciiMetadataValue};
17use tonic::transport::Channel;
18use tonic::{Code, Request, Status};
19
20use crate::channels::ChannelCache;
21use crate::error::SailError;
22use crate::pb::workerproxy::v1 as pb;
23use pb::worker_proxy_service_client::WorkerProxyServiceClient;
24
25const FILE_CHANNEL_CAP: usize = 4;
28
29pub const FILE_WRITE_CHUNK_BYTES: usize = 1 << 20;
33
34pub(crate) const EXEC_TRANSIENT_RETRY_INITIAL_DELAY_SECONDS: f64 = 0.2;
37pub(crate) const EXEC_TRANSIENT_RETRY_MAX_DELAY_SECONDS: f64 = 2.0;
39pub(crate) const EXEC_RPC_ATTEMPT_TIMEOUT_SECONDS: f64 = 10.0;
43
44pub(crate) const TRANSIENT_TRANSPORT_FRAGMENTS: &[&str] = &[
48 "endpoint closing",
49 "error reading server preface",
50 "connection reset",
51 "socket closed",
52 "transport is closing",
53 "h2 protocol error",
54 "keep-alive timed out",
55];
56
57pub(crate) fn is_transient_transport_message(message: &str) -> bool {
60 let details = message.to_lowercase();
61 TRANSIENT_TRANSPORT_FRAGMENTS
62 .iter()
63 .any(|fragment| details.contains(fragment))
64}
65
66#[doc(hidden)]
71#[derive(Debug, Clone)]
72#[non_exhaustive]
73pub struct WaitOutcome {
74 pub status: i32,
76 pub stdout: String,
78 pub stderr: String,
80 pub exit_code: i32,
82 pub timed_out: bool,
84 pub stdout_truncated: bool,
87 pub stderr_truncated: bool,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
96#[non_exhaustive]
97pub struct Listener {
98 pub guest_port: u32,
100 pub protocol: crate::sailbox::types::ListenerProtocol,
102 pub route_status: crate::sailbox::types::ListenerRouteStatus,
104 #[serde(default)]
106 pub public_url: String,
107 #[serde(default)]
109 pub public_host: String,
110 #[serde(default)]
112 pub public_port: u32,
113}
114
115impl Listener {
116 pub fn endpoint(&self) -> Option<crate::sailbox::types::ListenerEndpoint> {
118 use crate::sailbox::types::ListenerEndpoint;
119 if !self.public_url.is_empty() {
120 return Some(ListenerEndpoint::Http {
121 url: self.public_url.clone(),
122 });
123 }
124 if !self.public_host.is_empty() && self.public_port != 0 {
125 return Some(ListenerEndpoint::Tcp {
126 host: self.public_host.clone(),
127 port: self.public_port,
128 });
129 }
130 None
131 }
132
133 pub fn is_active(&self) -> bool {
135 self.route_status == crate::sailbox::types::ListenerRouteStatus::Active
136 }
137}
138
139#[derive(Debug, Clone)]
144pub struct WriteOptions {
145 pub create_parents: bool,
147 pub mode: Option<u32>,
149 pub user: Option<String>,
160}
161
162impl Default for WriteOptions {
163 fn default() -> WriteOptions {
165 WriteOptions {
166 create_parents: true,
167 mode: None,
168 user: None,
169 }
170 }
171}
172
173pub struct FileReader {
179 rx: AsyncMutex<mpsc::Receiver<Result<Vec<u8>, SailError>>>,
180 abort: tokio::task::AbortHandle,
181 closed: std::sync::atomic::AtomicBool,
182}
183
184impl FileReader {
185 pub async fn next(&self) -> Option<Result<Vec<u8>, SailError>> {
188 if self.closed.load(std::sync::atomic::Ordering::Relaxed) {
189 return None;
190 }
191 self.rx.lock().await.recv().await
192 }
193
194 pub fn close(&self) {
199 self.closed
200 .store(true, std::sync::atomic::Ordering::Relaxed);
201 self.abort.abort();
202 }
203
204 pub fn into_stream(self) -> futures::stream::BoxStream<'static, Result<Vec<u8>, SailError>> {
208 Box::pin(futures::stream::unfold(self, |reader| async move {
209 reader.next().await.map(|chunk| (chunk, reader))
210 }))
211 }
212}
213
214impl std::fmt::Debug for FileReader {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 f.debug_struct("FileReader")
217 .field(
218 "closed",
219 &self.closed.load(std::sync::atomic::Ordering::Relaxed),
220 )
221 .finish_non_exhaustive()
222 }
223}
224
225impl Drop for FileReader {
226 fn drop(&mut self) {
227 self.abort.abort();
230 }
231}
232
233fn override_reason_metadata(reason: String) -> Result<AsciiMetadataValue, SailError> {
240 crate::http::validate_override_reason(&reason)?;
241 AsciiMetadataValue::try_from(reason).map_err(|_| SailError::Config {
242 message: "SAIL_OWNER_OVERRIDE_REASON must be printable ASCII (no control or non-ASCII characters)"
243 .to_string(),
244 })
245}
246
247#[doc(hidden)]
251pub struct WorkerProxy {
252 channels: ChannelCache,
253 authorization: AsciiMetadataValue,
254}
255
256pub(crate) fn retry_deadline(retry_timeout: f64) -> Instant {
261 let now = Instant::now();
262 if retry_timeout <= 0.0 {
263 return now;
264 }
265 now + Duration::from_secs_f64(retry_timeout.min(RETRY_FOREVER_SECS))
270}
271
272const RETRY_FOREVER_SECS: f64 = 100.0 * 365.0 * 24.0 * 60.0 * 60.0;
275
276pub(crate) fn rpc_attempt_timeout(deadline: Instant) -> Duration {
282 (deadline.saturating_duration_since(Instant::now()) / 2)
283 .min(Duration::from_secs_f64(EXEC_RPC_ATTEMPT_TIMEOUT_SECONDS))
284 .max(Duration::from_millis(1))
285}
286
287pub(crate) fn is_workerproxy_draining(status: &Status) -> bool {
288 status.code() == Code::Unavailable && status.message().to_lowercase().contains("draining")
289}
290
291pub(crate) fn should_invalidate_channel(status: &Status) -> bool {
300 is_workerproxy_draining(status)
301 || status.code() == Code::DeadlineExceeded
302 || status.source().is_some()
303}
304
305pub(crate) fn should_retry_transient_exec_rpc(status: &Status, deadline: Instant) -> bool {
306 if Instant::now() >= deadline {
307 return false;
308 }
309 match status.code() {
310 Code::Unavailable | Code::DeadlineExceeded => true,
311 Code::Cancelled => status.source().is_some(),
316 Code::Unknown | Code::Internal => {
326 status.source().is_some() || is_transient_transport_message(status.message())
327 }
328 _ => status.source().is_some(),
335 }
336}
337
338pub(crate) async fn sleep_before_retry(delay: f64, deadline: Instant) -> f64 {
341 let mut sleep_for = delay.min(EXEC_TRANSIENT_RETRY_MAX_DELAY_SECONDS);
342 let remaining = deadline
343 .saturating_duration_since(Instant::now())
344 .as_secs_f64();
345 if remaining <= 0.0 {
346 return delay;
347 }
348 sleep_for = sleep_for.min(remaining);
349 tokio::time::sleep(Duration::from_secs_f64(sleep_for.max(0.0))).await;
350 (delay * 2.0).min(EXEC_TRANSIENT_RETRY_MAX_DELAY_SECONDS)
351}
352
353pub(crate) fn bearer_metadata(api_key: &str) -> Result<AsciiMetadataValue, SailError> {
357 format!("Bearer {api_key}")
358 .parse()
359 .map_err(|_| SailError::Config {
360 message: "SAIL_API_KEY contains characters invalid in a gRPC metadata value"
361 .to_string(),
362 })
363}
364
365impl WorkerProxy {
366 pub fn new(api_key: &str) -> Result<WorkerProxy, SailError> {
369 let authorization = bearer_metadata(api_key)?;
370 Ok(WorkerProxy {
371 channels: ChannelCache::new(),
372 authorization,
373 })
374 }
375
376 pub(crate) fn channels(&self) -> &ChannelCache {
377 &self.channels
378 }
379
380 pub(crate) fn client_for(
381 &self,
382 endpoint: &str,
383 ) -> Result<WorkerProxyServiceClient<Channel>, SailError> {
384 let channel = self.channels.get(endpoint)?;
385 Ok(WorkerProxyServiceClient::new(channel))
386 }
387
388 pub(crate) fn request_for<T>(
389 &self,
390 message: T,
391 extra_metadata: &[(String, String)],
392 timeout: Option<Duration>,
393 ) -> Result<Request<T>, SailError> {
394 let mut request = Request::new(message);
395 request
396 .metadata_mut()
397 .insert("authorization", self.authorization.clone());
398 if let Some(reason) = crate::http::owner_override_reason() {
402 request.metadata_mut().insert(
403 "x-sail-owner-override-reason",
404 override_reason_metadata(reason)?,
405 );
406 }
407 for (key, value) in extra_metadata {
408 let key: AsciiMetadataKey = key.parse().map_err(|_| SailError::Config {
409 message: format!("invalid gRPC metadata key {key:?}"),
410 })?;
411 let value: AsciiMetadataValue = value.parse().map_err(|_| SailError::Config {
412 message: format!("invalid gRPC metadata value for key {key:?}"),
413 })?;
414 request.metadata_mut().insert(key, value);
415 }
416 if let Some(timeout) = timeout {
417 request.set_timeout(timeout);
418 }
419 Ok(request)
420 }
421
422 pub async fn wait_exec(
427 &self,
428 endpoint: &str,
429 sailbox_id: &str,
430 exec_request_id: &str,
431 retry_timeout: f64,
432 ) -> Result<WaitOutcome, SailError> {
433 let mut deadline: Option<Instant> = None;
434 let mut delay = EXEC_TRANSIENT_RETRY_INITIAL_DELAY_SECONDS;
435 loop {
436 let message = pb::WaitSailboxExecRequest {
437 sailbox_id: sailbox_id.to_string(),
438 exec_request_id: exec_request_id.to_string(),
439 };
440 let request = self.request_for(message, &[], None)?;
441 match self.client_for(endpoint)?.wait_sailbox_exec(request).await {
442 Ok(resp) => {
443 let resp = resp.into_inner();
444 return Ok(WaitOutcome {
445 status: resp.status,
446 stdout: resp.stdout,
447 stderr: resp.stderr,
448 exit_code: resp.return_code,
449 timed_out: resp.timed_out,
450 stdout_truncated: resp.stdout_truncated,
451 stderr_truncated: resp.stderr_truncated,
452 });
453 }
454 Err(status) => {
455 let deadline = *deadline.get_or_insert_with(|| retry_deadline(retry_timeout));
456 if !should_retry_transient_exec_rpc(&status, deadline) {
457 return Err(SailError::from_exec_status(&status));
458 }
459 tracing::warn!(code = ?status.code(), endpoint, "retrying transient worker-proxy RPC");
460 if should_invalidate_channel(&status) {
461 self.channels.invalidate(endpoint);
462 }
463 delay = sleep_before_retry(delay, deadline).await;
464 }
465 }
466 }
467 }
468
469 pub async fn cancel_exec(
474 &self,
475 endpoint: &str,
476 sailbox_id: &str,
477 exec_request_id: &str,
478 force: bool,
479 retry_timeout: f64,
480 ) -> Result<(), SailError> {
481 let deadline = retry_deadline(retry_timeout);
482 let mut delay = EXEC_TRANSIENT_RETRY_INITIAL_DELAY_SECONDS;
483 loop {
484 let per_attempt_timeout = if retry_timeout > 0.0 {
485 Some(rpc_attempt_timeout(deadline))
490 } else {
491 None
492 };
493 let message = pb::CancelSailboxExecRequest {
494 sailbox_id: sailbox_id.to_string(),
495 exec_request_id: exec_request_id.to_string(),
496 force,
497 };
498 let request = self.request_for(message, &[], per_attempt_timeout)?;
499 match self
500 .client_for(endpoint)?
501 .cancel_sailbox_exec(request)
502 .await
503 {
504 Ok(_) => return Ok(()),
505 Err(status) => {
506 if !should_retry_transient_exec_rpc(&status, deadline) {
507 return Err(SailError::from_exec_status(&status));
508 }
509 tracing::warn!(code = ?status.code(), endpoint, "retrying transient worker-proxy RPC");
510 if should_invalidate_channel(&status) {
511 self.channels.invalidate(endpoint);
512 }
513 delay = sleep_before_retry(delay, deadline).await;
514 }
515 }
516 }
517 }
518
519 pub fn read_file(self: &Arc<Self>, endpoint: &str, sailbox_id: &str, path: &str) -> FileReader {
528 let (tx, rx) = mpsc::channel(FILE_CHANNEL_CAP);
529 let worker = Arc::clone(self);
530 let endpoint = endpoint.to_string();
531 let message = pb::ReadSailboxFileRequest {
532 sailbox_id: sailbox_id.to_string(),
533 path: path.to_string(),
534 };
535 let task = tokio::spawn(async move {
536 let request = match worker.request_for(message, &[], None) {
537 Ok(request) => request,
538 Err(err) => {
539 let _ = tx.send(Err(err)).await;
540 return;
541 }
542 };
543 let mut client = match worker.client_for(&endpoint) {
544 Ok(client) => client,
545 Err(err) => {
546 let _ = tx.send(Err(err)).await;
547 return;
548 }
549 };
550 let mut stream = match client.read_sailbox_file(request).await {
551 Ok(resp) => resp.into_inner(),
552 Err(status) => {
553 let _ = tx.send(Err(SailError::from_file_rpc_status(&status))).await;
554 return;
555 }
556 };
557 loop {
558 match stream.message().await {
559 Ok(Some(resp)) => {
560 if !resp.data.is_empty() && tx.send(Ok(resp.data)).await.is_err() {
561 return; }
563 }
564 Ok(None) => return,
565 Err(status) => {
566 let _ = tx.send(Err(SailError::from_file_rpc_status(&status))).await;
567 return;
568 }
569 }
570 }
571 });
572 FileReader {
573 rx: AsyncMutex::new(rx),
574 abort: task.abort_handle(),
575 closed: std::sync::atomic::AtomicBool::new(false),
576 }
577 }
578
579 pub fn write_file(
589 self: &Arc<Self>,
590 endpoint: &str,
591 sailbox_id: &str,
592 path: &str,
593 create_parents: bool,
594 mode: Option<u32>,
595 user: Option<String>,
596 ) -> FileWriter {
597 let (tx, rx) = mpsc::channel(FILE_CHANNEL_CAP);
598 let worker = Arc::clone(self);
599 let endpoint = endpoint.to_string();
600 let task = tokio::spawn(async move {
603 let request =
604 worker.request_for(ReceiverStream::new(rx), &[], None)?;
605 worker
606 .client_for(&endpoint)?
607 .write_sailbox_file(request)
608 .await
609 .map(|_| ())
610 .map_err(|status| SailError::from_file_rpc_status(&status))
611 });
612 FileWriter {
613 tx: Some(tx),
614 task: Some(task),
615 aborted: Arc::new(std::sync::atomic::AtomicBool::new(false)),
616 first: true,
617 sailbox_id: sailbox_id.to_string(),
618 path: path.to_string(),
619 create_parents,
620 mode,
621 user: user.unwrap_or_default(),
622 }
623 }
624}
625
626#[must_use = "dropping a FileWriter aborts the upload; call finish() to commit it"]
634pub struct FileWriter {
635 tx: Option<mpsc::Sender<pb::WriteSailboxFileRequest>>,
636 task: Option<tokio::task::JoinHandle<Result<(), SailError>>>,
637 aborted: Arc<std::sync::atomic::AtomicBool>,
638 first: bool,
639 sailbox_id: String,
640 path: String,
641 create_parents: bool,
642 mode: Option<u32>,
643 user: String,
644}
645
646impl std::fmt::Debug for FileWriter {
647 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
648 f.debug_struct("FileWriter")
649 .field("sailbox_id", &self.sailbox_id)
650 .field("path", &self.path)
651 .field(
652 "aborted",
653 &self.aborted.load(std::sync::atomic::Ordering::Relaxed),
654 )
655 .finish_non_exhaustive()
656 }
657}
658
659impl FileWriter {
660 fn build(&mut self, data: Vec<u8>) -> pb::WriteSailboxFileRequest {
661 let header = self.first;
662 self.first = false;
663 pb::WriteSailboxFileRequest {
664 sailbox_id: if header {
665 self.sailbox_id.clone()
666 } else {
667 String::new()
668 },
669 path: if header {
670 self.path.clone()
671 } else {
672 String::new()
673 },
674 data,
675 create_parents: header && self.create_parents,
676 mode: if header { self.mode } else { None },
677 user: if header {
678 self.user.clone()
679 } else {
680 String::new()
681 },
682 }
683 }
684
685 async fn join(&mut self) -> Result<(), SailError> {
687 self.tx = None; match self.task.take() {
689 Some(task) => task.await.unwrap_or_else(|join_err| {
690 Err(SailError::Internal {
691 message: format!("file write task failed: {join_err}"),
692 })
693 }),
694 None => Ok(()),
695 }
696 }
697
698 pub async fn write(&mut self, data: &[u8]) -> Result<(), SailError> {
702 for chunk in data.chunks(FILE_WRITE_CHUNK_BYTES) {
703 self.write_chunk(chunk.to_vec()).await?;
704 }
705 Ok(())
706 }
707
708 pub async fn write_chunk(&mut self, data: Vec<u8>) -> Result<(), SailError> {
712 if self.aborted.load(std::sync::atomic::Ordering::Relaxed) {
713 return Err(aborted_write());
714 }
715 let request = self.build(data);
716 match &self.tx {
717 Some(tx) if tx.send(request).await.is_ok() => Ok(()),
719 _ => self.join().await,
720 }
721 }
722
723 pub async fn finish(&mut self) -> Result<(), SailError> {
726 if self.aborted.load(std::sync::atomic::Ordering::Relaxed) {
727 return Err(aborted_write());
728 }
729 if self.first {
730 let request = self.build(Vec::new());
733 if let Some(tx) = &self.tx {
734 let _ = tx.send(request).await;
735 }
736 }
737 self.join().await
738 }
739
740 pub fn abort(&mut self) {
744 if let Some(task) = self.task.take() {
748 self.aborted
749 .store(true, std::sync::atomic::Ordering::Relaxed);
750 task.abort();
751 }
752 self.tx = None;
753 }
754
755 pub fn abort_handle(&self) -> WriteAbortHandle {
759 WriteAbortHandle {
760 aborted: Arc::clone(&self.aborted),
761 task: self
762 .task
763 .as_ref()
764 .map(tokio::task::JoinHandle::abort_handle),
765 }
766 }
767}
768
769#[derive(Debug, Clone)]
773pub struct WriteAbortHandle {
774 aborted: Arc<std::sync::atomic::AtomicBool>,
775 task: Option<tokio::task::AbortHandle>,
776}
777
778impl WriteAbortHandle {
779 pub fn abort(&self) {
782 self.aborted
783 .store(true, std::sync::atomic::Ordering::Relaxed);
784 if let Some(task) = &self.task {
785 task.abort();
786 }
787 }
788}
789
790fn aborted_write() -> SailError {
791 SailError::InvalidArgument {
792 message: "the write was aborted; nothing was committed".to_string(),
793 }
794}
795
796impl Drop for FileWriter {
797 fn drop(&mut self) {
798 self.abort();
800 }
801}
802
803#[cfg(test)]
804mod tests {
805 use super::*;
806
807 fn test_writer(tx: mpsc::Sender<pb::WriteSailboxFileRequest>) -> FileWriter {
808 FileWriter {
809 tx: Some(tx),
810 task: Some(tokio::spawn(async { Ok(()) })),
811 aborted: Arc::new(std::sync::atomic::AtomicBool::new(false)),
812 first: true,
813 sailbox_id: "sb_1".to_string(),
814 path: "/f".to_string(),
815 create_parents: true,
816 mode: None,
817 user: "alice".to_string(),
818 }
819 }
820
821 #[tokio::test]
822 async fn write_splits_at_the_transport_chunk_size() {
823 let (tx, mut rx) = mpsc::channel(16);
824 let mut writer = test_writer(tx);
825 writer
826 .write(&vec![7u8; FILE_WRITE_CHUNK_BYTES * 2 + 10])
827 .await
828 .expect("write succeeds");
829 writer.finish().await.expect("finish succeeds");
830 let mut sizes = Vec::new();
831 while let Some(message) = rx.recv().await {
832 sizes.push(message.data.len());
833 }
834 assert_eq!(
835 sizes,
836 vec![FILE_WRITE_CHUNK_BYTES, FILE_WRITE_CHUNK_BYTES, 10]
837 );
838 }
839
840 #[tokio::test]
841 async fn write_carries_the_target_metadata_on_the_first_chunk_only() {
842 let (tx, mut rx) = mpsc::channel(16);
843 let mut writer = test_writer(tx);
844 writer
845 .write(&vec![7u8; FILE_WRITE_CHUNK_BYTES + 10])
846 .await
847 .expect("write succeeds");
848 writer.finish().await.expect("finish succeeds");
849 let mut messages = Vec::new();
850 while let Some(message) = rx.recv().await {
851 messages.push(message);
852 }
853 assert_eq!(messages.len(), 2);
854 assert_eq!(messages[0].sailbox_id, "sb_1");
855 assert_eq!(messages[0].path, "/f");
856 assert_eq!(messages[0].user, "alice");
857 assert!(messages[0].create_parents);
858 assert_eq!(messages[1].sailbox_id, "");
859 assert_eq!(messages[1].path, "");
860 assert_eq!(messages[1].user, "");
861 assert!(!messages[1].create_parents);
862 }
863
864 #[test]
865 fn override_reason_metadata_accepts_ascii_and_rejects_non_ascii() {
866 let ok = override_reason_metadata("incident inc-42".to_string())
867 .expect("printable ASCII reason builds gRPC metadata");
868 assert_eq!(ok.to_str().unwrap(), "incident inc-42");
869
870 let err = override_reason_metadata("incident café".to_string())
871 .expect_err("non-ASCII reason must be rejected, not silently dropped");
872 match err {
873 SailError::Config { message } => {
874 assert!(
875 message.contains("SAIL_OWNER_OVERRIDE_REASON"),
876 "message = {message}"
877 );
878 }
879 other => panic!("expected Config error, got {other:?}"),
880 }
881 }
882
883 #[test]
884 fn transient_codes_retry_within_deadline() {
885 let deadline = Instant::now() + Duration::from_secs(5);
886 assert!(should_retry_transient_exec_rpc(
887 &Status::unavailable("x"),
888 deadline
889 ));
890 assert!(should_retry_transient_exec_rpc(
891 &Status::deadline_exceeded("x"),
892 deadline
893 ));
894 assert!(should_retry_transient_exec_rpc(
895 &Status::unknown("HTTP/2 connection reset by remote"),
896 deadline
897 ));
898 assert!(!should_retry_transient_exec_rpc(
899 &Status::unknown("guest exploded"),
900 deadline
901 ));
902 assert!(!should_retry_transient_exec_rpc(
903 &Status::not_found("x"),
904 deadline
905 ));
906 }
907
908 #[test]
909 fn h2_connection_failures_retry_within_deadline() {
910 let deadline = Instant::now() + Duration::from_secs(5);
911 let io = std::io::Error::new(
914 std::io::ErrorKind::ConnectionAborted,
915 "connection error: h2 protocol error: http2 error",
916 );
917 let transport = Status::from_error(Box::new(io));
918 assert!(transport.source().is_some());
919 assert!(should_retry_transient_exec_rpc(&transport, deadline));
920 assert!(should_retry_transient_exec_rpc(
923 &Status::internal("h2 protocol error: http2 error"),
924 deadline
925 ));
926 assert!(should_retry_transient_exec_rpc(
927 &Status::unknown("connection error: keep-alive timed out"),
928 deadline
929 ));
930 assert!(!should_retry_transient_exec_rpc(
933 &Status::internal("guest agent panicked"),
934 deadline
935 ));
936 }
937
938 #[test]
939 fn goaway_reason_statuses_retry_only_with_transport_source() {
940 let deadline = Instant::now() + Duration::from_secs(5);
941 let mut goaway = Status::new(Code::ResourceExhausted, "h2 protocol error: http2 error");
948 goaway.set_source(std::sync::Arc::new(std::io::Error::new(
949 std::io::ErrorKind::ConnectionReset,
950 "transport error",
951 )));
952 assert!(should_retry_transient_exec_rpc(&goaway, deadline));
953 assert!(!should_retry_transient_exec_rpc(
956 &Status::resource_exhausted("org concurrency limit reached"),
957 deadline
958 ));
959 let mut sec = Status::new(Code::PermissionDenied, "h2 protocol error: http2 error");
962 sec.set_source(std::sync::Arc::new(std::io::Error::new(
963 std::io::ErrorKind::ConnectionReset,
964 "transport error",
965 )));
966 assert!(should_retry_transient_exec_rpc(&sec, deadline));
967 assert!(!should_retry_transient_exec_rpc(
968 &Status::permission_denied("invalid API key"),
969 deadline
970 ));
971 }
972
973 #[test]
974 fn fired_attempt_timeout_retries_but_server_cancel_does_not() {
975 let deadline = Instant::now() + Duration::from_secs(5);
976 let timed_out = Status::from_error(Box::new(tonic::TimeoutExpired(())));
979 assert_eq!(timed_out.code(), Code::Cancelled);
980 assert!(should_retry_transient_exec_rpc(&timed_out, deadline));
981 assert!(!should_retry_transient_exec_rpc(
983 &Status::cancelled("client went away"),
984 deadline
985 ));
986 }
987
988 #[test]
989 fn expired_deadline_never_retries() {
990 let deadline = Instant::now();
991 assert!(!should_retry_transient_exec_rpc(
992 &Status::unavailable("x"),
993 deadline
994 ));
995 }
996
997 #[test]
998 fn draining_detection_is_case_insensitive_and_code_scoped() {
999 assert!(is_workerproxy_draining(&Status::unavailable(
1000 "workerproxy DRAINING for deploy"
1001 )));
1002 assert!(!is_workerproxy_draining(&Status::internal("draining")));
1003 assert!(!is_workerproxy_draining(&Status::unavailable("lameduck")));
1004 }
1005
1006 #[test]
1007 fn rpc_attempt_timeout_caps_and_leaves_retry_headroom() {
1008 let now = Instant::now();
1009 let far = rpc_attempt_timeout(now + Duration::from_mins(1));
1011 assert!(far <= Duration::from_secs_f64(EXEC_RPC_ATTEMPT_TIMEOUT_SECONDS));
1012 assert!(far > Duration::from_secs(5));
1013 let small = rpc_attempt_timeout(now + Duration::from_secs(5));
1016 assert!(small < Duration::from_secs(5));
1017 assert!(small <= Duration::from_secs(3));
1018 assert_eq!(
1020 rpc_attempt_timeout(now.checked_sub(Duration::from_secs(1)).unwrap()),
1021 Duration::from_millis(1)
1022 );
1023 }
1024
1025 #[test]
1026 fn invalidate_on_draining_or_transport_failure_only() {
1027 assert!(should_invalidate_channel(&Status::unavailable(
1029 "workerproxy is draining"
1030 )));
1031 let io = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "socket closed");
1034 assert!(should_invalidate_channel(&Status::from_error(Box::new(io))));
1035 assert!(!should_invalidate_channel(&Status::unavailable(
1037 "try again"
1038 )));
1039 }
1040}