1use std::sync::Arc;
9use std::time::{Duration, Instant};
10
11use serde::{Deserialize, Serialize};
12use tokio::sync::mpsc;
13use tokio::sync::Mutex as AsyncMutex;
14use tokio_stream::wrappers::ReceiverStream;
15use tonic::metadata::{AsciiMetadataKey, AsciiMetadataValue};
16use tonic::transport::Channel;
17use tonic::{Code, Request, Status};
18
19use crate::channels::ChannelCache;
20use crate::error::SailError;
21use crate::pb::workerproxy::v1 as pb;
22use pb::worker_proxy_service_client::WorkerProxyServiceClient;
23
24const FILE_READ_CHANNEL_CAP: usize = 4;
27
28pub const FILE_WRITE_CHUNK_BYTES: usize = 1 << 20;
32
33pub const EXEC_TRANSIENT_RETRY_INITIAL_DELAY_SECONDS: f64 = 0.2;
36pub const EXEC_TRANSIENT_RETRY_MAX_DELAY_SECONDS: f64 = 2.0;
38pub const EXEC_RPC_ATTEMPT_TIMEOUT_SECONDS: f64 = 10.0;
42
43pub(crate) const TRANSIENT_TRANSPORT_FRAGMENTS: &[&str] = &[
47 "endpoint closing",
48 "error reading server preface",
49 "connection reset",
50 "socket closed",
51 "transport is closing",
52];
53
54pub(crate) fn is_transient_transport_message(message: &str) -> bool {
57 let details = message.to_lowercase();
58 TRANSIENT_TRANSPORT_FRAGMENTS
59 .iter()
60 .any(|fragment| details.contains(fragment))
61}
62
63#[derive(Debug, Clone)]
66pub struct WaitOutcome {
67 pub status: i32,
69 pub stdout: String,
71 pub stderr: String,
73 pub return_code: i32,
75 pub timed_out: bool,
77 pub stdout_truncated: bool,
80 pub stderr_truncated: bool,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, Default)]
88#[serde(default)]
89pub struct Listener {
90 pub guest_port: u32,
92 pub public_url: String,
94 pub protocol: String,
96 pub route_status: String,
98 pub public_host: String,
100 pub public_port: u32,
102}
103
104#[derive(Debug, Clone, Default)]
108pub struct UploadOptions {
109 pub create_parents: bool,
111 pub mode: Option<u32>,
113}
114
115pub struct FileReader {
120 rx: AsyncMutex<mpsc::Receiver<Result<Vec<u8>, SailError>>>,
121}
122
123impl FileReader {
124 pub async fn next(&self) -> Option<Result<Vec<u8>, SailError>> {
127 self.rx.lock().await.recv().await
128 }
129}
130
131#[doc(hidden)]
135pub struct WorkerProxy {
136 channels: ChannelCache,
137 authorization: AsciiMetadataValue,
138}
139
140pub(crate) fn retry_deadline(retry_timeout: f64) -> Instant {
145 let now = Instant::now();
146 if retry_timeout <= 0.0 {
147 return now;
148 }
149 Duration::try_from_secs_f64(retry_timeout)
150 .ok()
151 .and_then(|timeout| now.checked_add(timeout))
152 .unwrap_or(now + RETRY_FOREVER_FALLBACK)
153}
154
155const RETRY_FOREVER_FALLBACK: Duration = Duration::from_hours(876_000);
158
159pub(crate) fn rpc_attempt_timeout(deadline: Instant) -> Duration {
165 (deadline.saturating_duration_since(Instant::now()) / 2)
166 .min(Duration::from_secs_f64(EXEC_RPC_ATTEMPT_TIMEOUT_SECONDS))
167 .max(Duration::from_millis(1))
168}
169
170pub(crate) fn is_workerproxy_draining(status: &Status) -> bool {
171 status.code() == Code::Unavailable && status.message().to_lowercase().contains("draining")
172}
173
174pub(crate) fn should_invalidate_channel(status: &Status) -> bool {
183 use std::error::Error;
184 is_workerproxy_draining(status)
185 || status.code() == Code::DeadlineExceeded
186 || status.source().is_some()
187}
188
189pub(crate) fn should_retry_transient_exec_rpc(status: &Status, deadline: Instant) -> bool {
190 use std::error::Error;
191 if Instant::now() >= deadline {
192 return false;
193 }
194 match status.code() {
195 Code::Unavailable | Code::DeadlineExceeded => true,
196 Code::Cancelled => status.source().is_some(),
201 Code::Unknown => is_transient_transport_message(status.message()),
202 _ => false,
203 }
204}
205
206pub(crate) async fn sleep_before_retry(delay: f64, deadline: Instant) -> f64 {
209 let mut sleep_for = delay.min(EXEC_TRANSIENT_RETRY_MAX_DELAY_SECONDS);
210 let remaining = deadline
211 .saturating_duration_since(Instant::now())
212 .as_secs_f64();
213 if remaining <= 0.0 {
214 return delay;
215 }
216 sleep_for = sleep_for.min(remaining);
217 tokio::time::sleep(Duration::from_secs_f64(sleep_for.max(0.0))).await;
218 (delay * 2.0).min(EXEC_TRANSIENT_RETRY_MAX_DELAY_SECONDS)
219}
220
221impl WorkerProxy {
222 pub fn new(api_key: &str) -> Result<WorkerProxy, SailError> {
225 let authorization = format!("Bearer {api_key}")
226 .parse()
227 .map_err(|_| SailError::Config {
228 message: "SAIL_API_KEY contains characters invalid in a gRPC metadata value"
229 .to_string(),
230 })?;
231 Ok(WorkerProxy {
232 channels: ChannelCache::new(),
233 authorization,
234 })
235 }
236
237 pub(crate) fn channels(&self) -> &ChannelCache {
238 &self.channels
239 }
240
241 pub(crate) fn client_for(
242 &self,
243 endpoint: &str,
244 ) -> Result<WorkerProxyServiceClient<Channel>, SailError> {
245 let channel = self.channels.get(endpoint)?;
246 Ok(WorkerProxyServiceClient::new(channel))
247 }
248
249 pub(crate) fn request_for<T>(
250 &self,
251 message: T,
252 extra_metadata: &[(String, String)],
253 timeout: Option<Duration>,
254 ) -> Result<Request<T>, SailError> {
255 let mut request = Request::new(message);
256 request
257 .metadata_mut()
258 .insert("authorization", self.authorization.clone());
259 for (key, value) in extra_metadata {
260 let key: AsciiMetadataKey = key.parse().map_err(|_| SailError::Config {
261 message: format!("invalid gRPC metadata key {key:?}"),
262 })?;
263 let value: AsciiMetadataValue = value.parse().map_err(|_| SailError::Config {
264 message: format!("invalid gRPC metadata value for key {key:?}"),
265 })?;
266 request.metadata_mut().insert(key, value);
267 }
268 if let Some(timeout) = timeout {
269 request.set_timeout(timeout);
270 }
271 Ok(request)
272 }
273
274 pub async fn wait_exec(
279 &self,
280 endpoint: &str,
281 sailbox_id: &str,
282 exec_request_id: &str,
283 retry_timeout: f64,
284 ) -> Result<WaitOutcome, SailError> {
285 let mut deadline: Option<Instant> = None;
286 let mut delay = EXEC_TRANSIENT_RETRY_INITIAL_DELAY_SECONDS;
287 loop {
288 let message = pb::WaitSailboxExecRequest {
289 sailbox_id: sailbox_id.to_string(),
290 exec_request_id: exec_request_id.to_string(),
291 };
292 let request = self.request_for(message, &[], None)?;
293 match self.client_for(endpoint)?.wait_sailbox_exec(request).await {
294 Ok(resp) => {
295 let resp = resp.into_inner();
296 return Ok(WaitOutcome {
297 status: resp.status,
298 stdout: resp.stdout,
299 stderr: resp.stderr,
300 return_code: resp.return_code,
301 timed_out: resp.timed_out,
302 stdout_truncated: resp.stdout_truncated,
303 stderr_truncated: resp.stderr_truncated,
304 });
305 }
306 Err(status) => {
307 let deadline = *deadline.get_or_insert_with(|| retry_deadline(retry_timeout));
308 if !should_retry_transient_exec_rpc(&status, deadline) {
309 return Err(SailError::from_exec_status(&status));
310 }
311 tracing::warn!(code = ?status.code(), endpoint, "retrying transient worker-proxy RPC");
312 if should_invalidate_channel(&status) {
313 self.channels.invalidate(endpoint);
314 }
315 delay = sleep_before_retry(delay, deadline).await;
316 }
317 }
318 }
319 }
320
321 pub async fn cancel_exec(
326 &self,
327 endpoint: &str,
328 sailbox_id: &str,
329 exec_request_id: &str,
330 force: bool,
331 retry_timeout: f64,
332 ) -> Result<(), SailError> {
333 let deadline = retry_deadline(retry_timeout);
334 let mut delay = EXEC_TRANSIENT_RETRY_INITIAL_DELAY_SECONDS;
335 loop {
336 let per_attempt_timeout = if retry_timeout > 0.0 {
337 Some(rpc_attempt_timeout(deadline))
342 } else {
343 None
344 };
345 let message = pb::CancelSailboxExecRequest {
346 sailbox_id: sailbox_id.to_string(),
347 exec_request_id: exec_request_id.to_string(),
348 force,
349 };
350 let request = self.request_for(message, &[], per_attempt_timeout)?;
351 match self
352 .client_for(endpoint)?
353 .cancel_sailbox_exec(request)
354 .await
355 {
356 Ok(_) => return Ok(()),
357 Err(status) => {
358 if !should_retry_transient_exec_rpc(&status, deadline) {
359 return Err(SailError::from_exec_status(&status));
360 }
361 tracing::warn!(code = ?status.code(), endpoint, "retrying transient worker-proxy RPC");
362 if should_invalidate_channel(&status) {
363 self.channels.invalidate(endpoint);
364 }
365 delay = sleep_before_retry(delay, deadline).await;
366 }
367 }
368 }
369 }
370
371 pub fn read_file(self: &Arc<Self>, endpoint: &str, sailbox_id: &str, path: &str) -> FileReader {
380 let (tx, rx) = mpsc::channel(FILE_READ_CHANNEL_CAP);
381 let worker = Arc::clone(self);
382 let endpoint = endpoint.to_string();
383 let message = pb::ReadSailboxFileRequest {
384 sailbox_id: sailbox_id.to_string(),
385 path: path.to_string(),
386 };
387 tokio::spawn(async move {
388 let request = match worker.request_for(message, &[], None) {
389 Ok(request) => request,
390 Err(err) => {
391 let _ = tx.send(Err(err)).await;
392 return;
393 }
394 };
395 let mut client = match worker.client_for(&endpoint) {
396 Ok(client) => client,
397 Err(err) => {
398 let _ = tx.send(Err(err)).await;
399 return;
400 }
401 };
402 let mut stream = match client.read_sailbox_file(request).await {
403 Ok(resp) => resp.into_inner(),
404 Err(status) => {
405 let _ = tx.send(Err(SailError::from_file_rpc_status(&status))).await;
406 return;
407 }
408 };
409 loop {
410 match stream.message().await {
411 Ok(Some(resp)) => {
412 if !resp.data.is_empty() && tx.send(Ok(resp.data)).await.is_err() {
413 return; }
415 }
416 Ok(None) => return,
417 Err(status) => {
418 let _ = tx.send(Err(SailError::from_file_rpc_status(&status))).await;
419 return;
420 }
421 }
422 }
423 });
424 FileReader {
425 rx: AsyncMutex::new(rx),
426 }
427 }
428
429 pub fn write_file(
439 self: &Arc<Self>,
440 endpoint: &str,
441 sailbox_id: &str,
442 path: &str,
443 create_parents: bool,
444 mode: Option<u32>,
445 ) -> FileWriter {
446 let (tx, rx) = mpsc::channel(FILE_READ_CHANNEL_CAP);
447 let worker = Arc::clone(self);
448 let endpoint = endpoint.to_string();
449 let task = tokio::spawn(async move {
452 let request = worker.request_for(ReceiverStream::new(rx), &[], None)?;
453 worker
454 .client_for(&endpoint)?
455 .write_sailbox_file(request)
456 .await
457 .map(|_| ())
458 .map_err(|status| SailError::from_file_rpc_status(&status))
459 });
460 FileWriter {
461 tx: Some(tx),
462 task: Some(task),
463 first: true,
464 sailbox_id: sailbox_id.to_string(),
465 path: path.to_string(),
466 create_parents,
467 mode,
468 }
469 }
470}
471
472pub struct FileWriter {
476 tx: Option<mpsc::Sender<pb::WriteSailboxFileRequest>>,
477 task: Option<tokio::task::JoinHandle<Result<(), SailError>>>,
478 first: bool,
479 sailbox_id: String,
480 path: String,
481 create_parents: bool,
482 mode: Option<u32>,
483}
484
485impl FileWriter {
486 fn build(&mut self, data: Vec<u8>) -> pb::WriteSailboxFileRequest {
487 let header = self.first;
488 self.first = false;
489 pb::WriteSailboxFileRequest {
490 sailbox_id: if header {
491 self.sailbox_id.clone()
492 } else {
493 String::new()
494 },
495 path: if header {
496 self.path.clone()
497 } else {
498 String::new()
499 },
500 data,
501 create_parents: header && self.create_parents,
502 mode: if header { self.mode } else { None },
503 }
504 }
505
506 async fn join(&mut self) -> Result<(), SailError> {
508 self.tx = None; match self.task.take() {
510 Some(task) => task.await.unwrap_or_else(|join_err| {
511 Err(SailError::Internal {
512 message: format!("file write task failed: {join_err}"),
513 })
514 }),
515 None => Ok(()),
516 }
517 }
518
519 pub async fn write_chunk(&mut self, data: Vec<u8>) -> Result<(), SailError> {
522 let request = self.build(data);
523 match &self.tx {
524 Some(tx) if tx.send(request).await.is_ok() => Ok(()),
526 _ => self.join().await,
527 }
528 }
529
530 pub async fn finish(&mut self) -> Result<(), SailError> {
533 if self.first {
534 let request = self.build(Vec::new());
537 if let Some(tx) = &self.tx {
538 let _ = tx.send(request).await;
539 }
540 }
541 self.join().await
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 #[test]
550 fn transient_codes_retry_within_deadline() {
551 let deadline = Instant::now() + Duration::from_secs(5);
552 assert!(should_retry_transient_exec_rpc(
553 &Status::unavailable("x"),
554 deadline
555 ));
556 assert!(should_retry_transient_exec_rpc(
557 &Status::deadline_exceeded("x"),
558 deadline
559 ));
560 assert!(should_retry_transient_exec_rpc(
561 &Status::unknown("HTTP/2 connection reset by remote"),
562 deadline
563 ));
564 assert!(!should_retry_transient_exec_rpc(
565 &Status::unknown("guest exploded"),
566 deadline
567 ));
568 assert!(!should_retry_transient_exec_rpc(
569 &Status::not_found("x"),
570 deadline
571 ));
572 }
573
574 #[test]
575 fn fired_attempt_timeout_retries_but_server_cancel_does_not() {
576 let deadline = Instant::now() + Duration::from_secs(5);
577 let timed_out = Status::from_error(Box::new(tonic::TimeoutExpired(())));
580 assert_eq!(timed_out.code(), Code::Cancelled);
581 assert!(should_retry_transient_exec_rpc(&timed_out, deadline));
582 assert!(!should_retry_transient_exec_rpc(
584 &Status::cancelled("client went away"),
585 deadline
586 ));
587 }
588
589 #[test]
590 fn expired_deadline_never_retries() {
591 let deadline = Instant::now();
592 assert!(!should_retry_transient_exec_rpc(
593 &Status::unavailable("x"),
594 deadline
595 ));
596 }
597
598 #[test]
599 fn draining_detection_is_case_insensitive_and_code_scoped() {
600 assert!(is_workerproxy_draining(&Status::unavailable(
601 "workerproxy DRAINING for deploy"
602 )));
603 assert!(!is_workerproxy_draining(&Status::internal("draining")));
604 assert!(!is_workerproxy_draining(&Status::unavailable("lameduck")));
605 }
606
607 #[test]
608 fn rpc_attempt_timeout_caps_and_leaves_retry_headroom() {
609 let now = Instant::now();
610 let far = rpc_attempt_timeout(now + Duration::from_mins(1));
612 assert!(far <= Duration::from_secs_f64(EXEC_RPC_ATTEMPT_TIMEOUT_SECONDS));
613 assert!(far > Duration::from_secs(5));
614 let small = rpc_attempt_timeout(now + Duration::from_secs(5));
617 assert!(small < Duration::from_secs(5));
618 assert!(small <= Duration::from_secs(3));
619 assert_eq!(
621 rpc_attempt_timeout(now.checked_sub(Duration::from_secs(1)).unwrap()),
622 Duration::from_millis(1)
623 );
624 }
625
626 #[test]
627 fn invalidate_on_draining_or_transport_failure_only() {
628 assert!(should_invalidate_channel(&Status::unavailable(
630 "workerproxy is draining"
631 )));
632 let io = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "socket closed");
635 assert!(should_invalidate_channel(&Status::from_error(Box::new(io))));
636 assert!(!should_invalidate_channel(&Status::unavailable(
638 "try again"
639 )));
640 }
641}