1use std::{
4 error::Error as StdError,
5 ffi::OsString,
6 fmt,
7 future::Future,
8 marker::PhantomData,
9 pin::Pin,
10 sync::{
11 Arc, OnceLock,
12 atomic::{AtomicU64, Ordering},
13 },
14 task::{Context, Poll},
15 time::Duration,
16};
17
18use http_body::{Body as HttpBody, Frame, SizeHint};
19use prost::bytes::Buf;
20use tonic::{
21 Code, GrpcMethod, Request, Status, TimeoutExpired,
22 body::Body,
23 codegen::{Service, http},
24 metadata::{Ascii, MetadataMap, MetadataValue},
25};
26
27tokio::task_local! {
28 static ENCLOSING_DEADLINE: GrpcEnclosingDeadline;
29}
30
31pub const ANYTYPE_GRPC_TIMEOUT_SECS: &str = "ANYTYPE_GRPC_TIMEOUT_SECS";
33pub const MAX_GRPC_TIMEOUT: Duration = Duration::from_secs(3_600);
35pub const MAX_LONG_GRPC_TIMEOUT: Duration = Duration::from_secs(7_200);
37pub const MAX_CLEANUP_GRPC_TIMEOUT: Duration = Duration::from_secs(30);
39pub const DEFAULT_CREDENTIAL_GRPC_TIMEOUT: Duration = Duration::from_secs(120);
41pub const DEFAULT_ORDINARY_GRPC_TIMEOUT: Duration = Duration::from_secs(120);
43pub const DEFAULT_LONG_GRPC_TIMEOUT: Duration = Duration::from_secs(1_800);
45pub const DEFAULT_STREAM_SETUP_GRPC_TIMEOUT: Duration = Duration::from_secs(120);
47pub const DEFAULT_CLEANUP_GRPC_TIMEOUT: Duration = Duration::from_secs(5);
49
50const DEADLINE_SOURCE_METADATA: &str = "x-anytype-deadline-source";
51const DEADLINE_CLASS_METADATA: &str = "x-anytype-deadline-class";
52const DEADLINE_OUTCOME_METADATA: &str = "x-anytype-deadline-outcome";
53const GRPC_TIMEOUT_HEADER: &str = "grpc-timeout";
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub struct GrpcTimeoutPolicy {
63 pub credential_setup: Option<Duration>,
65 pub ordinary_unary: Option<Duration>,
67 pub long_unary: Option<Duration>,
69 pub stream_setup: Option<Duration>,
71 pub stream_idle: Option<Duration>,
73 pub stream_total_lifetime: Option<Duration>,
75 pub cleanup: Option<Duration>,
77}
78
79impl Default for GrpcTimeoutPolicy {
80 fn default() -> Self {
81 Self {
82 credential_setup: Some(DEFAULT_CREDENTIAL_GRPC_TIMEOUT),
83 ordinary_unary: Some(DEFAULT_ORDINARY_GRPC_TIMEOUT),
84 long_unary: Some(DEFAULT_LONG_GRPC_TIMEOUT),
85 stream_setup: Some(DEFAULT_STREAM_SETUP_GRPC_TIMEOUT),
86 stream_idle: None,
87 stream_total_lifetime: None,
88 cleanup: Some(DEFAULT_CLEANUP_GRPC_TIMEOUT),
89 }
90 }
91}
92
93impl GrpcTimeoutPolicy {
94 pub fn resolve(explicit: Option<Self>) -> Result<Self, GrpcTimeoutConfigError> {
98 if let Some(policy) = explicit {
99 return policy.validate();
100 }
101 Self::from_environment(std::env::var_os(ANYTYPE_GRPC_TIMEOUT_SECS))
102 }
103
104 pub fn validate(self) -> Result<Self, GrpcTimeoutConfigError> {
106 for (field, value, maximum) in [
107 ("credential_setup", self.credential_setup, MAX_GRPC_TIMEOUT),
108 ("ordinary_unary", self.ordinary_unary, MAX_GRPC_TIMEOUT),
109 ("long_unary", self.long_unary, MAX_LONG_GRPC_TIMEOUT),
110 ("stream_setup", self.stream_setup, MAX_GRPC_TIMEOUT),
111 ("stream_idle", self.stream_idle, MAX_GRPC_TIMEOUT),
112 (
113 "stream_total_lifetime",
114 self.stream_total_lifetime,
115 MAX_GRPC_TIMEOUT,
116 ),
117 ("cleanup", self.cleanup, MAX_CLEANUP_GRPC_TIMEOUT),
118 ] {
119 if let Some(duration) = value
120 && !(Duration::from_secs(1)..=maximum).contains(&duration)
121 {
122 return Err(GrpcTimeoutConfigError::InvalidField {
123 field,
124 maximum_seconds: maximum.as_secs(),
125 });
126 }
127 }
128 Ok(self)
129 }
130
131 #[must_use]
133 pub const fn duration(self, class: GrpcTimeoutClass) -> Option<Duration> {
134 match class {
135 GrpcTimeoutClass::CredentialSetup => self.credential_setup,
136 GrpcTimeoutClass::OrdinaryUnary => self.ordinary_unary,
137 GrpcTimeoutClass::LongUnary => self.long_unary,
138 GrpcTimeoutClass::StreamSetup => self.stream_setup,
139 GrpcTimeoutClass::StreamIdle => self.stream_idle,
140 GrpcTimeoutClass::StreamLifetime => self.stream_total_lifetime,
141 GrpcTimeoutClass::Cleanup => self.cleanup,
142 }
143 }
144
145 fn from_environment(value: Option<OsString>) -> Result<Self, GrpcTimeoutConfigError> {
146 let Some(value) = value else {
147 return Ok(Self::default());
148 };
149 let value = value
150 .into_string()
151 .map_err(|_| GrpcTimeoutConfigError::InvalidEnvironment)?;
152 if value.is_empty()
153 || !value.bytes().all(|byte| byte.is_ascii_digit())
154 || (value.len() > 1 && value.starts_with('0'))
155 {
156 return Err(GrpcTimeoutConfigError::InvalidEnvironment);
157 }
158 let seconds = value
159 .parse::<u64>()
160 .map_err(|_| GrpcTimeoutConfigError::InvalidEnvironment)?;
161 if seconds > MAX_GRPC_TIMEOUT.as_secs() {
162 return Err(GrpcTimeoutConfigError::EnvironmentOutOfRange);
163 }
164 let inherited = (seconds != 0).then(|| Duration::from_secs(seconds));
165 Ok(Self {
166 credential_setup: inherited,
167 ordinary_unary: inherited,
168 long_unary: inherited,
169 stream_setup: inherited,
170 stream_idle: None,
171 stream_total_lifetime: None,
172 cleanup: Some(DEFAULT_CLEANUP_GRPC_TIMEOUT),
173 })
174 }
175}
176
177#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179pub enum GrpcTimeoutConfigError {
180 InvalidField {
182 field: &'static str,
184 maximum_seconds: u64,
186 },
187 InvalidEnvironment,
189 EnvironmentOutOfRange,
191 UnrepresentableDeadline,
193}
194
195impl fmt::Display for GrpcTimeoutConfigError {
196 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
197 match self {
198 Self::InvalidField {
199 field,
200 maximum_seconds,
201 } => write!(
202 formatter,
203 "grpc_timeouts.{field} must be disabled or between 1 and {maximum_seconds} seconds"
204 ),
205 Self::InvalidEnvironment => write!(
206 formatter,
207 "{ANYTYPE_GRPC_TIMEOUT_SECS} must be an ASCII decimal from 0 through 3600"
208 ),
209 Self::EnvironmentOutOfRange => write!(
210 formatter,
211 "{ANYTYPE_GRPC_TIMEOUT_SECS} must not exceed 3600 seconds"
212 ),
213 Self::UnrepresentableDeadline => {
214 formatter.write_str("gRPC absolute deadline is not representable")
215 }
216 }
217 }
218}
219
220impl StdError for GrpcTimeoutConfigError {}
221
222#[derive(Clone, Copy, Debug, PartialEq, Eq)]
224pub enum GrpcTimeoutClass {
225 CredentialSetup,
227 OrdinaryUnary,
229 LongUnary,
231 StreamSetup,
233 StreamIdle,
235 StreamLifetime,
237 Cleanup,
239}
240
241impl GrpcTimeoutClass {
242 const fn as_str(self) -> &'static str {
243 match self {
244 Self::CredentialSetup => "credential_setup",
245 Self::OrdinaryUnary => "ordinary_unary",
246 Self::LongUnary => "long_unary",
247 Self::StreamSetup => "stream_setup",
248 Self::StreamIdle => "stream_idle",
249 Self::StreamLifetime => "stream_lifetime",
250 Self::Cleanup => "cleanup",
251 }
252 }
253
254 fn from_str(value: &str) -> Option<Self> {
255 match value {
256 "credential_setup" => Some(Self::CredentialSetup),
257 "ordinary_unary" => Some(Self::OrdinaryUnary),
258 "long_unary" => Some(Self::LongUnary),
259 "stream_setup" => Some(Self::StreamSetup),
260 "stream_idle" => Some(Self::StreamIdle),
261 "stream_lifetime" => Some(Self::StreamLifetime),
262 "cleanup" => Some(Self::Cleanup),
263 _ => None,
264 }
265 }
266}
267
268impl fmt::Display for GrpcTimeoutClass {
269 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
270 formatter.write_str(self.as_str())
271 }
272}
273
274#[derive(Clone, Copy, Debug, PartialEq, Eq)]
276pub enum GrpcTimeoutOutcome {
277 ReadAborted,
279 MutationIndeterminate,
281 StreamTerminated,
283}
284
285impl GrpcTimeoutOutcome {
286 const fn as_str(self) -> &'static str {
287 match self {
288 Self::ReadAborted => "read_aborted",
289 Self::MutationIndeterminate => "mutation_indeterminate",
290 Self::StreamTerminated => "stream_terminated",
291 }
292 }
293
294 fn from_str(value: &str) -> Option<Self> {
295 match value {
296 "read_aborted" => Some(Self::ReadAborted),
297 "mutation_indeterminate" => Some(Self::MutationIndeterminate),
298 "stream_terminated" => Some(Self::StreamTerminated),
299 _ => None,
300 }
301 }
302}
303
304impl fmt::Display for GrpcTimeoutOutcome {
305 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
306 formatter.write_str(self.as_str())
307 }
308}
309
310#[derive(Clone, Copy, Debug, PartialEq, Eq)]
312pub enum GrpcTimeoutSource {
313 Local,
315 Server,
317}
318
319impl fmt::Display for GrpcTimeoutSource {
320 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
321 formatter.write_str(match self {
322 Self::Local => "local",
323 Self::Server => "server",
324 })
325 }
326}
327
328#[derive(Clone, Copy, Debug, PartialEq, Eq)]
330pub struct GrpcDeadlineError {
331 pub class: GrpcTimeoutClass,
333 pub outcome: GrpcTimeoutOutcome,
335 pub source: GrpcTimeoutSource,
337 pub elapsed: Duration,
339}
340
341impl GrpcDeadlineError {
342 #[must_use]
344 pub fn from_status(
345 status: &Status,
346 fallback_class: GrpcTimeoutClass,
347 fallback_outcome: GrpcTimeoutOutcome,
348 elapsed: Duration,
349 ) -> Option<Self> {
350 if status.code() != Code::DeadlineExceeded {
351 return None;
352 }
353 let class = metadata_text(status.metadata(), DEADLINE_CLASS_METADATA)
354 .and_then(GrpcTimeoutClass::from_str)
355 .unwrap_or(fallback_class);
356 let outcome = metadata_text(status.metadata(), DEADLINE_OUTCOME_METADATA)
357 .and_then(GrpcTimeoutOutcome::from_str)
358 .unwrap_or(fallback_outcome);
359 let source = if metadata_text(status.metadata(), DEADLINE_SOURCE_METADATA) == Some("local")
360 {
361 GrpcTimeoutSource::Local
362 } else {
363 GrpcTimeoutSource::Server
364 };
365 Some(Self {
366 class,
367 outcome,
368 source,
369 elapsed,
370 })
371 }
372}
373
374impl fmt::Display for GrpcDeadlineError {
375 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
376 write!(
377 formatter,
378 "gRPC deadline expired class={} outcome={} source={} elapsed_ms={}",
379 self.class,
380 self.outcome,
381 self.source,
382 self.elapsed.as_millis()
383 )
384 }
385}
386
387impl StdError for GrpcDeadlineError {}
388
389#[derive(Clone, Copy, Debug, PartialEq, Eq)]
391pub struct GrpcEnclosingDeadline(tokio::time::Instant);
392
393impl GrpcEnclosingDeadline {
394 pub fn from_now(duration: Duration) -> Result<Self, GrpcTimeoutConfigError> {
396 tokio::time::Instant::now()
397 .checked_add(duration)
398 .map(Self)
399 .ok_or(GrpcTimeoutConfigError::UnrepresentableDeadline)
400 }
401
402 #[must_use]
404 pub const fn from_instant(deadline: tokio::time::Instant) -> Self {
405 Self(deadline)
406 }
407
408 #[must_use]
410 pub const fn instant(self) -> tokio::time::Instant {
411 self.0
412 }
413}
414
415pub async fn scope_grpc_enclosing_deadline<F, T>(deadline: GrpcEnclosingDeadline, operation: F) -> T
420where
421 F: Future<Output = T>,
422{
423 ENCLOSING_DEADLINE.scope(deadline, operation).await
424}
425
426#[derive(Clone, Copy, Debug, PartialEq, Eq)]
428pub struct GrpcCallOptions {
429 pub class: GrpcTimeoutClass,
431 pub outcome: GrpcTimeoutOutcome,
433 pub enclosing: Option<GrpcEnclosingDeadline>,
435}
436
437impl GrpcCallOptions {
438 #[must_use]
440 pub const fn new(class: GrpcTimeoutClass, outcome: GrpcTimeoutOutcome) -> Self {
441 Self {
442 class,
443 outcome,
444 enclosing: None,
445 }
446 }
447
448 #[must_use]
450 pub const fn enclosing(mut self, deadline: GrpcEnclosingDeadline) -> Self {
451 self.enclosing = Some(deadline);
452 self
453 }
454
455 #[must_use]
457 pub const fn ordinary_read() -> Self {
458 Self::new(
459 GrpcTimeoutClass::OrdinaryUnary,
460 GrpcTimeoutOutcome::ReadAborted,
461 )
462 }
463
464 #[must_use]
466 pub const fn ordinary_mutation() -> Self {
467 Self::new(
468 GrpcTimeoutClass::OrdinaryUnary,
469 GrpcTimeoutOutcome::MutationIndeterminate,
470 )
471 }
472
473 #[must_use]
475 pub const fn long_read() -> Self {
476 Self::new(GrpcTimeoutClass::LongUnary, GrpcTimeoutOutcome::ReadAborted)
477 }
478
479 #[must_use]
481 pub const fn stream_setup() -> Self {
482 Self::new(
483 GrpcTimeoutClass::StreamSetup,
484 GrpcTimeoutOutcome::StreamTerminated,
485 )
486 }
487
488 #[must_use]
490 pub const fn cleanup() -> Self {
491 Self::new(
492 GrpcTimeoutClass::Cleanup,
493 GrpcTimeoutOutcome::MutationIndeterminate,
494 )
495 }
496}
497
498impl Default for GrpcCallOptions {
499 fn default() -> Self {
500 Self::ordinary_mutation()
503 }
504}
505
506pub fn with_grpc_call_options<T>(mut request: Request<T>, options: GrpcCallOptions) -> Request<T> {
508 request.extensions_mut().insert(options);
509 request
510}
511
512#[derive(Clone)]
517pub struct GrpcTransportProgress {
518 inner: Arc<GrpcTransportProgressInner>,
519}
520
521struct GrpcTransportProgressInner {
522 generation: AtomicU64,
523 notify: tokio::sync::Notify,
524}
525
526impl GrpcTransportProgress {
527 fn new() -> Self {
528 Self {
529 inner: Arc::new(GrpcTransportProgressInner {
530 generation: AtomicU64::new(0),
531 notify: tokio::sync::Notify::new(),
532 }),
533 }
534 }
535
536 fn record(&self) {
537 self.inner.generation.fetch_add(1, Ordering::Relaxed);
538 self.inner.notify.notify_waiters();
539 }
540
541 fn generation(&self) -> u64 {
542 self.inner.generation.load(Ordering::Relaxed)
543 }
544
545 async fn changed_after(&self, generation: u64) -> u64 {
546 loop {
547 let notified = self.inner.notify.notified();
548 let current = self.generation();
549 if current != generation {
550 return current;
551 }
552 notified.await;
553 }
554 }
555}
556
557impl fmt::Debug for GrpcTransportProgress {
558 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
559 formatter
560 .debug_struct("GrpcTransportProgress")
561 .field("generation", &self.generation())
562 .finish()
563 }
564}
565
566#[derive(Clone)]
571pub struct GrpcDeadlineService<S> {
572 inner: S,
573 policy: GrpcTimeoutPolicy,
574}
575
576impl<S> fmt::Debug for GrpcDeadlineService<S> {
577 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
578 formatter
579 .debug_struct("GrpcDeadlineService")
580 .field("policy", &self.policy)
581 .field("inner", &"redacted")
582 .finish()
583 }
584}
585
586impl<S> GrpcDeadlineService<S> {
587 pub fn try_new(inner: S, policy: GrpcTimeoutPolicy) -> Result<Self, GrpcTimeoutConfigError> {
589 Ok(Self {
590 inner,
591 policy: policy.validate()?,
592 })
593 }
594
595 pub(crate) const fn new_resolved(inner: S, policy: GrpcTimeoutPolicy) -> Self {
596 Self { inner, policy }
597 }
598
599 #[must_use]
601 pub const fn policy(&self) -> GrpcTimeoutPolicy {
602 self.policy
603 }
604
605 #[must_use]
607 pub const fn inner(&self) -> &S {
608 &self.inner
609 }
610}
611
612pub enum GrpcDeadlineServiceError<E> {
614 Transport {
616 code: Code,
618 error_type: PhantomData<fn() -> E>,
620 },
621 Deadline(Status),
623 InvalidTimeout(Status),
625}
626
627impl<E> fmt::Debug for GrpcDeadlineServiceError<E> {
628 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
629 match self {
630 Self::Transport { code, .. } => formatter
631 .debug_struct("GrpcDeadlineServiceError::Transport")
632 .field("code", code)
633 .field("source", &"redacted")
634 .finish(),
635 Self::Deadline(_) => {
636 formatter.write_str("GrpcDeadlineServiceError::Deadline(redacted)")
637 }
638 Self::InvalidTimeout(_) => {
639 formatter.write_str("GrpcDeadlineServiceError::InvalidTimeout(redacted)")
640 }
641 }
642 }
643}
644
645impl<E> fmt::Display for GrpcDeadlineServiceError<E> {
646 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
647 formatter.write_str(match self {
648 Self::Transport { .. } => "gRPC transport failed",
649 Self::Deadline(_) => "gRPC local deadline expired",
650 Self::InvalidTimeout(_) => "invalid gRPC timeout metadata",
651 })
652 }
653}
654
655impl<E> StdError for GrpcDeadlineServiceError<E> {
656 fn source(&self) -> Option<&(dyn StdError + 'static)> {
657 match self {
658 Self::Transport { code, .. } => Some(redacted_transport_status_source(*code)),
659 Self::Deadline(status) => Some(redacted_deadline_status_source(status)),
660 Self::InvalidTimeout(_) => Some(redacted_invalid_timeout_source()),
661 }
662 }
663}
664
665impl<E> GrpcDeadlineServiceError<E>
666where
667 E: StdError + Send + Sync + 'static,
668{
669 fn transport(source: E) -> Self {
670 let code = closed_grpc_error_code(source);
671 Self::Transport {
672 code,
673 error_type: PhantomData,
674 }
675 }
676}
677
678fn closed_grpc_error_code<E>(error: E) -> Code
679where
680 E: StdError + Send + Sync + 'static,
681{
682 Status::from_error(Box::new(error)).code()
683}
684
685fn redacted_transport_status_source(code: Code) -> &'static Status {
686 static SOURCES: [OnceLock<Status>; 17] = [const { OnceLock::new() }; 17];
687 let index = match code {
688 Code::Ok => 0,
689 Code::Cancelled => 1,
690 Code::Unknown => 2,
691 Code::InvalidArgument => 3,
692 Code::DeadlineExceeded => 4,
693 Code::NotFound => 5,
694 Code::AlreadyExists => 6,
695 Code::PermissionDenied => 7,
696 Code::ResourceExhausted => 8,
697 Code::FailedPrecondition => 9,
698 Code::Aborted => 10,
699 Code::OutOfRange => 11,
700 Code::Unimplemented => 12,
701 Code::Internal => 13,
702 Code::Unavailable => 14,
703 Code::DataLoss => 15,
704 Code::Unauthenticated => 16,
705 };
706 SOURCES[index].get_or_init(|| Status::new(code, "gRPC transport failed (details redacted)"))
707}
708
709fn redacted_deadline_status_source(status: &Status) -> &'static Status {
710 static SOURCES: [OnceLock<Status>; 21] = [const { OnceLock::new() }; 21];
711 let class = metadata_text(status.metadata(), DEADLINE_CLASS_METADATA)
712 .and_then(GrpcTimeoutClass::from_str)
713 .unwrap_or(GrpcTimeoutClass::OrdinaryUnary);
714 let outcome = metadata_text(status.metadata(), DEADLINE_OUTCOME_METADATA)
715 .and_then(GrpcTimeoutOutcome::from_str)
716 .unwrap_or(GrpcTimeoutOutcome::MutationIndeterminate);
717 let class_index = match class {
718 GrpcTimeoutClass::CredentialSetup => 0,
719 GrpcTimeoutClass::OrdinaryUnary => 1,
720 GrpcTimeoutClass::LongUnary => 2,
721 GrpcTimeoutClass::StreamSetup => 3,
722 GrpcTimeoutClass::StreamIdle => 4,
723 GrpcTimeoutClass::StreamLifetime => 5,
724 GrpcTimeoutClass::Cleanup => 6,
725 };
726 let outcome_index = match outcome {
727 GrpcTimeoutOutcome::ReadAborted => 0,
728 GrpcTimeoutOutcome::MutationIndeterminate => 1,
729 GrpcTimeoutOutcome::StreamTerminated => 2,
730 };
731 SOURCES[class_index * 3 + outcome_index]
732 .get_or_init(|| local_deadline_status(GrpcCallOptions::new(class, outcome), Duration::ZERO))
733}
734
735fn redacted_invalid_timeout_source() -> &'static Status {
736 static SOURCE: OnceLock<Status> = OnceLock::new();
737 SOURCE.get_or_init(|| Status::invalid_argument("invalid gRPC timeout metadata"))
738}
739
740type BoxServiceFuture<T, E> = Pin<
741 Box<
742 dyn Future<
743 Output = Result<http::Response<GrpcDeadlineBody<T>>, GrpcDeadlineServiceError<E>>,
744 > + Send
745 + 'static,
746 >,
747>;
748
749pub struct GrpcDeadlineBody<B> {
751 inner: Pin<Box<B>>,
752 sleep: Option<Pin<Box<tokio::time::Sleep>>>,
753 progress: Option<GrpcTransportProgress>,
754 options: GrpcCallOptions,
755 started: tokio::time::Instant,
756 finished: bool,
757}
758
759impl<B> fmt::Debug for GrpcDeadlineBody<B> {
760 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
761 formatter
762 .debug_struct("GrpcDeadlineBody")
763 .field("class", &self.options.class)
764 .field("outcome", &self.options.outcome)
765 .field("deadline_enabled", &self.sleep.is_some())
766 .field("progress_enabled", &self.progress.is_some())
767 .field("finished", &self.finished)
768 .field("inner", &"redacted")
769 .finish()
770 }
771}
772
773impl<B> GrpcDeadlineBody<B> {
774 fn new(
775 inner: B,
776 deadline: Option<tokio::time::Instant>,
777 progress: Option<GrpcTransportProgress>,
778 options: GrpcCallOptions,
779 started: tokio::time::Instant,
780 ) -> Self {
781 Self {
782 inner: Box::pin(inner),
783 sleep: deadline.map(|deadline| Box::pin(tokio::time::sleep_until(deadline))),
784 progress,
785 options,
786 started,
787 finished: false,
788 }
789 }
790}
791
792impl<B> HttpBody for GrpcDeadlineBody<B>
793where
794 B: HttpBody,
795 B::Error: StdError + Send + Sync + 'static,
796{
797 type Data = B::Data;
798 type Error = GrpcDeadlineServiceError<B::Error>;
799
800 fn poll_frame(
801 mut self: Pin<&mut Self>,
802 context: &mut Context<'_>,
803 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
804 let this = self.as_mut().get_mut();
805 if this.finished {
806 return Poll::Ready(None);
807 }
808
809 if let Some(sleep) = this.sleep.as_mut()
810 && sleep.as_mut().poll(context).is_ready()
811 {
812 this.finished = true;
813 return Poll::Ready(Some(Err(GrpcDeadlineServiceError::Deadline(
814 local_deadline_status(this.options, this.started.elapsed()),
815 ))));
816 }
817
818 match this.inner.as_mut().poll_frame(context) {
819 Poll::Ready(Some(Ok(mut frame))) => {
820 if frame.data_ref().is_some_and(|data| data.remaining() > 0)
821 && let Some(progress) = this.progress.as_ref()
822 {
823 progress.record();
824 }
825 if let Some(trailers) = frame.trailers_mut() {
826 annotate_server_deadline_headers(trailers, this.options);
827 }
828 return Poll::Ready(Some(Ok(frame)));
829 }
830 Poll::Ready(Some(Err(source))) => {
831 this.finished = true;
832 return Poll::Ready(Some(Err(GrpcDeadlineServiceError::transport(source))));
833 }
834 Poll::Ready(None) => {
835 this.finished = true;
836 return Poll::Ready(None);
837 }
838 Poll::Pending => {}
839 }
840 Poll::Pending
841 }
842
843 fn is_end_stream(&self) -> bool {
844 self.finished || self.inner.is_end_stream()
845 }
846
847 fn size_hint(&self) -> SizeHint {
848 self.inner.size_hint()
849 }
850}
851
852impl<S, ResponseBody> Service<http::Request<Body>> for GrpcDeadlineService<S>
853where
854 S: Service<http::Request<Body>, Response = http::Response<ResponseBody>>
855 + Clone
856 + Send
857 + 'static,
858 S::Error: StdError + Send + Sync + 'static,
859 S::Future: Send + 'static,
860 ResponseBody: Send + 'static,
861{
862 type Response = http::Response<GrpcDeadlineBody<ResponseBody>>;
863 type Error = GrpcDeadlineServiceError<S::Error>;
864 type Future = BoxServiceFuture<ResponseBody, S::Error>;
865
866 fn poll_ready(&mut self, context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
867 let _ = context;
868 Poll::Ready(Ok(()))
869 }
870
871 fn call(&mut self, mut request: http::Request<Body>) -> Self::Future {
872 let mut options = request
873 .extensions()
874 .get::<GrpcCallOptions>()
875 .copied()
876 .unwrap_or_else(|| inferred_call_options(request.extensions().get::<GrpcMethod>()));
877 if let Ok(scoped) = ENCLOSING_DEADLINE.try_with(|deadline| *deadline) {
878 options.enclosing = Some(match options.enclosing {
879 Some(explicit) => {
880 GrpcEnclosingDeadline::from_instant(explicit.instant().min(scoped.instant()))
881 }
882 None => scoped,
883 });
884 }
885 let started = tokio::time::Instant::now();
886 let caller_deadline = match request.headers().get(GRPC_TIMEOUT_HEADER) {
887 Some(value) => match parse_grpc_timeout(value) {
888 Ok(Some(duration)) => match started.checked_add(duration) {
889 Some(deadline) => Some(deadline),
890 None => {
891 let status =
892 Status::invalid_argument("unrepresentable grpc-timeout metadata");
893 return Box::pin(async move {
894 Err(GrpcDeadlineServiceError::InvalidTimeout(status))
895 });
896 }
897 },
898 Ok(None) => None,
899 Err(_) => {
900 let status = Status::invalid_argument("invalid grpc-timeout metadata");
901 return Box::pin(async move {
902 Err(GrpcDeadlineServiceError::InvalidTimeout(status))
903 });
904 }
905 },
906 None => None,
907 };
908 let selected = select_deadline(
909 self.policy.duration(options.class),
910 options.enclosing,
911 request.headers().get(GRPC_TIMEOUT_HEADER),
912 started,
913 );
914 let duration = match selected {
915 Ok(duration) => duration,
916 Err(SelectDeadlineError::Expired) => {
917 let status = local_deadline_status(
918 GrpcCallOptions {
919 outcome: GrpcTimeoutOutcome::ReadAborted,
920 ..options
921 },
922 Duration::ZERO,
923 );
924 return Box::pin(async move { Err(GrpcDeadlineServiceError::Deadline(status)) });
925 }
926 Err(SelectDeadlineError::InvalidHeader) => {
927 let status = Status::invalid_argument("invalid grpc-timeout metadata");
928 return Box::pin(
929 async move { Err(GrpcDeadlineServiceError::InvalidTimeout(status)) },
930 );
931 }
932 };
933
934 let absolute_deadline = match duration {
935 Some(duration) => match started.checked_add(duration) {
936 Some(deadline) => Some(deadline),
937 None => {
938 let status = Status::invalid_argument("unrepresentable gRPC deadline");
939 return Box::pin(async move {
940 Err(GrpcDeadlineServiceError::InvalidTimeout(status))
941 });
942 }
943 },
944 None => None,
945 };
946 let mut inner = self.inner.clone();
947 Box::pin(async move {
948 let ready = async { std::future::poll_fn(|context| inner.poll_ready(context)).await };
949 let ready_result = if let Some(deadline) = absolute_deadline {
950 tokio::pin!(ready);
951 tokio::select! {
952 biased;
953 result = &mut ready => Some(result),
954 () = tokio::time::sleep_until(deadline) => None,
955 }
956 } else {
957 Some(ready.await)
958 };
959 match ready_result {
960 Some(Ok(())) => {}
961 Some(Err(source)) if error_chain_contains_timeout(&source) => {
962 let status = local_deadline_status(
963 GrpcCallOptions {
964 outcome: GrpcTimeoutOutcome::ReadAborted,
965 ..options
966 },
967 started.elapsed(),
968 );
969 return Err(GrpcDeadlineServiceError::Deadline(status));
970 }
971 Some(Err(source)) => return Err(GrpcDeadlineServiceError::transport(source)),
972 None => {
973 let status = local_deadline_status(
974 GrpcCallOptions {
975 outcome: GrpcTimeoutOutcome::ReadAborted,
976 ..options
977 },
978 started.elapsed(),
979 );
980 return Err(GrpcDeadlineServiceError::Deadline(status));
981 }
982 }
983
984 if let Some(deadline) = absolute_deadline {
985 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
986 if remaining.is_zero() {
987 let status = local_deadline_status(
988 GrpcCallOptions {
989 outcome: GrpcTimeoutOutcome::ReadAborted,
990 ..options
991 },
992 started.elapsed(),
993 );
994 return Err(GrpcDeadlineServiceError::Deadline(status));
995 }
996 let propagated = if options.class == GrpcTimeoutClass::StreamSetup {
997 caller_deadline
998 .map(|deadline| {
999 deadline.saturating_duration_since(tokio::time::Instant::now())
1000 })
1001 .filter(|duration| !duration.is_zero())
1002 } else {
1003 Some(remaining)
1004 };
1005 if let Some(propagated) = propagated {
1006 let value = grpc_timeout_value(propagated).ok_or_else(|| {
1007 GrpcDeadlineServiceError::InvalidTimeout(Status::invalid_argument(
1008 "unrepresentable grpc-timeout metadata",
1009 ))
1010 })?;
1011 let header = http::HeaderValue::from_str(&value).map_err(|_| {
1012 GrpcDeadlineServiceError::InvalidTimeout(Status::invalid_argument(
1013 "invalid grpc-timeout metadata",
1014 ))
1015 })?;
1016 request.headers_mut().insert(GRPC_TIMEOUT_HEADER, header);
1017 }
1018 }
1019
1020 let future = inner.call(request);
1021 let result = if let Some(deadline) = absolute_deadline {
1022 tokio::pin!(future);
1023 tokio::select! {
1024 biased;
1025 result = &mut future => Some(result),
1026 () = tokio::time::sleep_until(deadline) => None,
1027 }
1028 } else {
1029 Some(future.await)
1030 };
1031
1032 match result {
1033 Some(Ok(mut response)) => {
1034 sanitize_response_deadline_headers(response.headers_mut(), options);
1035 let body_deadline = (options.class != GrpcTimeoutClass::StreamSetup)
1036 .then_some(absolute_deadline)
1037 .flatten();
1038 let progress = (options.class == GrpcTimeoutClass::StreamSetup)
1039 .then(GrpcTransportProgress::new);
1040 if let Some(progress) = progress.as_ref() {
1041 response.extensions_mut().insert(progress.clone());
1042 }
1043 Ok(response.map(|body| {
1044 GrpcDeadlineBody::new(body, body_deadline, progress, options, started)
1045 }))
1046 }
1047 Some(Err(source)) if error_chain_contains_timeout(&source) => {
1048 Err(GrpcDeadlineServiceError::Deadline(local_deadline_status(
1049 options,
1050 started.elapsed(),
1051 )))
1052 }
1053 Some(Err(source)) => Err(GrpcDeadlineServiceError::transport(source)),
1054 None => Err(GrpcDeadlineServiceError::Deadline(local_deadline_status(
1055 options,
1056 started.elapsed(),
1057 ))),
1058 }
1059 })
1060 }
1061}
1062
1063#[derive(Debug)]
1065pub struct GrpcStreamDeadline {
1066 idle: Option<Duration>,
1067 idle_deadline: Option<tokio::time::Instant>,
1068 lifetime_deadline: Option<tokio::time::Instant>,
1069 enclosing: Option<GrpcEnclosingDeadline>,
1070 transport_progress: Option<GrpcTransportProgress>,
1071 observed_transport_generation: u64,
1072 started: tokio::time::Instant,
1073}
1074
1075pub enum GrpcStreamError {
1077 Deadline(GrpcDeadlineError),
1079 Status(Status),
1081}
1082
1083impl fmt::Debug for GrpcStreamError {
1084 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1085 match self {
1086 Self::Deadline(source) => formatter
1087 .debug_tuple("GrpcStreamError::Deadline")
1088 .field(source)
1089 .finish(),
1090 Self::Status(_) => formatter.write_str("GrpcStreamError::Status(redacted)"),
1091 }
1092 }
1093}
1094
1095impl fmt::Display for GrpcStreamError {
1096 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1097 match self {
1098 Self::Deadline(source) => source.fmt(formatter),
1099 Self::Status(_) => formatter.write_str("gRPC stream failed (details redacted)"),
1100 }
1101 }
1102}
1103
1104impl StdError for GrpcStreamError {
1105 fn source(&self) -> Option<&(dyn StdError + 'static)> {
1106 match self {
1107 Self::Deadline(source) => Some(source),
1108 Self::Status(_) => None,
1109 }
1110 }
1111}
1112
1113impl GrpcStreamDeadline {
1114 pub fn new(
1116 policy: GrpcTimeoutPolicy,
1117 enclosing: Option<GrpcEnclosingDeadline>,
1118 ) -> Result<Self, GrpcTimeoutConfigError> {
1119 let started = tokio::time::Instant::now();
1120 let idle = policy.stream_idle;
1121 let idle_deadline = checked_deadline(started, idle)?;
1122 let lifetime_deadline = checked_deadline(started, policy.stream_total_lifetime)?;
1123 Ok(Self {
1124 idle,
1125 idle_deadline,
1126 lifetime_deadline,
1127 enclosing,
1128 transport_progress: None,
1129 observed_transport_generation: 0,
1130 started,
1131 })
1132 }
1133
1134 #[must_use]
1136 pub fn with_transport_progress(mut self, progress: Option<GrpcTransportProgress>) -> Self {
1137 self.set_transport_progress(progress);
1138 self
1139 }
1140
1141 pub fn set_transport_progress(&mut self, progress: Option<GrpcTransportProgress>) {
1143 self.observed_transport_generation = progress
1144 .as_ref()
1145 .map_or(0, GrpcTransportProgress::generation);
1146 self.transport_progress = progress;
1147 }
1148
1149 pub fn reset_idle(&mut self) -> Result<(), GrpcTimeoutConfigError> {
1153 self.idle_deadline = checked_deadline(tokio::time::Instant::now(), self.idle)?;
1154 if let Some(progress) = self.transport_progress.as_ref() {
1155 self.observed_transport_generation = progress.generation();
1156 }
1157 Ok(())
1158 }
1159
1160 pub fn observe_decoded_message(&mut self) -> Result<(), GrpcTimeoutConfigError> {
1165 if self.transport_progress.is_some() {
1166 Ok(())
1167 } else {
1168 self.reset_idle()
1169 }
1170 }
1171
1172 #[must_use]
1174 pub fn workflow_deadline(&self) -> Option<GrpcEnclosingDeadline> {
1175 [
1176 self.lifetime_deadline,
1177 self.enclosing.map(GrpcEnclosingDeadline::instant),
1178 ]
1179 .into_iter()
1180 .flatten()
1181 .min()
1182 .map(GrpcEnclosingDeadline::from_instant)
1183 }
1184
1185 pub async fn phase<T, F>(&self, future: F) -> Result<T, GrpcStreamError>
1190 where
1191 F: Future<Output = T>,
1192 {
1193 let Some(deadline) = self.workflow_deadline().map(GrpcEnclosingDeadline::instant) else {
1194 return Ok(future.await);
1195 };
1196 if deadline <= tokio::time::Instant::now() {
1197 return Err(GrpcStreamError::Deadline(self.stream_error(
1198 GrpcTimeoutClass::StreamLifetime,
1199 GrpcTimeoutSource::Local,
1200 )));
1201 }
1202 tokio::pin!(future);
1203 tokio::select! {
1204 biased;
1205 () = tokio::time::sleep_until(deadline) => Err(GrpcStreamError::Deadline(
1206 self.stream_error(GrpcTimeoutClass::StreamLifetime, GrpcTimeoutSource::Local),
1207 )),
1208 value = &mut future => Ok(value),
1209 }
1210 }
1211
1212 pub async fn established_phase<T, F>(&mut self, future: F) -> Result<T, GrpcStreamError>
1217 where
1218 F: Future<Output = T>,
1219 {
1220 tokio::pin!(future);
1221 loop {
1222 self.observe_transport_progress()?;
1223 let now = tokio::time::Instant::now();
1224 let boundary = earliest_stream_boundary(
1225 self.idle_deadline,
1226 self.lifetime_deadline,
1227 self.enclosing.map(GrpcEnclosingDeadline::instant),
1228 );
1229 if let Some((deadline, class)) = boundary
1230 && deadline <= now
1231 {
1232 return Err(GrpcStreamError::Deadline(
1233 self.stream_error(class, GrpcTimeoutSource::Local),
1234 ));
1235 }
1236
1237 let progress = self.transport_progress.clone();
1238 let observed = self.observed_transport_generation;
1239 let progress_wait = async move {
1240 match progress {
1241 Some(progress) => progress.changed_after(observed).await,
1242 None => std::future::pending().await,
1243 }
1244 };
1245 let result = if let Some((deadline, class)) = boundary {
1246 tokio::select! {
1247 biased;
1248 () = tokio::time::sleep_until(deadline) => {
1249 return Err(GrpcStreamError::Deadline(
1250 self.stream_error(class, GrpcTimeoutSource::Local),
1251 ));
1252 }
1253 value = &mut future => return Ok(value),
1254 generation = progress_wait => generation,
1255 }
1256 } else {
1257 tokio::select! {
1258 biased;
1259 value = &mut future => return Ok(value),
1260 generation = progress_wait => generation,
1261 }
1262 };
1263 self.observed_transport_generation = result;
1264 self.idle_deadline =
1265 checked_deadline(tokio::time::Instant::now(), self.idle).map_err(|_| {
1266 GrpcStreamError::Deadline(
1267 self.stream_error(GrpcTimeoutClass::StreamIdle, GrpcTimeoutSource::Local),
1268 )
1269 })?;
1270 }
1271 }
1272
1273 pub async fn next<T, F>(&mut self, future: F) -> Result<T, GrpcStreamError>
1275 where
1276 F: Future<Output = Result<T, Status>>,
1277 {
1278 tokio::pin!(future);
1279 loop {
1280 self.observe_transport_progress()?;
1281 let now = tokio::time::Instant::now();
1282 let boundary = earliest_stream_boundary(
1283 self.idle_deadline,
1284 self.lifetime_deadline,
1285 self.enclosing.map(GrpcEnclosingDeadline::instant),
1286 );
1287 if let Some((deadline, class)) = boundary
1288 && deadline <= now
1289 {
1290 return Err(GrpcStreamError::Deadline(
1291 self.stream_error(class, GrpcTimeoutSource::Local),
1292 ));
1293 }
1294
1295 let progress = self.transport_progress.clone();
1296 let observed = self.observed_transport_generation;
1297 let progress_wait = async move {
1298 match progress {
1299 Some(progress) => progress.changed_after(observed).await,
1300 None => std::future::pending().await,
1301 }
1302 };
1303 let result = if let Some((deadline, class)) = boundary {
1304 tokio::select! {
1305 biased;
1306 () = tokio::time::sleep_until(deadline) => None,
1307 result = &mut future => Some(StreamWait::Result(result, class)),
1308 generation = progress_wait => Some(StreamWait::Progress(generation)),
1309 }
1310 } else {
1311 tokio::select! {
1312 biased;
1313 result = &mut future => Some(StreamWait::Result(
1314 result,
1315 GrpcTimeoutClass::StreamLifetime,
1316 )),
1317 generation = progress_wait => Some(StreamWait::Progress(generation)),
1318 }
1319 };
1320
1321 match result {
1322 Some(StreamWait::Progress(generation)) => {
1323 self.observed_transport_generation = generation;
1324 self.idle_deadline =
1325 checked_deadline(tokio::time::Instant::now(), self.idle).map_err(|_| {
1326 GrpcStreamError::Deadline(self.stream_error(
1327 GrpcTimeoutClass::StreamIdle,
1328 GrpcTimeoutSource::Local,
1329 ))
1330 })?;
1331 }
1332 Some(StreamWait::Result(Ok(value), _)) => {
1333 self.idle_deadline =
1334 checked_deadline(tokio::time::Instant::now(), self.idle).map_err(|_| {
1335 GrpcStreamError::Deadline(self.stream_error(
1336 GrpcTimeoutClass::StreamIdle,
1337 GrpcTimeoutSource::Local,
1338 ))
1339 })?;
1340 return Ok(value);
1341 }
1342 Some(StreamWait::Result(Err(status), class)) => {
1343 return if let Some(error) = GrpcDeadlineError::from_status(
1344 &status,
1345 class,
1346 GrpcTimeoutOutcome::StreamTerminated,
1347 self.started.elapsed(),
1348 ) {
1349 Err(GrpcStreamError::Deadline(error))
1350 } else {
1351 Err(GrpcStreamError::Status(status))
1352 };
1353 }
1354 None => {
1355 let (_, class) = boundary.unwrap_or((now, GrpcTimeoutClass::StreamLifetime));
1356 return Err(GrpcStreamError::Deadline(
1357 self.stream_error(class, GrpcTimeoutSource::Local),
1358 ));
1359 }
1360 }
1361 }
1362 }
1363
1364 fn observe_transport_progress(&mut self) -> Result<(), GrpcStreamError> {
1365 let Some(progress) = self.transport_progress.as_ref() else {
1366 return Ok(());
1367 };
1368 let generation = progress.generation();
1369 if generation == self.observed_transport_generation {
1370 return Ok(());
1371 }
1372 self.observed_transport_generation = generation;
1373 self.idle_deadline =
1374 checked_deadline(tokio::time::Instant::now(), self.idle).map_err(|_| {
1375 GrpcStreamError::Deadline(
1376 self.stream_error(GrpcTimeoutClass::StreamIdle, GrpcTimeoutSource::Local),
1377 )
1378 })?;
1379 Ok(())
1380 }
1381
1382 fn stream_error(
1383 &self,
1384 class: GrpcTimeoutClass,
1385 source: GrpcTimeoutSource,
1386 ) -> GrpcDeadlineError {
1387 GrpcDeadlineError {
1388 class,
1389 outcome: GrpcTimeoutOutcome::StreamTerminated,
1390 source,
1391 elapsed: self.started.elapsed(),
1392 }
1393 }
1394}
1395
1396enum StreamWait<T> {
1397 Result(Result<T, Status>, GrpcTimeoutClass),
1398 Progress(u64),
1399}
1400
1401#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1402enum SelectDeadlineError {
1403 Expired,
1404 InvalidHeader,
1405}
1406
1407fn select_deadline(
1408 profile: Option<Duration>,
1409 enclosing: Option<GrpcEnclosingDeadline>,
1410 existing: Option<&http::HeaderValue>,
1411 now: tokio::time::Instant,
1412) -> Result<Option<Duration>, SelectDeadlineError> {
1413 let existing = existing.map(parse_grpc_timeout).transpose()?.flatten();
1414 let enclosing = enclosing
1415 .map(GrpcEnclosingDeadline::instant)
1416 .map(|deadline| {
1417 (deadline > now)
1418 .then(|| deadline.saturating_duration_since(now))
1419 .ok_or(SelectDeadlineError::Expired)
1420 })
1421 .transpose()?;
1422 let selected = [profile, enclosing, existing].into_iter().flatten().min();
1423 if selected == Some(Duration::ZERO) {
1424 return Err(SelectDeadlineError::Expired);
1425 }
1426 Ok(selected)
1427}
1428
1429fn parse_grpc_timeout(value: &http::HeaderValue) -> Result<Option<Duration>, SelectDeadlineError> {
1430 let value = value
1431 .to_str()
1432 .map_err(|_| SelectDeadlineError::InvalidHeader)?;
1433 if value.is_empty() || value.len() > 9 {
1434 return Err(SelectDeadlineError::InvalidHeader);
1435 }
1436 let (digits, unit) = value.split_at(value.len() - 1);
1437 if digits.is_empty() || digits.len() > 8 || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
1438 return Err(SelectDeadlineError::InvalidHeader);
1439 }
1440 let amount = digits
1441 .parse::<u64>()
1442 .map_err(|_| SelectDeadlineError::InvalidHeader)?;
1443 let duration = match unit {
1444 "H" => Duration::from_secs(amount.saturating_mul(3_600)),
1445 "M" => Duration::from_secs(amount.saturating_mul(60)),
1446 "S" => Duration::from_secs(amount),
1447 "m" => Duration::from_millis(amount),
1448 "u" => Duration::from_micros(amount),
1449 "n" => Duration::from_nanos(amount),
1450 _ => return Err(SelectDeadlineError::InvalidHeader),
1451 };
1452 Ok(Some(duration))
1453}
1454
1455fn grpc_timeout_value(duration: Duration) -> Option<String> {
1456 fn format_unit(value: u128, unit: char) -> Option<String> {
1457 (value <= 99_999_999).then(|| format!("{value}{unit}"))
1458 }
1459
1460 format_unit(duration.as_nanos(), 'n')
1461 .or_else(|| format_unit(duration.as_micros(), 'u'))
1462 .or_else(|| format_unit(duration.as_millis(), 'm'))
1463 .or_else(|| format_unit(duration.as_secs().into(), 'S'))
1464 .or_else(|| format_unit((duration.as_secs() / 60).into(), 'M'))
1465 .or_else(|| format_unit((duration.as_secs() / 3_600).into(), 'H'))
1466}
1467
1468fn local_deadline_status(options: GrpcCallOptions, _elapsed: Duration) -> Status {
1469 let mut metadata = MetadataMap::new();
1470 metadata.insert(
1471 DEADLINE_SOURCE_METADATA,
1472 MetadataValue::<Ascii>::from_static("local"),
1473 );
1474 if let Ok(value) = options.class.as_str().parse() {
1475 metadata.insert(DEADLINE_CLASS_METADATA, value);
1476 }
1477 if let Ok(value) = options.outcome.as_str().parse() {
1478 metadata.insert(DEADLINE_OUTCOME_METADATA, value);
1479 }
1480 Status::with_metadata(Code::DeadlineExceeded, "gRPC deadline exceeded", metadata)
1481}
1482
1483fn annotate_server_deadline_headers(headers: &mut http::HeaderMap, options: GrpcCallOptions) {
1484 headers.insert(
1485 DEADLINE_SOURCE_METADATA,
1486 http::HeaderValue::from_static("server"),
1487 );
1488 if let Ok(value) = http::HeaderValue::from_str(options.class.as_str()) {
1489 headers.insert(DEADLINE_CLASS_METADATA, value);
1490 }
1491 if let Ok(value) = http::HeaderValue::from_str(options.outcome.as_str()) {
1492 headers.insert(DEADLINE_OUTCOME_METADATA, value);
1493 }
1494}
1495
1496fn sanitize_response_deadline_headers(headers: &mut http::HeaderMap, options: GrpcCallOptions) {
1497 let has_terminal_status = headers.contains_key("grpc-status");
1498 headers.remove(DEADLINE_SOURCE_METADATA);
1499 headers.remove(DEADLINE_CLASS_METADATA);
1500 headers.remove(DEADLINE_OUTCOME_METADATA);
1501 if has_terminal_status {
1502 annotate_server_deadline_headers(headers, options);
1503 }
1504}
1505
1506macro_rules! generated_method_profiles {
1507 (
1508 credential: [$($credential:literal),* $(,)?],
1509 stream: [$($stream:literal),* $(,)?],
1510 cleanup: [$($cleanup:literal),* $(,)?],
1511 long_read: [$($long_read:literal),* $(,)?],
1512 long_mutation: [$($long_mutation:literal),* $(,)?],
1513 ordinary_read: [$($ordinary_read:literal),* $(,)?],
1514 ordinary_mutation: [$($ordinary_mutation:literal),* $(,)?],
1515 ) => {
1516 fn reviewed_generated_call_options(method: &str) -> Option<GrpcCallOptions> {
1517 Some(match method {
1518 $($credential => GrpcCallOptions::new(
1519 GrpcTimeoutClass::CredentialSetup,
1520 GrpcTimeoutOutcome::MutationIndeterminate,
1521 ),)*
1522 $($stream => GrpcCallOptions::stream_setup(),)*
1523 $($cleanup => GrpcCallOptions::cleanup(),)*
1524 $($long_read => GrpcCallOptions::long_read(),)*
1525 $($long_mutation => GrpcCallOptions::new(
1526 GrpcTimeoutClass::LongUnary,
1527 GrpcTimeoutOutcome::MutationIndeterminate,
1528 ),)*
1529 $($ordinary_read => GrpcCallOptions::ordinary_read(),)*
1530 $($ordinary_mutation => GrpcCallOptions::ordinary_mutation(),)*
1531 _ => return None,
1532 })
1533 }
1534
1535 #[cfg(test)]
1536 const REVIEWED_GENERATED_METHODS: &[&str] = &[
1537 $($credential,)*
1538 $($stream,)*
1539 $($cleanup,)*
1540 $($long_read,)*
1541 $($long_mutation,)*
1542 $($ordinary_read,)*
1543 $($ordinary_mutation,)*
1544 ];
1545 };
1546}
1547
1548generated_method_profiles! {
1549 credential: [
1550 "AccountLocalLinkNewChallenge",
1551 "AccountLocalLinkSolveChallenge",
1552 "WalletCreateSession",
1553 ],
1554 stream: [
1555 "ListenSessionEvents",
1556 ],
1557 cleanup: [
1558 "WalletCloseSession",
1559 "AccountMigrateCancel",
1560 "SpaceJoinCancel",
1561 "ObjectClose",
1562 "ObjectCrossSpaceSearchUnsubscribe",
1563 "ObjectSearchUnsubscribe",
1564 "FileDiscardPreload",
1565 "FileCacheCancelDownload",
1566 "ProcessCancel",
1567 "ProcessUnsubscribe",
1568 "ChatUnsubscribe",
1569 "ChatUnsubscribeFromMessagePreviews",
1570 ],
1571 long_read: [
1572 "WorkspaceExport",
1573 "ObjectListExport",
1574 "ObjectExport",
1575 "TemplateExportAll",
1576 "BlockExport",
1577 "DebugExportLocalstore",
1578 "DebugExportReport",
1579 "FileDownload",
1580 ],
1581 long_mutation: [
1582 "AccountRecoverFromLegacyExport",
1583 "ObjectImport",
1584 "ObjectImportUseCase",
1585 "ObjectImportExperience",
1586 "BlockUpload",
1587 "FileUpload",
1588 ],
1589 ordinary_read: [
1590 "AppGetVersion",
1591 "AccountLocalLinkListApps",
1592 "WorkspaceGetCurrent",
1593 "WorkspaceGetAll",
1594 "SpaceInviteGetCurrent",
1595 "SpaceInviteGetGuest",
1596 "SpaceInviteView",
1597 "PublishingList",
1598 "PublishingResolveUri",
1599 "PublishingGetStatus",
1600 "ObjectShow",
1601 "ObjectGraph",
1602 "ObjectSearch",
1603 "ObjectSearchWithMeta",
1604 "ObjectCleanupSuggestions",
1605 "ObjectImportList",
1606 "ObjectImportNotionValidateToken",
1607 "ObjectDateByTimestamp",
1608 "RelationOptions",
1609 "RelationListWithValue",
1610 "ObjectRelationListAvailable",
1611 "ObjectTypeListConflictingRelations",
1612 "HistoryShowVersion",
1613 "HistoryGetVersions",
1614 "HistoryDiffVersions",
1615 "FileSpaceUsage",
1616 "FileNodeUsage",
1617 "NavigationListObjects",
1618 "NavigationGetObjectInfoWithLinks",
1619 "TemplateGetPlaceholders",
1620 "LinkPreview",
1621 "UnsplashSearch",
1622 "UnsplashDownload",
1623 "GalleryDownloadManifest",
1624 "GalleryDownloadIndex",
1625 "BlockPreview",
1626 "DebugStat",
1627 "DebugTree",
1628 "DebugTreeHeads",
1629 "DebugSpaceSummary",
1630 "DebugStackGoroutines",
1631 "DebugPing",
1632 "DebugSubscriptions",
1633 "DebugOpenedObjects",
1634 "DebugAccountSelectTrace",
1635 "DebugAnystoreObjectChanges",
1636 "DebugNetCheck",
1637 "NotificationList",
1638 "MembershipGetStatus",
1639 "MembershipIsNameValid",
1640 "MembershipGetPortalLinkUrl",
1641 "MembershipGetVerificationEmailStatus",
1642 "MembershipGetTiers",
1643 "MembershipCodeGetInfo",
1644 "MembershipV2GetProducts",
1645 "MembershipV2GetStatus",
1646 "MembershipV2GetPortalLink",
1647 "MembershipV2AnyNameIsValid",
1648 "MembershipV2CartGet",
1649 "NameServiceUserAccountGet",
1650 "NameServiceResolveName",
1651 "NameServiceResolveAnyId",
1652 "DeviceList",
1653 "ChatGetMessages",
1654 "ChatGetMessagesByIds",
1655 "ChatUnreadMessages",
1656 "ChatReadReactions",
1657 "ChatSearch",
1658 "ChatGetPinnedMessages",
1659 "AIWritingTools",
1660 "AIAutofill",
1661 "AIListSummary",
1662 ],
1663 ordinary_mutation: [
1664 "AppSetDeviceState",
1665 "AppShutdown",
1666 "WalletCreate",
1667 "WalletRecover",
1668 "WalletConvert",
1669 "AccountLocalLinkCreateApp",
1670 "AccountLocalLinkRevokeApp",
1671 "WorkspaceCreate",
1672 "WorkspaceOpen",
1673 "WorkspaceObjectAdd",
1674 "WorkspaceObjectListAdd",
1675 "WorkspaceObjectListRemove",
1676 "WorkspaceSelect",
1677 "WorkspaceSetInfo",
1678 "WorkspaceSetHomepage",
1679 "AccountRecover",
1680 "AccountMigrate",
1681 "AccountCreate",
1682 "AccountDelete",
1683 "AccountPreloadRemainingSpaces",
1684 "AccountRevertDeletion",
1685 "AccountSelect",
1686 "AccountEnableLocalNetworkSync",
1687 "AccountChangeJsonApiAddr",
1688 "AccountStop",
1689 "AccountMove",
1690 "AccountConfigUpdate",
1691 "AccountChangeNetworkConfigAndRestart",
1692 "SpaceDelete",
1693 "SpaceInviteGenerate",
1694 "SpaceInviteChange",
1695 "SpaceInviteRevoke",
1696 "SpaceJoin",
1697 "SpaceStopSharing",
1698 "SpaceRequestApprove",
1699 "SpaceRequestDecline",
1700 "SpaceLeaveApprove",
1701 "SpaceMakeShareable",
1702 "SpaceParticipantRemove",
1703 "SpaceParticipantPermissionsChange",
1704 "SpaceSetOrder",
1705 "SpaceUnsetOrder",
1706 "SpaceChangeOwnership",
1707 "SpaceDeleteCorruptedBackup",
1708 "SpaceParticipantsAddList",
1709 "PublishingCreate",
1710 "PublishingRemove",
1711 "ObjectOpen",
1712 "ObjectRefresh",
1713 "ObjectCreate",
1714 "ObjectCreateBookmark",
1715 "ObjectCreateFromUrl",
1716 "ObjectCreateSet",
1717 "ObjectSearchSubscribe",
1718 "ObjectCrossSpaceSearchSubscribe",
1719 "ObjectSubscribeIds",
1720 "ObjectGroupsSubscribe",
1721 "ObjectSetDetails",
1722 "ObjectDuplicate",
1723 "ObjectSetObjectType",
1724 "ObjectSetLayout",
1725 "ObjectSetInternalFlags",
1726 "ObjectSetIsFavorite",
1727 "ObjectSetIsArchived",
1728 "ObjectSetSource",
1729 "ObjectListDuplicate",
1730 "ObjectListDelete",
1731 "ObjectListSetIsArchived",
1732 "ObjectCleanupSuggestionIgnore",
1733 "ObjectListSetIsFavorite",
1734 "ObjectListSetObjectType",
1735 "ObjectListSetDetails",
1736 "ObjectListModifyDetailValues",
1737 "ObjectApplyTemplate",
1738 "ObjectToSet",
1739 "ObjectToCollection",
1740 "ObjectShareByLink",
1741 "ObjectUndo",
1742 "ObjectRedo",
1743 "ObjectBookmarkFetch",
1744 "ObjectCollectionAdd",
1745 "ObjectCollectionRemove",
1746 "ObjectCollectionSort",
1747 "ObjectCreateRelation",
1748 "ObjectCreateRelationOption",
1749 "RelationListRemoveOption",
1750 "RelationOptionSetOrder",
1751 "ObjectRelationAdd",
1752 "ObjectRelationDelete",
1753 "ObjectRelationAddFeatured",
1754 "ObjectRelationRemoveFeatured",
1755 "ObjectCreateObjectType",
1756 "ObjectTypeRelationAdd",
1757 "ObjectTypeRelationRemove",
1758 "ObjectTypeRecommendedRelationsSet",
1759 "ObjectTypeRecommendedFeaturedRelationsSet",
1760 "ObjectTypeResolveLayoutConflicts",
1761 "ObjectTypeSetOrder",
1762 "HistorySetVersion",
1763 "FileSpaceOffload",
1764 "FileReconcile",
1765 "FileListOffload",
1766 "FileDrop",
1767 "FileSetAutoDownload",
1768 "FileCacheDownload",
1769 "FileAutoDownloadSetLimit",
1770 "TemplateCreateFromObject",
1771 "TemplateClone",
1772 "TemplateSetPlaceholders",
1773 "TemplateDeletePlaceholders",
1774 "BlockReplace",
1775 "BlockCreate",
1776 "BlockSplit",
1777 "BlockMerge",
1778 "BlockCopy",
1779 "BlockPaste",
1780 "BlockCut",
1781 "BlockSetFields",
1782 "BlockSetCarriage",
1783 "BlockListDelete",
1784 "BlockListMoveToExistingObject",
1785 "BlockListMoveToNewObject",
1786 "BlockListConvertToObjects",
1787 "BlockListSetFields",
1788 "BlockListDuplicate",
1789 "BlockListSetBackgroundColor",
1790 "BlockListSetAlign",
1791 "BlockListSetVerticalAlign",
1792 "BlockListTurnInto",
1793 "BlockTextSetText",
1794 "BlockTextSetColor",
1795 "BlockTextSetStyle",
1796 "BlockTextSetChecked",
1797 "BlockTextSetIcon",
1798 "BlockTextListSetColor",
1799 "BlockTextListSetMark",
1800 "BlockTextListSetStyle",
1801 "BlockTextListClearStyle",
1802 "BlockTextListClearContent",
1803 "BlockFileSetName",
1804 "BlockFileSetTargetObjectId",
1805 "BlockImageSetName",
1806 "BlockVideoSetName",
1807 "BlockFileCreateAndUpload",
1808 "BlockFileListSetStyle",
1809 "BlockDataviewViewCreate",
1810 "BlockDataviewViewDelete",
1811 "BlockDataviewViewUpdate",
1812 "BlockDataviewViewSetActive",
1813 "BlockDataviewViewSetPosition",
1814 "BlockDataviewSetSource",
1815 "BlockDataviewRelationSet",
1816 "BlockDataviewRelationAdd",
1817 "BlockDataviewRelationDelete",
1818 "BlockDataviewGroupOrderUpdate",
1819 "BlockDataviewObjectOrderUpdate",
1820 "BlockDataviewObjectOrderMove",
1821 "BlockDataviewCreateFromExistingObject",
1822 "BlockDataviewFilterAdd",
1823 "BlockDataviewFilterRemove",
1824 "BlockDataviewFilterReplace",
1825 "BlockDataviewFilterSort",
1826 "BlockDataviewSortAdd",
1827 "BlockDataviewSortRemove",
1828 "BlockDataviewSortReplace",
1829 "BlockDataviewSortSort",
1830 "BlockDataviewViewRelationAdd",
1831 "BlockDataviewViewRelationRemove",
1832 "BlockDataviewViewRelationReplace",
1833 "BlockDataviewViewRelationSort",
1834 "BlockTableCreate",
1835 "BlockTableExpand",
1836 "BlockTableRowCreate",
1837 "BlockTableRowDelete",
1838 "BlockTableRowDuplicate",
1839 "BlockTableRowSetHeader",
1840 "BlockTableColumnCreate",
1841 "BlockTableColumnMove",
1842 "BlockTableColumnDelete",
1843 "BlockTableColumnDuplicate",
1844 "BlockTableRowListFill",
1845 "BlockTableRowListClean",
1846 "BlockTableColumnListFill",
1847 "BlockTableSort",
1848 "BlockCreateWidget",
1849 "BlockWidgetSetTargetId",
1850 "BlockWidgetSetLayout",
1851 "BlockWidgetSetLimit",
1852 "BlockWidgetSetViewId",
1853 "BlockLinkCreateWithObject",
1854 "BlockLinkListSetAppearance",
1855 "BlockBookmarkFetch",
1856 "BlockBookmarkCreateAndFetch",
1857 "BlockRelationSetKey",
1858 "BlockRelationAdd",
1859 "BlockDivListSetStyle",
1860 "BlockLatexSetText",
1861 "ProcessSubscribe",
1862 "LogSend",
1863 "DebugRunProfiler",
1864 "DebugCleanupReport",
1865 "InitialSetParameters",
1866 "NotificationReply",
1867 "NotificationTest",
1868 "MembershipRegisterPaymentRequest",
1869 "MembershipGetVerificationEmail",
1870 "MembershipVerifyEmailCode",
1871 "MembershipFinalize",
1872 "MembershipVerifyAppStoreReceipt",
1873 "MembershipCodeRedeem",
1874 "MembershipV2AnyNameAllocate",
1875 "MembershipV2CartUpdate",
1876 "MembershipV2SubscribeToUpdates",
1877 "BroadcastPayloadEvent",
1878 "DeviceSetName",
1879 "DeviceNetworkStateSet",
1880 "ChatAddMessage",
1881 "ChatEditMessageContent",
1882 "ChatToggleMessageReaction",
1883 "ChatDeleteMessage",
1884 "ChatSubscribeLastMessages",
1885 "ChatReadMessages",
1886 "ChatSubscribeToMessagePreviews",
1887 "ObjectChatAdd",
1888 "ObjectAddDiscussion",
1889 "ChatReadAll",
1890 "ChatSetPinnedMessages",
1891 "ChatAddNotificationSubscriber",
1892 "ChatRemoveNotificationSubscriber",
1893 "AIObjectCreateFromUrl",
1894 "PushNotificationRegisterToken",
1895 "PushNotificationSetSpaceMode",
1896 "PushNotificationSetForceModeIds",
1897 "PushNotificationResetIds",
1898 ],
1899}
1900
1901fn inferred_call_options(method: Option<&GrpcMethod<'_>>) -> GrpcCallOptions {
1902 let Some(method) = method else {
1903 return GrpcCallOptions::default();
1904 };
1905 if method.service() != "anytype.ClientCommands" {
1906 return GrpcCallOptions::default();
1907 }
1908 reviewed_generated_call_options(method.method()).unwrap_or_default()
1909}
1910
1911fn metadata_text<'a>(metadata: &'a MetadataMap, key: &'static str) -> Option<&'a str> {
1912 metadata.get(key).and_then(|value| value.to_str().ok())
1913}
1914
1915fn error_chain_contains_timeout(error: &(dyn StdError + 'static)) -> bool {
1916 let mut source = Some(error);
1917 while let Some(current) = source {
1918 if current.downcast_ref::<TimeoutExpired>().is_some() {
1919 return true;
1920 }
1921 source = current.source();
1922 }
1923 false
1924}
1925
1926fn checked_deadline(
1927 start: tokio::time::Instant,
1928 duration: Option<Duration>,
1929) -> Result<Option<tokio::time::Instant>, GrpcTimeoutConfigError> {
1930 duration
1931 .map(|duration| {
1932 start
1933 .checked_add(duration)
1934 .ok_or(GrpcTimeoutConfigError::UnrepresentableDeadline)
1935 })
1936 .transpose()
1937}
1938
1939fn earliest_stream_boundary(
1940 idle: Option<tokio::time::Instant>,
1941 lifetime: Option<tokio::time::Instant>,
1942 enclosing: Option<tokio::time::Instant>,
1943) -> Option<(tokio::time::Instant, GrpcTimeoutClass)> {
1944 [
1945 idle.map(|deadline| (deadline, GrpcTimeoutClass::StreamIdle)),
1946 lifetime.map(|deadline| (deadline, GrpcTimeoutClass::StreamLifetime)),
1947 enclosing.map(|deadline| (deadline, GrpcTimeoutClass::StreamLifetime)),
1948 ]
1949 .into_iter()
1950 .flatten()
1951 .min_by_key(|(deadline, _)| *deadline)
1952}
1953
1954#[cfg(test)]
1955mod tests {
1956 use std::{
1957 convert::Infallible,
1958 sync::{
1959 Arc, Mutex,
1960 atomic::{AtomicUsize, Ordering},
1961 },
1962 };
1963
1964 use tokio::sync::oneshot;
1965
1966 use super::*;
1967
1968 #[derive(Clone)]
1969 struct ScriptService {
1970 calls: Arc<AtomicUsize>,
1971 header: Arc<Mutex<Option<oneshot::Sender<Option<String>>>>>,
1972 }
1973
1974 #[derive(Debug)]
1975 struct PendingBody;
1976
1977 impl HttpBody for PendingBody {
1978 type Data = tonic::codegen::Bytes;
1979 type Error = Infallible;
1980
1981 fn poll_frame(
1982 self: Pin<&mut Self>,
1983 _context: &mut Context<'_>,
1984 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
1985 Poll::Pending
1986 }
1987
1988 fn is_end_stream(&self) -> bool {
1989 false
1990 }
1991
1992 fn size_hint(&self) -> SizeHint {
1993 SizeHint::default()
1994 }
1995 }
1996
1997 struct StatusErrorBody {
1998 status: Option<Status>,
1999 }
2000
2001 #[derive(Debug)]
2002 struct HostileTransportError(&'static str);
2003
2004 impl fmt::Display for HostileTransportError {
2005 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2006 formatter.write_str(self.0)
2007 }
2008 }
2009
2010 impl StdError for HostileTransportError {}
2011
2012 impl HttpBody for StatusErrorBody {
2013 type Data = tonic::codegen::Bytes;
2014 type Error = Status;
2015
2016 fn poll_frame(
2017 mut self: Pin<&mut Self>,
2018 _context: &mut Context<'_>,
2019 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
2020 Poll::Ready(self.status.take().map(Err))
2021 }
2022 }
2023
2024 #[derive(Clone)]
2025 struct StatusReadyFailureService;
2026
2027 impl Service<http::Request<Body>> for StatusReadyFailureService {
2028 type Response = http::Response<Body>;
2029 type Error = Status;
2030 type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
2031
2032 fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
2033 Poll::Ready(Err(Status::unavailable("HOSTILE_READINESS_SECRET")))
2034 }
2035
2036 fn call(&mut self, _request: http::Request<Body>) -> Self::Future {
2037 std::future::ready(Ok(http::Response::new(Body::empty())))
2038 }
2039 }
2040
2041 #[derive(Debug)]
2042 struct TrailerBody {
2043 frame: Option<Frame<tonic::codegen::Bytes>>,
2044 }
2045
2046 impl HttpBody for TrailerBody {
2047 type Data = tonic::codegen::Bytes;
2048 type Error = Infallible;
2049
2050 fn poll_frame(
2051 mut self: Pin<&mut Self>,
2052 _context: &mut Context<'_>,
2053 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
2054 Poll::Ready(self.frame.take().map(Ok))
2055 }
2056
2057 fn is_end_stream(&self) -> bool {
2058 self.frame.is_none()
2059 }
2060
2061 fn size_hint(&self) -> SizeHint {
2062 SizeHint::default()
2063 }
2064 }
2065
2066 #[derive(Debug)]
2067 struct SignaledDataBody {
2068 ready: oneshot::Receiver<()>,
2069 data: tonic::codegen::Bytes,
2070 emitted: bool,
2071 }
2072
2073 impl HttpBody for SignaledDataBody {
2074 type Data = tonic::codegen::Bytes;
2075 type Error = Infallible;
2076
2077 fn poll_frame(
2078 mut self: Pin<&mut Self>,
2079 context: &mut Context<'_>,
2080 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
2081 if self.emitted {
2082 return Poll::Pending;
2083 }
2084 match Pin::new(&mut self.ready).poll(context) {
2085 Poll::Ready(_) => {
2086 self.emitted = true;
2087 let data = std::mem::take(&mut self.data);
2088 Poll::Ready(Some(Ok(Frame::data(data))))
2089 }
2090 Poll::Pending => Poll::Pending,
2091 }
2092 }
2093
2094 fn is_end_stream(&self) -> bool {
2095 false
2096 }
2097
2098 fn size_hint(&self) -> SizeHint {
2099 SizeHint::default()
2100 }
2101 }
2102
2103 #[derive(Clone)]
2104 struct PendingReadyService {
2105 calls: Arc<AtomicUsize>,
2106 }
2107
2108 impl Service<http::Request<Body>> for PendingReadyService {
2109 type Response = http::Response<Body>;
2110 type Error = Infallible;
2111 type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
2112
2113 fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
2114 Poll::Pending
2115 }
2116
2117 fn call(&mut self, _request: http::Request<Body>) -> Self::Future {
2118 self.calls.fetch_add(1, Ordering::Relaxed);
2119 std::future::ready(Ok(http::Response::new(Body::empty())))
2120 }
2121 }
2122
2123 #[derive(Clone)]
2124 struct ReadyAtDeadlineService {
2125 calls: Arc<AtomicUsize>,
2126 ready: Arc<Mutex<Pin<Box<tokio::time::Sleep>>>>,
2127 header: Arc<Mutex<Option<oneshot::Sender<Option<String>>>>>,
2128 }
2129
2130 impl Service<http::Request<Body>> for ReadyAtDeadlineService {
2131 type Response = http::Response<Body>;
2132 type Error = Infallible;
2133 type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
2134
2135 fn poll_ready(&mut self, context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
2136 let Ok(mut ready) = self.ready.lock() else {
2137 return Poll::Pending;
2138 };
2139 ready.as_mut().poll(context).map(|()| Ok(()))
2140 }
2141
2142 fn call(&mut self, request: http::Request<Body>) -> Self::Future {
2143 self.calls.fetch_add(1, Ordering::Relaxed);
2144 if let Ok(mut slot) = self.header.lock()
2145 && let Some(sender) = slot.take()
2146 {
2147 let value = request
2148 .headers()
2149 .get(GRPC_TIMEOUT_HEADER)
2150 .and_then(|value| value.to_str().ok())
2151 .map(str::to_owned);
2152 let _ = sender.send(value);
2153 }
2154 std::future::ready(Ok(http::Response::new(Body::empty())))
2155 }
2156 }
2157
2158 struct DelayedGrpcBody {
2159 delay: Pin<Box<tokio::time::Sleep>>,
2160 state: u8,
2161 }
2162
2163 impl HttpBody for DelayedGrpcBody {
2164 type Data = tonic::codegen::Bytes;
2165 type Error = Infallible;
2166
2167 fn poll_frame(
2168 mut self: Pin<&mut Self>,
2169 context: &mut Context<'_>,
2170 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
2171 match self.state {
2172 0 => {
2173 if self.delay.as_mut().poll(context).is_pending() {
2174 return Poll::Pending;
2175 }
2176 self.state = 1;
2177 Poll::Ready(Some(Ok(Frame::data(tonic::codegen::Bytes::from_static(
2178 &[0, 0, 0, 0, 0],
2179 )))))
2180 }
2181 1 => {
2182 self.state = 2;
2183 let mut trailers = http::HeaderMap::new();
2184 trailers.insert("grpc-status", http::HeaderValue::from_static("0"));
2185 Poll::Ready(Some(Ok(Frame::trailers(trailers))))
2186 }
2187 _ => Poll::Ready(None),
2188 }
2189 }
2190
2191 fn is_end_stream(&self) -> bool {
2192 self.state >= 2
2193 }
2194
2195 fn size_hint(&self) -> SizeHint {
2196 SizeHint::default()
2197 }
2198 }
2199
2200 #[derive(Clone)]
2201 struct DelayedStreamService {
2202 header: Arc<Mutex<Option<oneshot::Sender<Option<String>>>>>,
2203 }
2204
2205 impl tonic::server::NamedService for DelayedStreamService {
2206 const NAME: &'static str = "anytype.ClientCommands";
2207 }
2208
2209 impl Service<http::Request<Body>> for DelayedStreamService {
2210 type Response = http::Response<Body>;
2211 type Error = Infallible;
2212 type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
2213
2214 fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
2215 Poll::Ready(Ok(()))
2216 }
2217
2218 fn call(&mut self, request: http::Request<Body>) -> Self::Future {
2219 if let Ok(mut slot) = self.header.lock()
2220 && let Some(sender) = slot.take()
2221 {
2222 let header = request
2223 .headers()
2224 .get(GRPC_TIMEOUT_HEADER)
2225 .and_then(|value| value.to_str().ok())
2226 .map(str::to_owned);
2227 let _ = sender.send(header);
2228 }
2229 let body = DelayedGrpcBody {
2230 delay: Box::pin(tokio::time::sleep(Duration::from_millis(1_100))),
2231 state: 0,
2232 };
2233 let response = http::Response::builder()
2234 .status(200)
2235 .header("content-type", "application/grpc")
2236 .body(Body::new(body))
2237 .unwrap_or_else(|_| http::Response::new(Body::empty()));
2238 std::future::ready(Ok(response))
2239 }
2240 }
2241
2242 struct ListenerIncoming {
2243 listener: tokio::net::TcpListener,
2244 }
2245
2246 impl tonic::codegen::tokio_stream::Stream for ListenerIncoming {
2247 type Item = std::io::Result<tokio::net::TcpStream>;
2248
2249 fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
2250 self.listener
2251 .poll_accept(context)
2252 .map(|result| Some(result.map(|(stream, _)| stream)))
2253 }
2254 }
2255
2256 #[derive(Clone)]
2257 struct ServerDeadlineService;
2258
2259 impl Service<http::Request<Body>> for ServerDeadlineService {
2260 type Response = http::Response<Body>;
2261 type Error = Infallible;
2262 type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
2263
2264 fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
2265 Poll::Ready(Ok(()))
2266 }
2267
2268 fn call(&mut self, _request: http::Request<Body>) -> Self::Future {
2269 let mut trailers = http::HeaderMap::new();
2270 trailers.insert("grpc-status", http::HeaderValue::from_static("4"));
2271 trailers.insert(
2272 "grpc-message",
2273 http::HeaderValue::from_static("UNTRUSTED_SERVER_SECRET"),
2274 );
2275 trailers.insert(
2276 DEADLINE_SOURCE_METADATA,
2277 http::HeaderValue::from_static("local"),
2278 );
2279 trailers.insert(
2280 DEADLINE_OUTCOME_METADATA,
2281 http::HeaderValue::from_static("mutation_indeterminate"),
2282 );
2283 let body = Body::new(TrailerBody {
2284 frame: Some(Frame::trailers(trailers)),
2285 });
2286 let response = http::Response::builder()
2287 .status(200)
2288 .header("content-type", "application/grpc")
2289 .body(body)
2290 .unwrap_or_else(|_| http::Response::new(Body::empty()));
2291 std::future::ready(Ok(response))
2292 }
2293 }
2294
2295 #[derive(Clone)]
2296 struct HeaderThenStallService {
2297 header: Arc<Mutex<Option<oneshot::Sender<Option<String>>>>>,
2298 }
2299
2300 impl Service<http::Request<Body>> for HeaderThenStallService {
2301 type Response = http::Response<Body>;
2302 type Error = Infallible;
2303 type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
2304
2305 fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
2306 Poll::Ready(Ok(()))
2307 }
2308
2309 fn call(&mut self, request: http::Request<Body>) -> Self::Future {
2310 if let Ok(mut slot) = self.header.lock()
2311 && let Some(sender) = slot.take()
2312 {
2313 let value = request
2314 .headers()
2315 .get(GRPC_TIMEOUT_HEADER)
2316 .and_then(|value| value.to_str().ok())
2317 .map(str::to_owned);
2318 let _ = sender.send(value);
2319 }
2320 let response = http::Response::builder()
2321 .status(200)
2322 .header("content-type", "application/grpc")
2323 .body(Body::new(PendingBody))
2324 .unwrap_or_else(|_| http::Response::new(Body::empty()));
2325 std::future::ready(Ok(response))
2326 }
2327 }
2328
2329 fn header_then_stall_service() -> (HeaderThenStallService, oneshot::Receiver<Option<String>>) {
2330 let (sender, receiver) = oneshot::channel();
2331 (
2332 HeaderThenStallService {
2333 header: Arc::new(Mutex::new(Some(sender))),
2334 },
2335 receiver,
2336 )
2337 }
2338
2339 impl Service<http::Request<Body>> for ScriptService {
2340 type Response = http::Response<Body>;
2341 type Error = Infallible;
2342 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
2343
2344 fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
2345 Poll::Ready(Ok(()))
2346 }
2347
2348 fn call(&mut self, request: http::Request<Body>) -> Self::Future {
2349 self.calls.fetch_add(1, Ordering::Relaxed);
2350 if let Ok(mut slot) = self.header.lock()
2351 && let Some(sender) = slot.take()
2352 {
2353 let value = request
2354 .headers()
2355 .get(GRPC_TIMEOUT_HEADER)
2356 .and_then(|value| value.to_str().ok())
2357 .map(str::to_owned);
2358 let _ = sender.send(value);
2359 }
2360 Box::pin(std::future::pending())
2361 }
2362 }
2363
2364 fn scripted_service() -> (
2365 ScriptService,
2366 Arc<AtomicUsize>,
2367 oneshot::Receiver<Option<String>>,
2368 ) {
2369 let calls = Arc::new(AtomicUsize::new(0));
2370 let (sender, receiver) = oneshot::channel();
2371 (
2372 ScriptService {
2373 calls: calls.clone(),
2374 header: Arc::new(Mutex::new(Some(sender))),
2375 },
2376 calls,
2377 receiver,
2378 )
2379 }
2380
2381 #[test]
2382 fn defaults_and_finite_ranges_match_policy() {
2383 let policy = GrpcTimeoutPolicy::default();
2384 assert_eq!(policy.credential_setup, Some(Duration::from_secs(120)));
2385 assert_eq!(policy.ordinary_unary, Some(Duration::from_secs(120)));
2386 assert_eq!(policy.long_unary, Some(Duration::from_secs(1_800)));
2387 assert_eq!(policy.stream_setup, Some(Duration::from_secs(120)));
2388 assert_eq!(policy.stream_idle, None);
2389 assert_eq!(policy.stream_total_lifetime, None);
2390 assert_eq!(policy.cleanup, Some(Duration::from_secs(5)));
2391
2392 for duration in [Duration::ZERO, Duration::from_millis(999)] {
2393 assert!(
2394 GrpcTimeoutPolicy {
2395 ordinary_unary: Some(duration),
2396 ..policy
2397 }
2398 .validate()
2399 .is_err()
2400 );
2401 }
2402 assert!(
2403 GrpcTimeoutPolicy {
2404 long_unary: Some(Duration::from_secs(7_201)),
2405 ..policy
2406 }
2407 .validate()
2408 .is_err()
2409 );
2410 assert!(
2411 GrpcTimeoutPolicy {
2412 cleanup: Some(Duration::from_secs(31)),
2413 ..policy
2414 }
2415 .validate()
2416 .is_err()
2417 );
2418 }
2419
2420 #[test]
2421 fn environment_override_has_exact_grammar_and_limited_scope() {
2422 let disabled = GrpcTimeoutPolicy::from_environment(Some(OsString::from("0")))
2423 .expect("zero disables inherited generic profiles");
2424 assert_eq!(disabled.credential_setup, None);
2425 assert_eq!(disabled.ordinary_unary, None);
2426 assert_eq!(disabled.long_unary, None);
2427 assert_eq!(disabled.stream_setup, None);
2428 assert_eq!(disabled.cleanup, Some(Duration::from_secs(5)));
2429
2430 let finite = GrpcTimeoutPolicy::from_environment(Some(OsString::from("17")))
2431 .expect("finite override");
2432 assert_eq!(finite.credential_setup, Some(Duration::from_secs(17)));
2433 assert_eq!(finite.long_unary, Some(Duration::from_secs(17)));
2434 assert_eq!(finite.stream_idle, None);
2435
2436 for malformed in [
2437 "",
2438 "00",
2439 "01",
2440 " 1",
2441 "+1",
2442 "-1",
2443 "1.0",
2444 "3601",
2445 "18446744073709551616",
2446 ] {
2447 assert!(
2448 GrpcTimeoutPolicy::from_environment(Some(OsString::from(malformed))).is_err(),
2449 "accepted {malformed:?}"
2450 );
2451 }
2452 }
2453
2454 #[test]
2455 fn explicit_policy_resolution_returns_the_validated_policy() {
2456 let explicit = GrpcTimeoutPolicy {
2457 credential_setup: None,
2458 ordinary_unary: Some(Duration::from_secs(11)),
2459 long_unary: Some(Duration::from_secs(7_200)),
2460 stream_setup: None,
2461 stream_idle: Some(Duration::from_secs(12)),
2462 stream_total_lifetime: None,
2463 cleanup: Some(Duration::from_secs(30)),
2464 };
2465 assert_eq!(
2466 GrpcTimeoutPolicy::resolve(Some(explicit)).expect("explicit policy"),
2467 explicit
2468 );
2469 assert!(
2470 GrpcDeadlineService::try_new(
2471 (),
2472 GrpcTimeoutPolicy {
2473 cleanup: Some(Duration::from_secs(31)),
2474 ..explicit
2475 }
2476 )
2477 .is_err()
2478 );
2479 }
2480
2481 #[test]
2482 fn generated_method_profiles_cover_the_checked_in_client_inventory() {
2483 let generated_source = include_str!("gen/anytype.rs");
2484 let mut generated = generated_source
2485 .split("GrpcMethod::new(")
2486 .skip(1)
2487 .filter_map(|tail| {
2488 let mut quoted = tail.split('"');
2489 let _ = quoted.next()?;
2490 let service = quoted.next()?;
2491 let _ = quoted.next()?;
2492 let method = quoted.next()?;
2493 (service == "anytype.ClientCommands").then_some(method)
2494 })
2495 .collect::<Vec<_>>();
2496 generated.sort_unstable();
2497 generated.dedup();
2498
2499 let reviewed_count = REVIEWED_GENERATED_METHODS.len();
2500 let mut reviewed = REVIEWED_GENERATED_METHODS.to_vec();
2501 reviewed.sort_unstable();
2502 reviewed.dedup();
2503 assert_eq!(
2504 reviewed.len(),
2505 reviewed_count,
2506 "profile authority has duplicates"
2507 );
2508 let generated_without_profile = generated
2509 .iter()
2510 .copied()
2511 .filter(|method| reviewed.binary_search(method).is_err())
2512 .collect::<Vec<_>>();
2513 assert!(
2514 generated_without_profile.is_empty(),
2515 "generated RPCs missing reviewed profiles: {generated_without_profile:?}"
2516 );
2517 let reviewed_without_generated = reviewed
2518 .iter()
2519 .copied()
2520 .filter(|method| generated.binary_search(method).is_err())
2521 .collect::<Vec<_>>();
2522 let mut expected_forward_profiles =
2525 ["ObjectCleanupSuggestions", "ObjectCleanupSuggestionIgnore"]
2526 .into_iter()
2527 .filter(|method| generated.binary_search(method).is_err())
2528 .collect::<Vec<_>>();
2529 expected_forward_profiles.sort_unstable();
2530 assert_eq!(
2531 reviewed_without_generated, expected_forward_profiles,
2532 "profile authority contains an unexpected method absent from generated RPCs"
2533 );
2534 }
2535
2536 #[test]
2537 fn generated_method_defaults_are_closed_and_conservative() {
2538 let read = GrpcMethod::new("anytype.ClientCommands", "ObjectShow");
2539 assert_eq!(
2540 inferred_call_options(Some(&read)).outcome,
2541 GrpcTimeoutOutcome::ReadAborted
2542 );
2543 let stream = GrpcMethod::new("anytype.ClientCommands", "ListenSessionEvents");
2544 assert_eq!(
2545 inferred_call_options(Some(&stream)).class,
2546 GrpcTimeoutClass::StreamSetup
2547 );
2548 let import = GrpcMethod::new("anytype.ClientCommands", "ObjectImport");
2549 assert_eq!(
2550 inferred_call_options(Some(&import)),
2551 GrpcCallOptions::new(
2552 GrpcTimeoutClass::LongUnary,
2553 GrpcTimeoutOutcome::MutationIndeterminate,
2554 )
2555 );
2556 let close = GrpcMethod::new("anytype.ClientCommands", "ObjectClose");
2557 assert_eq!(
2558 inferred_call_options(Some(&close)).class,
2559 GrpcTimeoutClass::Cleanup
2560 );
2561 let unknown = GrpcMethod::new("anytype.ClientCommands", "FutureMutation");
2562 assert_eq!(
2563 inferred_call_options(Some(&unknown)).outcome,
2564 GrpcTimeoutOutcome::MutationIndeterminate
2565 );
2566 }
2567
2568 #[test]
2569 fn cleanup_suggestion_methods_have_explicit_read_and_mutation_outcomes() {
2570 let suggestions = GrpcMethod::new("anytype.ClientCommands", "ObjectCleanupSuggestions");
2571 assert_eq!(
2572 inferred_call_options(Some(&suggestions)),
2573 GrpcCallOptions::ordinary_read()
2574 );
2575 let ignore = GrpcMethod::new("anytype.ClientCommands", "ObjectCleanupSuggestionIgnore");
2576 assert_eq!(
2577 inferred_call_options(Some(&ignore)),
2578 GrpcCallOptions::ordinary_mutation()
2579 );
2580 }
2581
2582 #[cfg(unix)]
2583 #[test]
2584 fn environment_rejects_non_unicode() {
2585 use std::os::unix::ffi::OsStringExt;
2586
2587 assert!(GrpcTimeoutPolicy::from_environment(Some(OsString::from_vec(vec![0xff]))).is_err());
2588 }
2589
2590 #[tokio::test(start_paused = true)]
2591 async fn request_header_and_local_deadline_use_the_same_budget() {
2592 let (inner, calls, header) = scripted_service();
2593 let mut service = GrpcDeadlineService::try_new(
2594 inner,
2595 GrpcTimeoutPolicy {
2596 ordinary_unary: Some(Duration::from_secs(10)),
2597 ..GrpcTimeoutPolicy::default()
2598 },
2599 )
2600 .expect("valid policy");
2601 let request = http::Request::new(Body::empty());
2602 let call = tokio::spawn(async move { service.call(request).await });
2603
2604 assert_eq!(
2605 header.await.expect("captured header").as_deref(),
2606 Some("10000000u")
2607 );
2608 assert_eq!(calls.load(Ordering::Relaxed), 1);
2609 tokio::time::advance(Duration::from_secs(10)).await;
2610 let error = call
2611 .await
2612 .expect("service task")
2613 .expect_err("local timeout");
2614 assert!(matches!(
2615 error,
2616 GrpcDeadlineServiceError::Deadline(ref status)
2617 if status.code() == Code::DeadlineExceeded
2618 ));
2619 let source = error.source().expect("payload-free deadline source");
2620 assert!(!format!("{source:?}").contains("SECRET"));
2621 }
2622
2623 #[tokio::test(start_paused = true)]
2624 async fn scoped_enclosing_deadline_caps_generated_header_and_local_wait() {
2625 let (inner, calls, header) = scripted_service();
2626 let mut service = GrpcDeadlineService::try_new(
2627 inner,
2628 GrpcTimeoutPolicy {
2629 ordinary_unary: Some(Duration::from_secs(120)),
2630 ..GrpcTimeoutPolicy::default()
2631 },
2632 )
2633 .expect("valid policy");
2634 let enclosing = GrpcEnclosingDeadline::from_now(Duration::from_secs(3))
2635 .expect("representable enclosing deadline");
2636 let call = tokio::spawn(async move {
2637 scope_grpc_enclosing_deadline(enclosing, async move {
2638 service.call(http::Request::new(Body::empty())).await
2639 })
2640 .await
2641 });
2642
2643 assert_eq!(
2644 header.await.expect("captured header").as_deref(),
2645 Some("3000000u")
2646 );
2647 assert_eq!(calls.load(Ordering::Relaxed), 1);
2648 tokio::time::advance(Duration::from_secs(3)).await;
2649 assert!(matches!(
2650 call.await.expect("service task").expect_err("local timeout"),
2651 GrpcDeadlineServiceError::Deadline(ref status)
2652 if status.code() == Code::DeadlineExceeded
2653 ));
2654 }
2655
2656 #[tokio::test(start_paused = true)]
2657 async fn generated_tonic_client_preserves_local_timeout_classification() {
2658 use crate::{
2659 anytype::{ClientCommandsClient, rpc::account::local_link::list_apps},
2660 deadline::with_grpc_call_options,
2661 };
2662
2663 let (inner, calls, header) = scripted_service();
2664 let service = GrpcDeadlineService::try_new(
2665 inner,
2666 GrpcTimeoutPolicy {
2667 ordinary_unary: Some(Duration::from_secs(4)),
2668 ..GrpcTimeoutPolicy::default()
2669 },
2670 )
2671 .expect("valid policy");
2672 let mut client = ClientCommandsClient::new(service);
2673 let request = with_grpc_call_options(
2674 Request::new(list_apps::Request {}),
2675 GrpcCallOptions::ordinary_read(),
2676 );
2677 let call = tokio::spawn(async move { client.account_local_link_list_apps(request).await });
2678 assert_eq!(
2679 header.await.expect("captured header").as_deref(),
2680 Some("4000000u")
2681 );
2682 assert_eq!(calls.load(Ordering::Relaxed), 1);
2683 tokio::time::advance(Duration::from_secs(4)).await;
2684 let status = call
2685 .await
2686 .expect("generated client task")
2687 .expect_err("local timeout");
2688 let error = GrpcDeadlineError::from_status(
2689 &status,
2690 GrpcTimeoutClass::LongUnary,
2691 GrpcTimeoutOutcome::MutationIndeterminate,
2692 Duration::from_secs(4),
2693 )
2694 .expect("classified deadline");
2695 assert_eq!(error.class, GrpcTimeoutClass::OrdinaryUnary);
2696 assert_eq!(error.outcome, GrpcTimeoutOutcome::ReadAborted);
2697 assert_eq!(error.source, GrpcTimeoutSource::Local);
2698 }
2699
2700 #[tokio::test(start_paused = true)]
2701 async fn unary_deadline_remains_active_after_response_headers() {
2702 use crate::anytype::{ClientCommandsClient, rpc::account::local_link::list_apps};
2703
2704 let (inner, header) = header_then_stall_service();
2705 let service = GrpcDeadlineService::try_new(
2706 inner,
2707 GrpcTimeoutPolicy {
2708 ordinary_unary: Some(Duration::from_secs(6)),
2709 ..GrpcTimeoutPolicy::default()
2710 },
2711 )
2712 .expect("valid policy");
2713 let mut client = ClientCommandsClient::new(service);
2714 let request = with_grpc_call_options(
2715 Request::new(list_apps::Request {}),
2716 GrpcCallOptions::ordinary_read(),
2717 );
2718 let call = tokio::spawn(async move { client.account_local_link_list_apps(request).await });
2719 assert_eq!(
2720 header.await.expect("captured header").as_deref(),
2721 Some("6000000u")
2722 );
2723 tokio::time::advance(Duration::from_secs(6)).await;
2724 let status = call
2725 .await
2726 .expect("generated client task")
2727 .expect_err("stalled unary body must expire");
2728 let error = GrpcDeadlineError::from_status(
2729 &status,
2730 GrpcTimeoutClass::OrdinaryUnary,
2731 GrpcTimeoutOutcome::ReadAborted,
2732 Duration::from_secs(6),
2733 )
2734 .expect("classified deadline");
2735 assert_eq!(error.source, GrpcTimeoutSource::Local);
2736 }
2737
2738 #[tokio::test(start_paused = true)]
2739 async fn successful_stream_headers_disarm_the_setup_deadline() {
2740 use crate::anytype::{ClientCommandsClient, StreamRequest};
2741
2742 let (inner, header) = header_then_stall_service();
2743 let service = GrpcDeadlineService::try_new(
2744 inner,
2745 GrpcTimeoutPolicy {
2746 stream_setup: Some(Duration::from_secs(5)),
2747 ..GrpcTimeoutPolicy::default()
2748 },
2749 )
2750 .expect("valid policy");
2751 let mut client = ClientCommandsClient::new(service);
2752 let request = Request::new(StreamRequest {
2753 token: String::new(),
2754 });
2755 let request = with_grpc_call_options(request, GrpcCallOptions::stream_setup());
2756 let response = client
2757 .listen_session_events(request)
2758 .await
2759 .expect("successful response headers");
2760 assert_eq!(header.await.expect("captured header").as_deref(), None);
2761 let mut stream = response.into_inner();
2762 tokio::time::advance(Duration::from_secs(6)).await;
2763 assert!(
2764 tokio::time::timeout(Duration::from_millis(1), stream.message())
2765 .await
2766 .is_err(),
2767 "setup deadline must not terminate an established stream body"
2768 );
2769 }
2770
2771 #[tokio::test(start_paused = true)]
2772 async fn stream_setup_preserves_a_caller_supplied_whole_call_timeout() {
2773 use crate::anytype::{ClientCommandsClient, StreamRequest};
2774
2775 let (inner, header) = header_then_stall_service();
2776 let service = GrpcDeadlineService::try_new(
2777 inner,
2778 GrpcTimeoutPolicy {
2779 stream_setup: Some(Duration::from_secs(5)),
2780 ..GrpcTimeoutPolicy::default()
2781 },
2782 )
2783 .expect("valid policy");
2784 let mut client = ClientCommandsClient::new(service);
2785 let mut request = Request::new(StreamRequest {
2786 token: String::new(),
2787 });
2788 request.set_timeout(Duration::from_secs(2));
2789 let request = with_grpc_call_options(request, GrpcCallOptions::stream_setup());
2790 let _response = client
2791 .listen_session_events(request)
2792 .await
2793 .expect("successful response headers");
2794 assert_eq!(
2795 header.await.expect("captured caller timeout").as_deref(),
2796 Some("2000000u")
2797 );
2798 }
2799
2800 #[tokio::test]
2801 async fn tonic_server_stream_remains_established_beyond_setup_budget() {
2802 use crate::anytype::{ClientCommandsClient, StreamRequest};
2803
2804 let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
2805 .await
2806 .expect("bind tonic test server");
2807 let address = listener.local_addr().expect("tonic test address");
2808 let (header_sender, header_receiver) = oneshot::channel();
2809 let service = DelayedStreamService {
2810 header: Arc::new(Mutex::new(Some(header_sender))),
2811 };
2812 let (shutdown_sender, shutdown_receiver) = oneshot::channel();
2813 let server = tokio::spawn(async move {
2814 tonic::transport::Server::builder()
2815 .add_service(service)
2816 .serve_with_incoming_shutdown(ListenerIncoming { listener }, async move {
2817 let _ = shutdown_receiver.await;
2818 })
2819 .await
2820 });
2821 let channel = tonic::transport::Endpoint::from_shared(format!("http://{address}"))
2822 .expect("valid tonic test endpoint")
2823 .connect()
2824 .await
2825 .expect("connect tonic test client");
2826 let deadline_service = GrpcDeadlineService::try_new(
2827 channel,
2828 GrpcTimeoutPolicy {
2829 stream_setup: Some(Duration::from_secs(1)),
2830 ..GrpcTimeoutPolicy::default()
2831 },
2832 )
2833 .expect("valid stream policy");
2834 let mut client = ClientCommandsClient::new(deadline_service);
2835 let request = with_grpc_call_options(
2836 Request::new(StreamRequest {
2837 token: String::new(),
2838 }),
2839 GrpcCallOptions::stream_setup(),
2840 );
2841 let response = client
2842 .listen_session_events(request)
2843 .await
2844 .expect("stream response headers");
2845 assert_eq!(
2846 header_receiver.await.expect("captured server header"),
2847 None,
2848 "stream setup must remain a local-only header boundary"
2849 );
2850 assert!(
2851 response
2852 .extensions()
2853 .get::<GrpcTransportProgress>()
2854 .is_some(),
2855 "tonic response extensions must carry raw transport progress"
2856 );
2857
2858 let mut stream = response.into_inner();
2859 let message = tokio::time::timeout(Duration::from_secs(2), stream.message())
2860 .await
2861 .expect("established stream wait")
2862 .expect("stream transport")
2863 .expect("delayed event");
2864 assert!(message.context_id.is_empty());
2865
2866 let _ = shutdown_sender.send(());
2867 server
2868 .await
2869 .expect("tonic server task")
2870 .expect("tonic server shutdown");
2871 }
2872
2873 #[tokio::test]
2874 async fn server_deadline_uses_request_classification_and_cannot_spoof_local_source() {
2875 use crate::anytype::{ClientCommandsClient, rpc::account::local_link::list_apps};
2876
2877 let service =
2878 GrpcDeadlineService::try_new(ServerDeadlineService, GrpcTimeoutPolicy::default())
2879 .expect("valid policy");
2880 let mut client = ClientCommandsClient::new(service);
2881 let request = with_grpc_call_options(
2882 Request::new(list_apps::Request {}),
2883 GrpcCallOptions::ordinary_read(),
2884 );
2885 let status = client
2886 .account_local_link_list_apps(request)
2887 .await
2888 .expect_err("server deadline");
2889 let error = GrpcDeadlineError::from_status(
2890 &status,
2891 GrpcTimeoutClass::LongUnary,
2892 GrpcTimeoutOutcome::MutationIndeterminate,
2893 Duration::from_millis(1),
2894 )
2895 .expect("classified deadline");
2896 assert_eq!(error.class, GrpcTimeoutClass::OrdinaryUnary);
2897 assert_eq!(error.outcome, GrpcTimeoutOutcome::ReadAborted);
2898 assert_eq!(error.source, GrpcTimeoutSource::Server);
2899 assert!(!error.to_string().contains("UNTRUSTED_SERVER_SECRET"));
2900 }
2901
2902 #[tokio::test(start_paused = true)]
2903 async fn enclosing_deadline_and_existing_tighter_timeout_win() {
2904 let now = tokio::time::Instant::now();
2905 let enclosing = GrpcEnclosingDeadline::from_instant(now + Duration::from_secs(7));
2906 let selected = select_deadline(Some(Duration::from_secs(120)), Some(enclosing), None, now)
2907 .expect("valid deadline");
2908 assert_eq!(selected, Some(Duration::from_secs(7)));
2909
2910 let header = http::HeaderValue::from_static("3000000u");
2911 let selected = select_deadline(
2912 Some(Duration::from_secs(120)),
2913 Some(enclosing),
2914 Some(&header),
2915 now,
2916 )
2917 .expect("valid deadline");
2918 assert_eq!(selected, Some(Duration::from_secs(3)));
2919
2920 let exhausted = http::HeaderValue::from_static("0n");
2921 assert_eq!(
2922 select_deadline(None, None, Some(&exhausted), now),
2923 Err(SelectDeadlineError::Expired)
2924 );
2925 }
2926
2927 #[tokio::test(start_paused = true)]
2928 async fn expired_enclosing_deadline_prevents_dispatch() {
2929 let (inner, calls, _header) = scripted_service();
2930 let mut service = GrpcDeadlineService::try_new(inner, GrpcTimeoutPolicy::default())
2931 .expect("valid policy");
2932 let mut request = http::Request::new(Body::empty());
2933 request
2934 .extensions_mut()
2935 .insert(GrpcCallOptions::ordinary_mutation().enclosing(
2936 GrpcEnclosingDeadline::from_instant(tokio::time::Instant::now()),
2937 ));
2938 let error = service.call(request).await.expect_err("expired deadline");
2939 assert_eq!(calls.load(Ordering::Relaxed), 0);
2940 let status = match &error {
2941 GrpcDeadlineServiceError::Deadline(status) => status,
2942 other => panic!("unexpected service error: {other:?}"),
2943 };
2944 assert_eq!(status.code(), Code::DeadlineExceeded);
2945 let classified = GrpcDeadlineError::from_status(
2946 status,
2947 GrpcTimeoutClass::OrdinaryUnary,
2948 GrpcTimeoutOutcome::MutationIndeterminate,
2949 Duration::ZERO,
2950 );
2951 assert_eq!(
2952 classified.map(|error| error.outcome),
2953 Some(GrpcTimeoutOutcome::ReadAborted)
2954 );
2955 assert!(error.source().is_some());
2956 }
2957
2958 #[tokio::test(start_paused = true)]
2959 async fn readiness_wait_consumes_cleanup_and_enclosing_budget() {
2960 let calls = Arc::new(AtomicUsize::new(0));
2961 let inner = PendingReadyService {
2962 calls: calls.clone(),
2963 };
2964 let mut service = GrpcDeadlineService::try_new(inner, GrpcTimeoutPolicy::default())
2965 .expect("valid policy");
2966 let enclosing = GrpcEnclosingDeadline::from_now(Duration::from_secs(2))
2967 .expect("valid enclosing deadline");
2968 let options = GrpcCallOptions::cleanup().enclosing(enclosing);
2969 let mut request = http::Request::new(Body::empty());
2970 request.extensions_mut().insert(options);
2971 let call = tokio::spawn(async move { service.call(request).await });
2972
2973 tokio::time::advance(Duration::from_secs(2)).await;
2974 let error = call
2975 .await
2976 .expect("service task")
2977 .expect_err("pending readiness must expire");
2978 let status = match error {
2979 GrpcDeadlineServiceError::Deadline(status) => status,
2980 other => panic!("unexpected service error: {other:?}"),
2981 };
2982 let classified = GrpcDeadlineError::from_status(
2983 &status,
2984 GrpcTimeoutClass::Cleanup,
2985 GrpcTimeoutOutcome::MutationIndeterminate,
2986 Duration::from_secs(2),
2987 )
2988 .expect("classified readiness deadline");
2989 assert_eq!(classified.class, GrpcTimeoutClass::Cleanup);
2990 assert_eq!(classified.outcome, GrpcTimeoutOutcome::ReadAborted);
2991 assert_eq!(calls.load(Ordering::Relaxed), 0);
2992 }
2993
2994 #[tokio::test(start_paused = true)]
2995 async fn stream_setup_ready_at_exact_deadline_does_not_dispatch() {
2996 let calls = Arc::new(AtomicUsize::new(0));
2997 let ready_at = tokio::time::Instant::now() + Duration::from_secs(1);
2998 let inner = ReadyAtDeadlineService {
2999 calls: calls.clone(),
3000 ready: Arc::new(Mutex::new(Box::pin(tokio::time::sleep_until(ready_at)))),
3001 header: Arc::new(Mutex::new(None)),
3002 };
3003 let mut service = GrpcDeadlineService::try_new(
3004 inner,
3005 GrpcTimeoutPolicy {
3006 stream_setup: Some(Duration::from_secs(1)),
3007 ..GrpcTimeoutPolicy::default()
3008 },
3009 )
3010 .expect("valid policy");
3011 let mut request = http::Request::new(Body::empty());
3012 request
3013 .extensions_mut()
3014 .insert(GrpcCallOptions::stream_setup());
3015 let call = tokio::spawn(async move { service.call(request).await });
3016
3017 tokio::task::yield_now().await;
3018 tokio::time::advance(Duration::from_secs(1)).await;
3019 let error = call
3020 .await
3021 .expect("service task")
3022 .expect_err("exhausted setup deadline");
3023 assert!(matches!(error, GrpcDeadlineServiceError::Deadline(_)));
3024 assert_eq!(calls.load(Ordering::Relaxed), 0);
3025 }
3026
3027 #[tokio::test(start_paused = true)]
3028 async fn caller_stream_timeout_propagates_only_its_remaining_absolute_budget() {
3029 let calls = Arc::new(AtomicUsize::new(0));
3030 let ready_at = tokio::time::Instant::now() + Duration::from_millis(1_900);
3031 let (header_sender, header_receiver) = oneshot::channel();
3032 let inner = ReadyAtDeadlineService {
3033 calls: calls.clone(),
3034 ready: Arc::new(Mutex::new(Box::pin(tokio::time::sleep_until(ready_at)))),
3035 header: Arc::new(Mutex::new(Some(header_sender))),
3036 };
3037 let mut service = GrpcDeadlineService::try_new(
3038 inner,
3039 GrpcTimeoutPolicy {
3040 stream_setup: Some(Duration::from_secs(5)),
3041 ..GrpcTimeoutPolicy::default()
3042 },
3043 )
3044 .expect("valid policy");
3045 let mut request = http::Request::new(Body::empty());
3046 request
3047 .headers_mut()
3048 .insert(GRPC_TIMEOUT_HEADER, http::HeaderValue::from_static("2S"));
3049 request
3050 .extensions_mut()
3051 .insert(GrpcCallOptions::stream_setup());
3052 let call = tokio::spawn(async move { service.call(request).await });
3053
3054 tokio::task::yield_now().await;
3055 tokio::time::advance(Duration::from_millis(1_900)).await;
3056 call.await
3057 .expect("service task")
3058 .expect("remaining caller timeout dispatches");
3059 let header = header_receiver
3060 .await
3061 .expect("captured remaining caller timeout")
3062 .expect("caller timeout header preserved");
3063 let remaining = parse_grpc_timeout(
3064 &http::HeaderValue::from_str(&header).expect("valid propagated header"),
3065 )
3066 .expect("parse propagated header")
3067 .expect("finite propagated header");
3068 assert!((Duration::from_millis(99)..=Duration::from_millis(100)).contains(&remaining));
3069 assert_eq!(calls.load(Ordering::Relaxed), 1);
3070 }
3071
3072 #[tokio::test(start_paused = true)]
3073 async fn expired_body_deadline_precedes_queued_data_and_trailers() {
3074 let options = GrpcCallOptions::ordinary_read();
3075 for frame in [
3076 Frame::data(tonic::codegen::Bytes::from_static(b"queued")),
3077 Frame::trailers(http::HeaderMap::new()),
3078 ] {
3079 let deadline = tokio::time::Instant::now() + Duration::from_secs(1);
3080 let mut body = GrpcDeadlineBody::new(
3081 TrailerBody { frame: Some(frame) },
3082 Some(deadline),
3083 None,
3084 options,
3085 tokio::time::Instant::now(),
3086 );
3087 tokio::time::advance(Duration::from_secs(1)).await;
3088 let frame = std::future::poll_fn(|context| Pin::new(&mut body).poll_frame(context))
3089 .await
3090 .expect("deadline frame");
3091 assert!(matches!(frame, Err(GrpcDeadlineServiceError::Deadline(_))));
3092 }
3093 }
3094
3095 #[tokio::test(start_paused = true)]
3096 async fn raw_body_progress_resets_idle_before_a_message_decodes() {
3097 let progress = GrpcTransportProgress::new();
3098 let (ready, receiver) = oneshot::channel();
3099 let body = SignaledDataBody {
3100 ready: receiver,
3101 data: tonic::codegen::Bytes::from_static(b"partial"),
3102 emitted: false,
3103 };
3104 let mut body = GrpcDeadlineBody::new(
3105 body,
3106 None,
3107 Some(progress.clone()),
3108 GrpcCallOptions::stream_setup(),
3109 tokio::time::Instant::now(),
3110 );
3111 let policy = GrpcTimeoutPolicy {
3112 stream_idle: Some(Duration::from_secs(5)),
3113 ..GrpcTimeoutPolicy::default()
3114 };
3115 let mut deadlines = GrpcStreamDeadline::new(policy, None)
3116 .expect("stream deadlines")
3117 .with_transport_progress(Some(progress));
3118 let waiting = tokio::spawn(async move {
3119 let undecoded = std::future::poll_fn(move |context| {
3120 let _ = Pin::new(&mut body).poll_frame(context);
3121 Poll::<Result<(), Status>>::Pending
3122 });
3123 deadlines.next(undecoded).await
3124 });
3125
3126 tokio::time::advance(Duration::from_secs(4)).await;
3127 let _ = ready.send(());
3128 tokio::task::yield_now().await;
3129 tokio::time::advance(Duration::from_secs(4)).await;
3130 assert!(
3131 !waiting.is_finished(),
3132 "raw progress must reset stream idle"
3133 );
3134 tokio::time::advance(Duration::from_secs(1)).await;
3135 let error = waiting
3136 .await
3137 .expect("stream task")
3138 .expect_err("idle expires after progress window");
3139 assert!(matches!(
3140 error,
3141 GrpcStreamError::Deadline(GrpcDeadlineError {
3142 class: GrpcTimeoutClass::StreamIdle,
3143 ..
3144 })
3145 ));
3146 }
3147
3148 #[tokio::test(start_paused = true)]
3149 async fn decoded_delivery_does_not_grant_a_second_idle_window_after_raw_progress() {
3150 let progress = GrpcTransportProgress::new();
3151 let mut deadlines = GrpcStreamDeadline::new(
3152 GrpcTimeoutPolicy {
3153 stream_idle: Some(Duration::from_secs(5)),
3154 ..GrpcTimeoutPolicy::default()
3155 },
3156 None,
3157 )
3158 .expect("stream deadlines")
3159 .with_transport_progress(Some(progress.clone()));
3160 let control = tokio::spawn(async move {
3161 let result = deadlines
3162 .established_phase(tokio::time::sleep(Duration::from_secs(8)))
3163 .await;
3164 (deadlines, result)
3165 });
3166
3167 tokio::task::yield_now().await;
3168 tokio::time::advance(Duration::from_secs(4)).await;
3169 progress.record();
3170 tokio::task::yield_now().await;
3171 tokio::time::advance(Duration::from_secs(4)).await;
3172 let (mut deadlines, result) = control.await.expect("control phase task");
3173 result.expect("raw progress keeps long control work alive");
3174 deadlines
3175 .observe_decoded_message()
3176 .expect("decoded delivery observation");
3177
3178 let waiting = tokio::spawn(async move {
3179 deadlines
3180 .established_phase(std::future::pending::<()>())
3181 .await
3182 });
3183 tokio::time::advance(Duration::from_millis(999)).await;
3184 assert!(!waiting.is_finished());
3185 tokio::time::advance(Duration::from_millis(1)).await;
3186 let error = waiting
3187 .await
3188 .expect("idle task")
3189 .expect_err("idle remains anchored to raw DATA arrival");
3190 assert!(matches!(
3191 error,
3192 GrpcStreamError::Deadline(GrpcDeadlineError {
3193 class: GrpcTimeoutClass::StreamIdle,
3194 ..
3195 })
3196 ));
3197 }
3198
3199 #[tokio::test(start_paused = true)]
3200 async fn empty_raw_data_does_not_reset_stream_idle() {
3201 let progress = GrpcTransportProgress::new();
3202 let (ready, receiver) = oneshot::channel();
3203 let body = SignaledDataBody {
3204 ready: receiver,
3205 data: tonic::codegen::Bytes::new(),
3206 emitted: false,
3207 };
3208 let mut body = GrpcDeadlineBody::new(
3209 body,
3210 None,
3211 Some(progress.clone()),
3212 GrpcCallOptions::stream_setup(),
3213 tokio::time::Instant::now(),
3214 );
3215 let policy = GrpcTimeoutPolicy {
3216 stream_idle: Some(Duration::from_secs(5)),
3217 ..GrpcTimeoutPolicy::default()
3218 };
3219 let mut deadlines = GrpcStreamDeadline::new(policy, None)
3220 .expect("stream deadlines")
3221 .with_transport_progress(Some(progress));
3222 let waiting = tokio::spawn(async move {
3223 let undecoded = std::future::poll_fn(move |context| {
3224 let _ = Pin::new(&mut body).poll_frame(context);
3225 Poll::<Result<(), Status>>::Pending
3226 });
3227 deadlines.next(undecoded).await
3228 });
3229
3230 tokio::time::advance(Duration::from_secs(4)).await;
3231 let _ = ready.send(());
3232 tokio::task::yield_now().await;
3233 tokio::time::advance(Duration::from_secs(1)).await;
3234 let error = waiting
3235 .await
3236 .expect("stream task")
3237 .expect_err("empty DATA must not reset idle");
3238 assert!(matches!(
3239 error,
3240 GrpcStreamError::Deadline(GrpcDeadlineError {
3241 class: GrpcTimeoutClass::StreamIdle,
3242 ..
3243 })
3244 ));
3245 }
3246
3247 async fn assert_dedicated_reader_progresses_during_stalled_work() {
3248 let progress = GrpcTransportProgress::new();
3249 let (ready, receiver) = oneshot::channel();
3250 let mut body = GrpcDeadlineBody::new(
3251 SignaledDataBody {
3252 ready: receiver,
3253 data: tonic::codegen::Bytes::from_static(b"partial"),
3254 emitted: false,
3255 },
3256 None,
3257 Some(progress.clone()),
3258 GrpcCallOptions::stream_setup(),
3259 tokio::time::Instant::now(),
3260 );
3261 let reader = tokio::spawn(async move {
3262 std::future::poll_fn(move |context| {
3263 let _ = Pin::new(&mut body).poll_frame(context);
3264 Poll::<()>::Pending
3265 })
3266 .await;
3267 });
3268 let mut deadlines = GrpcStreamDeadline::new(
3269 GrpcTimeoutPolicy {
3270 stream_idle: Some(Duration::from_secs(5)),
3271 ..GrpcTimeoutPolicy::default()
3272 },
3273 None,
3274 )
3275 .expect("stream deadlines")
3276 .with_transport_progress(Some(progress));
3277 let work = tokio::spawn(async move {
3278 deadlines
3279 .established_phase(std::future::pending::<()>())
3280 .await
3281 });
3282
3283 tokio::task::yield_now().await;
3284 tokio::time::advance(Duration::from_secs(4)).await;
3285 let _ = ready.send(());
3286 tokio::task::yield_now().await;
3287 tokio::time::advance(Duration::from_secs(4)).await;
3288 assert!(!work.is_finished(), "raw progress must reset stream idle");
3289 tokio::time::advance(Duration::from_secs(1)).await;
3290 let error = work
3291 .await
3292 .expect("established work task")
3293 .expect_err("idle expires after the raw-progress window");
3294 assert!(matches!(
3295 error,
3296 GrpcStreamError::Deadline(GrpcDeadlineError {
3297 class: GrpcTimeoutClass::StreamIdle,
3298 ..
3299 })
3300 ));
3301 reader.abort();
3302 }
3303
3304 #[tokio::test(start_paused = true)]
3305 async fn raw_progress_remains_observable_during_stalled_control_work() {
3306 assert_dedicated_reader_progresses_during_stalled_work().await;
3307 }
3308
3309 #[tokio::test(start_paused = true)]
3310 async fn raw_progress_remains_observable_during_stalled_output_work() {
3311 assert_dedicated_reader_progresses_during_stalled_work().await;
3312 }
3313
3314 #[tokio::test(start_paused = true)]
3315 async fn raw_progress_remains_observable_during_stalled_resubscribe_work() {
3316 assert_dedicated_reader_progresses_during_stalled_work().await;
3317 }
3318
3319 #[test]
3320 fn debug_output_redacts_inner_errors_statuses_and_bodies() {
3321 let transport =
3322 GrpcDeadlineServiceError::<Status>::transport(Status::internal("TRANSPORT_SECRET"));
3323 assert!(!format!("{transport:?}").contains("TRANSPORT_SECRET"));
3324 let source = transport.source().expect("payload-free transport source");
3325 assert!(!source.to_string().contains("TRANSPORT_SECRET"));
3326 assert!(!format!("{source:?}").contains("TRANSPORT_SECRET"));
3327 assert_eq!(
3328 Status::from_error(Box::new(transport)).code(),
3329 Code::Internal
3330 );
3331 let deadline = GrpcDeadlineServiceError::<Status>::Deadline(Status::deadline_exceeded(
3332 "DEADLINE_SECRET",
3333 ));
3334 assert!(!format!("{deadline:?}").contains("DEADLINE_SECRET"));
3335 let source = deadline.source().expect("payload-free deadline source");
3336 assert!(!source.to_string().contains("DEADLINE_SECRET"));
3337 assert!(!format!("{source:?}").contains("DEADLINE_SECRET"));
3338 let invalid = GrpcDeadlineServiceError::<Status>::InvalidTimeout(Status::invalid_argument(
3339 "INVALID_SECRET",
3340 ));
3341 assert!(!format!("{invalid:?}").contains("INVALID_SECRET"));
3342 let source = invalid.source().expect("payload-free invalid source");
3343 assert!(!source.to_string().contains("INVALID_SECRET"));
3344 assert!(!format!("{source:?}").contains("INVALID_SECRET"));
3345 let stream = GrpcStreamError::Status(Status::internal("SERVER_SECRET"));
3346 assert!(!format!("{stream:?}").contains("SERVER_SECRET"));
3347 let body = GrpcDeadlineBody::new(
3348 PendingBody,
3349 None,
3350 None,
3351 GrpcCallOptions::ordinary_read(),
3352 tokio::time::Instant::now(),
3353 );
3354 assert!(!format!("{body:?}").contains("PendingBody"));
3355 let service =
3356 GrpcDeadlineService::new_resolved("INNER_SECRET", GrpcTimeoutPolicy::default());
3357 assert!(!format!("{service:?}").contains("INNER_SECRET"));
3358 }
3359
3360 #[tokio::test]
3361 async fn redacted_transport_errors_preserve_readiness_and_body_status_codes() {
3362 let mut service = GrpcDeadlineService::new_resolved(
3363 StatusReadyFailureService,
3364 GrpcTimeoutPolicy::default(),
3365 );
3366 let readiness = service
3367 .call(http::Request::new(Body::empty()))
3368 .await
3369 .expect_err("readiness failure");
3370 let readiness_text = format!("{readiness:?} {readiness}");
3371 assert!(!readiness_text.contains("HOSTILE_READINESS_SECRET"));
3372 let readiness_status = Status::from_error(Box::new(readiness));
3373 assert_eq!(readiness_status.code(), Code::Unavailable);
3374 assert!(!format!("{readiness_status:?}").contains("HOSTILE_READINESS_SECRET"));
3375
3376 let mut body = GrpcDeadlineBody::new(
3377 StatusErrorBody {
3378 status: Some(Status::cancelled("HOSTILE_BODY_SECRET")),
3379 },
3380 None,
3381 None,
3382 GrpcCallOptions::ordinary_read(),
3383 tokio::time::Instant::now(),
3384 );
3385 let body_error = std::future::poll_fn(|context| Pin::new(&mut body).poll_frame(context))
3386 .await
3387 .expect("body failure frame")
3388 .expect_err("body status failure");
3389 let body_text = format!("{body_error:?} {body_error}");
3390 assert!(!body_text.contains("HOSTILE_BODY_SECRET"));
3391 let body_status = Status::from_error(Box::new(body_error));
3392 assert_eq!(body_status.code(), Code::Cancelled);
3393 assert!(!format!("{body_status:?}").contains("HOSTILE_BODY_SECRET"));
3394 }
3395
3396 #[test]
3397 fn redacted_transport_errors_preserve_non_status_tonic_classification() {
3398 let timeout = GrpcDeadlineServiceError::<TimeoutExpired>::transport(TimeoutExpired(()));
3399 assert_eq!(
3400 Status::from_error(Box::new(timeout)).code(),
3401 Code::Cancelled
3402 );
3403
3404 let connect =
3405 GrpcDeadlineServiceError::<tonic::ConnectError>::transport(tonic::ConnectError(
3406 Box::new(HostileTransportError("HOSTILE_CONNECT_SECRET\ncontrol")),
3407 ));
3408 let rendered = format!("{connect:?} {connect}");
3409 assert!(!rendered.contains("HOSTILE_CONNECT_SECRET"));
3410 let status = Status::from_error(Box::new(connect));
3411 assert_eq!(status.code(), Code::Unavailable);
3412 assert!(!format!("{status:?}").contains("HOSTILE_CONNECT_SECRET"));
3413 }
3414
3415 #[tokio::test(start_paused = true)]
3416 async fn stream_idle_resets_but_lifetime_does_not() {
3417 let policy = GrpcTimeoutPolicy {
3418 stream_idle: Some(Duration::from_secs(3)),
3419 stream_total_lifetime: Some(Duration::from_secs(8)),
3420 ..GrpcTimeoutPolicy::default()
3421 };
3422 let mut deadlines = GrpcStreamDeadline::new(policy, None).expect("stream deadlines");
3423 tokio::time::advance(Duration::from_secs(2)).await;
3424 assert!(matches!(
3425 deadlines.next(async { Ok::<_, Status>(1) }).await,
3426 Ok(1)
3427 ));
3428 tokio::time::advance(Duration::from_secs(2)).await;
3429 assert!(matches!(
3430 deadlines.next(async { Ok::<_, Status>(2) }).await,
3431 Ok(2)
3432 ));
3433 tokio::time::advance(Duration::from_secs(3)).await;
3434 let error = deadlines
3435 .next(std::future::pending::<Result<(), Status>>())
3436 .await
3437 .expect_err("idle timeout");
3438 let GrpcStreamError::Deadline(error) = error else {
3439 panic!("expected deadline error");
3440 };
3441 assert_eq!(error.class, GrpcTimeoutClass::StreamIdle);
3442 assert_eq!(error.outcome, GrpcTimeoutOutcome::StreamTerminated);
3443 }
3444
3445 #[tokio::test(start_paused = true)]
3446 async fn stream_progress_and_reopen_do_not_reset_total_lifetime() {
3447 let policy = GrpcTimeoutPolicy {
3448 stream_idle: Some(Duration::from_secs(4)),
3449 stream_total_lifetime: Some(Duration::from_secs(6)),
3450 ..GrpcTimeoutPolicy::default()
3451 };
3452 let mut deadlines = GrpcStreamDeadline::new(policy, None).expect("stream deadlines");
3453 tokio::time::advance(Duration::from_secs(3)).await;
3454 assert!(matches!(
3455 deadlines.next(async { Ok::<_, Status>(()) }).await,
3456 Ok(())
3457 ));
3458 tokio::time::advance(Duration::from_secs(2)).await;
3459 deadlines.reset_idle().expect("reopen resets idle only");
3460 tokio::time::advance(Duration::from_secs(1)).await;
3461 let error = deadlines
3462 .next(std::future::pending::<Result<(), Status>>())
3463 .await
3464 .expect_err("lifetime timeout");
3465 let GrpcStreamError::Deadline(error) = error else {
3466 panic!("expected deadline error");
3467 };
3468 assert_eq!(error.class, GrpcTimeoutClass::StreamLifetime);
3469 }
3470
3471 #[test]
3472 fn server_and_local_statuses_share_classification_but_retain_source() {
3473 let options = GrpcCallOptions::ordinary_mutation();
3474 let local = local_deadline_status(options, Duration::from_secs(1));
3475 let local = GrpcDeadlineError::from_status(
3476 &local,
3477 GrpcTimeoutClass::LongUnary,
3478 GrpcTimeoutOutcome::ReadAborted,
3479 Duration::from_secs(1),
3480 )
3481 .expect("local deadline");
3482 assert_eq!(local.class, GrpcTimeoutClass::OrdinaryUnary);
3483 assert_eq!(local.outcome, GrpcTimeoutOutcome::MutationIndeterminate);
3484 assert_eq!(local.source, GrpcTimeoutSource::Local);
3485
3486 let server = Status::deadline_exceeded("untrusted upstream detail");
3487 let server = GrpcDeadlineError::from_status(
3488 &server,
3489 GrpcTimeoutClass::LongUnary,
3490 GrpcTimeoutOutcome::ReadAborted,
3491 Duration::from_secs(1),
3492 )
3493 .expect("server deadline");
3494 assert_eq!(server.class, GrpcTimeoutClass::LongUnary);
3495 assert_eq!(server.source, GrpcTimeoutSource::Server);
3496 assert!(!server.to_string().contains("untrusted upstream detail"));
3497 }
3498}