1use crate::{
2 ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT, MESSAGE_TOO_LARGE_KEY,
3 grpc::IsUserLongPoll,
4 request_extensions::{IsWorkerTaskLongPoll, NoRetryOnMatching, RetryConfigForCall},
5};
6use backon::{BackoffBuilder, ExponentialBuilder};
7use futures_retry::{ErrorHandler, FutureRetry, RetryPolicy};
8use std::{
9 error::Error,
10 fmt::Debug,
11 future::Future,
12 time::{Duration, Instant},
13};
14use tonic::Code;
15
16#[doc(hidden)]
18pub const RETRYABLE_ERROR_CODES: [Code; 7] = [
19 Code::DataLoss,
20 Code::Internal,
21 Code::Unknown,
22 Code::ResourceExhausted,
23 Code::Aborted,
24 Code::OutOfRange,
25 Code::Unavailable,
26];
27const LONG_POLL_FATAL_GRACE: Duration = Duration::from_secs(60);
28
29#[derive(Clone, Debug, PartialEq)]
31pub struct RetryOptions {
32 pub initial_interval: Duration,
34 pub randomization_factor: f64,
37 pub multiplier: f64,
39 pub max_interval: Duration,
41 pub max_elapsed_time: Option<Duration>,
44 pub max_retries: usize,
46}
47
48impl Default for RetryOptions {
49 fn default() -> Self {
50 Self {
51 initial_interval: Duration::from_millis(100), randomization_factor: 0.2, multiplier: 1.7, max_interval: Duration::from_secs(5), max_elapsed_time: Some(Duration::from_secs(10)), max_retries: 10,
57 }
58 }
59}
60
61impl RetryOptions {
62 pub(crate) const fn task_poll_retry_policy() -> Self {
63 Self {
64 initial_interval: Duration::from_millis(200),
65 randomization_factor: 0.2,
66 multiplier: 2.0,
67 max_interval: Duration::from_secs(10),
68 max_elapsed_time: None,
69 max_retries: 0,
70 }
71 }
72
73 pub(crate) const fn throttle_retry_policy() -> Self {
74 Self {
75 initial_interval: Duration::from_secs(1),
76 randomization_factor: 0.2,
77 multiplier: 2.0,
78 max_interval: Duration::from_secs(10),
79 max_elapsed_time: None,
80 max_retries: 0,
81 }
82 }
83
84 pub const fn no_retries() -> Self {
86 Self {
87 initial_interval: Duration::from_secs(0),
88 randomization_factor: 0.0,
89 multiplier: 1.0,
90 max_interval: Duration::from_secs(0),
91 max_elapsed_time: None,
92 max_retries: 1,
93 }
94 }
95
96 pub(crate) fn get_call_info<R>(
97 &self,
98 call_name: &'static str,
99 request: Option<&tonic::Request<R>>,
100 ) -> CallInfo {
101 let mut call_type = CallType::Normal;
102 let mut retry_short_circuit = None;
103 let mut retry_cfg_override = None;
104 if let Some(r) = request.as_ref() {
105 let ext = r.extensions();
106 if ext.get::<IsUserLongPoll>().is_some() {
107 call_type = CallType::UserLongPoll;
108 } else if ext.get::<IsWorkerTaskLongPoll>().is_some() {
109 call_type = CallType::TaskLongPoll;
110 }
111
112 retry_short_circuit = ext.get::<NoRetryOnMatching>().cloned();
113 retry_cfg_override = ext.get::<RetryConfigForCall>().cloned();
114 }
115 let retry_cfg = if let Some(ovr) = retry_cfg_override {
116 ovr.0
117 } else if call_type == CallType::TaskLongPoll {
118 RetryOptions::task_poll_retry_policy()
119 } else {
120 self.clone()
121 };
122 CallInfo {
123 call_type,
124 call_name,
125 retry_cfg,
126 retry_short_circuit,
127 }
128 }
129
130 fn jittered_backoff(&self) -> JitteredBackoff {
131 let inner = ExponentialBuilder::new()
132 .with_min_delay(self.initial_interval)
133 .with_factor(self.multiplier as f32)
134 .with_max_delay(self.max_interval)
135 .without_max_times()
136 .build();
137 JitteredBackoff {
138 inner,
139 randomization_factor: self.randomization_factor,
140 max_elapsed_time: self.max_elapsed_time,
141 started_at: Instant::now(),
142 }
143 }
144}
145
146pub(crate) fn make_future_retry<R, F, Fut>(
147 info: CallInfo,
148 factory: F,
149) -> FutureRetry<F, TonicErrorHandler>
150where
151 F: FnMut() -> Fut + Unpin,
152 Fut: Future<Output = Result<R, tonic::Status>>,
153{
154 FutureRetry::new(
155 factory,
156 TonicErrorHandler::new(info, RetryOptions::throttle_retry_policy()),
157 )
158}
159
160#[doc(hidden)]
161pub fn jittered(base: Duration, randomization_factor: f64) -> Duration {
162 if randomization_factor <= 0.0 {
163 return base;
164 }
165 let base_secs = base.as_secs_f64();
170 let spread = randomization_factor * base_secs;
171 let offset = spread * (2.0 * rand::random::<f64>() - 1.0);
172 Duration::try_from_secs_f64((base_secs + offset).max(0.0)).unwrap_or(base)
173}
174
175#[derive(Debug)]
176struct JitteredBackoff {
177 inner: backon::ExponentialBackoff,
178 randomization_factor: f64,
179 max_elapsed_time: Option<Duration>,
180 started_at: Instant,
181}
182
183impl JitteredBackoff {
184 fn next_backoff(&mut self) -> Option<Duration> {
185 let base = self.inner.next()?;
189 let delay = jittered(base, self.randomization_factor);
190 if let Some(max_elapsed_time) = self.max_elapsed_time
191 && self.started_at.elapsed() + delay > max_elapsed_time
192 {
193 return None;
194 }
195 Some(delay)
196 }
197}
198
199#[derive(Debug)]
200pub(crate) struct TonicErrorHandler {
201 backoff: JitteredBackoff,
202 throttle_backoff: JitteredBackoff,
203 max_interval: Duration,
204 retry_started_at: Instant,
205 max_retries: usize,
206 call_type: CallType,
207 call_name: &'static str,
208 retry_short_circuit: Option<NoRetryOnMatching>,
209}
210
211impl TonicErrorHandler {
212 fn new(call_info: CallInfo, throttle_cfg: RetryOptions) -> Self {
213 Self {
214 call_type: call_info.call_type,
215 call_name: call_info.call_name,
216 max_retries: call_info.retry_cfg.max_retries,
217 max_interval: call_info.retry_cfg.max_interval,
218 backoff: call_info.retry_cfg.jittered_backoff(),
219 throttle_backoff: throttle_cfg.jittered_backoff(),
220 retry_started_at: Instant::now(),
221 retry_short_circuit: call_info.retry_short_circuit,
222 }
223 }
224
225 fn maybe_log_retry(&self, cur_attempt: usize, err: &tonic::Status) {
226 let mut do_log = false;
227 if self.max_retries == 0 && cur_attempt > 5 {
229 do_log = true;
230 }
231 if self.max_retries > 0 && cur_attempt * 2 >= self.max_retries {
233 do_log = true;
234 }
235
236 if do_log {
237 if self.max_retries == 0 && cur_attempt > 15 {
239 error!(error=?err, "gRPC call {} retried {} times", self.call_name, cur_attempt);
240 } else {
241 warn!(error=?err, "gRPC call {} retried {} times", self.call_name, cur_attempt);
242 }
243 }
244 }
245}
246
247#[derive(Clone, Debug)]
248pub(crate) struct CallInfo {
249 pub call_type: CallType,
250 call_name: &'static str,
251 retry_cfg: RetryOptions,
252 retry_short_circuit: Option<NoRetryOnMatching>,
253}
254
255#[doc(hidden)]
256#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
257pub enum CallType {
258 Normal,
259 UserLongPoll,
261 TaskLongPoll,
263}
264
265impl CallType {
266 pub(crate) fn is_long(&self) -> bool {
267 matches!(self, Self::UserLongPoll | Self::TaskLongPoll)
268 }
269}
270
271impl ErrorHandler<tonic::Status> for TonicErrorHandler {
272 type OutError = tonic::Status;
273
274 fn handle(
275 &mut self,
276 current_attempt: usize,
277 mut e: tonic::Status,
278 ) -> RetryPolicy<tonic::Status> {
279 if self.max_retries > 0 && current_attempt >= self.max_retries {
281 return RetryPolicy::ForwardError(e);
282 }
283
284 if let Some(sc) = self.retry_short_circuit.as_ref()
285 && (sc.predicate)(&e)
286 {
287 e.metadata_mut().insert(
288 ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT,
289 tonic::metadata::MetadataValue::from(0),
290 );
291 return RetryPolicy::ForwardError(e);
292 }
293
294 if e.code() == Code::ResourceExhausted
296 && (e
297 .message()
298 .starts_with("grpc: received message larger than max")
299 || e.message()
300 .starts_with("grpc: message after decompression larger than max")
301 || e.message()
302 .starts_with("grpc: received message after decompression larger than max"))
303 {
304 e.metadata_mut().insert(
306 MESSAGE_TOO_LARGE_KEY,
307 tonic::metadata::MetadataValue::from(0),
308 );
309 return RetryPolicy::ForwardError(e);
310 }
311
312 let long_poll_allowed = self.call_type == CallType::TaskLongPoll
315 && [Code::Cancelled, Code::DeadlineExceeded].contains(&e.code());
316
317 let transport_cancel_retry_allowed =
322 e.code() == Code::Cancelled && is_transport_cancelled(&e);
323
324 if RETRYABLE_ERROR_CODES.contains(&e.code())
325 || long_poll_allowed
326 || transport_cancel_retry_allowed
327 {
328 if current_attempt == 1 {
329 debug!(error=?e, "gRPC call {} failed on first attempt", self.call_name);
330 } else {
331 self.maybe_log_retry(current_attempt, &e);
332 }
333
334 match self.backoff.next_backoff() {
335 None => RetryPolicy::ForwardError(e), Some(backoff) => {
337 if e.code() == Code::ResourceExhausted {
340 let extended_backoff =
341 backoff.max(self.throttle_backoff.next_backoff().unwrap_or_default());
342 RetryPolicy::WaitRetry(extended_backoff)
343 } else {
344 RetryPolicy::WaitRetry(backoff)
345 }
346 }
347 }
348 } else if self.call_type == CallType::TaskLongPoll
349 && self.retry_started_at.elapsed() <= LONG_POLL_FATAL_GRACE
350 {
351 RetryPolicy::WaitRetry(self.max_interval)
354 } else {
355 RetryPolicy::ForwardError(e)
356 }
357 }
358}
359
360fn is_transport_cancelled(status: &tonic::Status) -> bool {
365 status
366 .source()
367 .and_then(|e| e.downcast_ref::<tonic::transport::Error>())
368 .and_then(|te| te.source())
369 .and_then(|tec| tec.downcast_ref::<hyper::Error>())
370 .is_some()
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use assert_matches::assert_matches;
377 use std::time::Instant;
378 use temporalio_common::protos::temporal::api::workflowservice::v1::{
379 PollActivityTaskQueueRequest, PollNexusTaskQueueRequest, PollWorkflowTaskQueueRequest,
380 };
381 use tonic::{IntoRequest, Status};
382
383 const TEST_RETRY_CONFIG: RetryOptions = RetryOptions {
385 initial_interval: Duration::from_millis(1),
386 randomization_factor: 0.0,
387 multiplier: 1.1,
388 max_interval: Duration::from_millis(2),
389 max_elapsed_time: None,
390 max_retries: 10,
391 };
392
393 const POLL_WORKFLOW_METH_NAME: &str = "poll_workflow_task_queue";
394 const POLL_ACTIVITY_METH_NAME: &str = "poll_activity_task_queue";
395 const POLL_NEXUS_METH_NAME: &str = "poll_nexus_task_queue";
396
397 #[tokio::test]
398 async fn long_poll_non_retryable_errors() {
399 for code in [
400 Code::InvalidArgument,
401 Code::NotFound,
402 Code::AlreadyExists,
403 Code::PermissionDenied,
404 Code::FailedPrecondition,
405 Code::Unauthenticated,
406 Code::Unimplemented,
407 ] {
408 for call_name in [POLL_WORKFLOW_METH_NAME, POLL_ACTIVITY_METH_NAME] {
409 let mut err_handler = TonicErrorHandler::new(
410 CallInfo {
411 call_type: CallType::TaskLongPoll,
412 call_name,
413 retry_cfg: TEST_RETRY_CONFIG,
414 retry_short_circuit: None,
415 },
416 TEST_RETRY_CONFIG,
417 );
418 let result = err_handler.handle(1, Status::new(code, "Ahh"));
419 assert_matches!(result, RetryPolicy::WaitRetry(_));
420 err_handler.retry_started_at =
421 Instant::now() - LONG_POLL_FATAL_GRACE - Duration::from_secs(1);
422 let result = err_handler.handle(2, Status::new(code, "Ahh"));
423 assert_matches!(result, RetryPolicy::ForwardError(_));
424 }
425 }
426 }
427
428 #[tokio::test]
429 async fn long_poll_retryable_errors_never_fatal() {
430 for code in RETRYABLE_ERROR_CODES {
431 for call_name in [POLL_WORKFLOW_METH_NAME, POLL_ACTIVITY_METH_NAME] {
432 let mut err_handler = TonicErrorHandler::new(
433 CallInfo {
434 call_type: CallType::TaskLongPoll,
435 call_name,
436 retry_cfg: TEST_RETRY_CONFIG,
437 retry_short_circuit: None,
438 },
439 TEST_RETRY_CONFIG,
440 );
441 let result = err_handler.handle(1, Status::new(code, "Ahh"));
442 assert_matches!(result, RetryPolicy::WaitRetry(_));
443 err_handler.retry_started_at =
444 Instant::now() - LONG_POLL_FATAL_GRACE - Duration::from_secs(1);
445 let result = err_handler.handle(2, Status::new(code, "Ahh"));
446 assert_matches!(result, RetryPolicy::WaitRetry(_));
447 }
448 }
449 }
450
451 #[tokio::test]
452 async fn retry_resource_exhausted() {
453 let mut err_handler = TonicErrorHandler::new(
454 CallInfo {
455 call_type: CallType::TaskLongPoll,
456 call_name: POLL_WORKFLOW_METH_NAME,
457 retry_cfg: TEST_RETRY_CONFIG,
458 retry_short_circuit: None,
459 },
460 RetryOptions {
461 initial_interval: Duration::from_millis(2),
462 randomization_factor: 0.0,
463 multiplier: 4.0,
464 max_interval: Duration::from_millis(10),
465 max_elapsed_time: None,
466 max_retries: 10,
467 },
468 );
469 let result = err_handler.handle(1, Status::new(Code::ResourceExhausted, "leave me alone"));
470 match result {
471 RetryPolicy::WaitRetry(duration) => assert_eq!(duration, Duration::from_millis(2)),
472 _ => panic!(),
473 }
474 let result = err_handler.handle(2, Status::new(Code::ResourceExhausted, "leave me alone"));
475 match result {
476 RetryPolicy::WaitRetry(duration) => assert_eq!(duration, Duration::from_millis(8)),
477 _ => panic!(),
478 }
479 }
480
481 #[tokio::test]
482 async fn retry_short_circuit() {
483 let mut err_handler = TonicErrorHandler::new(
484 CallInfo {
485 call_type: CallType::TaskLongPoll,
486 call_name: POLL_WORKFLOW_METH_NAME,
487 retry_cfg: TEST_RETRY_CONFIG,
488 retry_short_circuit: Some(NoRetryOnMatching {
489 predicate: |s: &Status| s.code() == Code::ResourceExhausted,
490 }),
491 },
492 TEST_RETRY_CONFIG,
493 );
494 let result = err_handler.handle(1, Status::new(Code::ResourceExhausted, "leave me alone"));
495 let e = assert_matches!(result, RetryPolicy::ForwardError(e) => e);
496 assert!(
497 e.metadata()
498 .get(ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT)
499 .is_some()
500 );
501 }
502
503 #[tokio::test]
504 async fn message_too_large_not_retried() {
505 let mut err_handler = TonicErrorHandler::new(
506 CallInfo {
507 call_type: CallType::TaskLongPoll,
508 call_name: POLL_WORKFLOW_METH_NAME,
509 retry_cfg: TEST_RETRY_CONFIG,
510 retry_short_circuit: None,
511 },
512 TEST_RETRY_CONFIG,
513 );
514 let result = err_handler.handle(
515 1,
516 Status::new(
517 Code::ResourceExhausted,
518 "grpc: received message larger than max",
519 ),
520 );
521 assert_matches!(result, RetryPolicy::ForwardError(_));
522
523 let result = err_handler.handle(
524 1,
525 Status::new(
526 Code::ResourceExhausted,
527 "grpc: message after decompression larger than max",
528 ),
529 );
530 assert_matches!(result, RetryPolicy::ForwardError(_));
531
532 let result = err_handler.handle(
533 1,
534 Status::new(
535 Code::ResourceExhausted,
536 "grpc: received message after decompression larger than max",
537 ),
538 );
539 assert_matches!(result, RetryPolicy::ForwardError(_));
540 }
541
542 #[rstest::rstest]
543 #[tokio::test]
544 async fn task_poll_retries_forever<R>(
545 #[values(
546 (
547 POLL_WORKFLOW_METH_NAME,
548 PollWorkflowTaskQueueRequest::default(),
549 ),
550 (
551 POLL_ACTIVITY_METH_NAME,
552 PollActivityTaskQueueRequest::default(),
553 ),
554 (
555 POLL_NEXUS_METH_NAME,
556 PollNexusTaskQueueRequest::default(),
557 ),
558 )]
559 (call_name, req): (&'static str, R),
560 ) {
561 let mut req = req.into_request();
564 req.extensions_mut().insert(IsWorkerTaskLongPoll);
565 for i in 1..=50 {
566 let mut err_handler = TonicErrorHandler::new(
567 TEST_RETRY_CONFIG.get_call_info::<R>(call_name, Some(&req)),
568 RetryOptions::throttle_retry_policy(),
569 );
570 let result = err_handler.handle(i, Status::new(Code::Unknown, "Ahh"));
571 assert_matches!(result, RetryPolicy::WaitRetry(_));
572 }
573 }
574
575 #[rstest::rstest]
576 #[tokio::test]
577 async fn task_poll_retries_deadline_exceeded<R>(
578 #[values(
579 (
580 POLL_WORKFLOW_METH_NAME,
581 PollWorkflowTaskQueueRequest::default(),
582 ),
583 (
584 POLL_ACTIVITY_METH_NAME,
585 PollActivityTaskQueueRequest::default(),
586 ),
587 (
588 POLL_NEXUS_METH_NAME,
589 PollNexusTaskQueueRequest::default(),
590 ),
591 )]
592 (call_name, req): (&'static str, R),
593 ) {
594 let mut req = req.into_request();
595 req.extensions_mut().insert(IsWorkerTaskLongPoll);
596 for code in [Code::Cancelled, Code::DeadlineExceeded] {
598 let mut err_handler = TonicErrorHandler::new(
599 TEST_RETRY_CONFIG.get_call_info::<R>(call_name, Some(&req)),
600 RetryOptions::throttle_retry_policy(),
601 );
602 for i in 1..=5 {
603 let result = err_handler.handle(i, Status::new(code, "retryable failure"));
604 assert_matches!(result, RetryPolicy::WaitRetry(_));
605 }
606 }
607 }
608
609 #[tokio::test]
610 async fn plain_cancelled_not_retried_on_normal_call() {
611 let mut err_handler = TonicErrorHandler::new(
614 CallInfo {
615 call_type: CallType::Normal,
616 call_name: "respond_activity_task_completed",
617 retry_cfg: TEST_RETRY_CONFIG,
618 retry_short_circuit: None,
619 },
620 TEST_RETRY_CONFIG,
621 );
622 let result = err_handler.handle(1, Status::new(Code::Cancelled, "caller cancelled"));
623 assert_matches!(result, RetryPolicy::ForwardError(_));
624 }
625
626 #[tokio::test]
627 async fn is_transport_cancelled_false_for_plain_status() {
628 let status = Status::new(Code::Cancelled, "caller cancelled");
631 assert!(!is_transport_cancelled(&status));
632 }
633
634 #[tokio::test]
635 async fn transport_sourced_cancelled_retried_on_full_budget() {
636 let mut err_handler = TonicErrorHandler::new(
647 CallInfo {
648 call_type: CallType::Normal,
649 call_name: "respond_activity_task_completed",
650 retry_cfg: TEST_RETRY_CONFIG,
651 retry_short_circuit: None,
652 },
653 TEST_RETRY_CONFIG,
654 );
655
656 for i in 1..=5 {
659 let endpoint = tonic::transport::Endpoint::from_static("http://[::1]:1")
660 .connect_timeout(Duration::from_millis(1));
661 let transport_err = endpoint.connect().await.unwrap_err();
662 let status = Status::from_error(Box::new(transport_err));
663
664 let result = err_handler.handle(i, status);
665 assert_matches!(
666 result,
667 RetryPolicy::WaitRetry(_),
668 "Transport error should be retried on attempt {i}"
669 );
670 }
671 }
672}