Skip to main content

google_cloud_gax/retry_policy/
client_timeout.rs

1// Copyright 2026 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::{Error, RetryPolicy, RetryResult, RetryState, ThrottleResult};
16use std::time::Duration;
17
18/// A retry policy decorator that continues on client timeouts.
19///
20/// This policy returns [RetryResult::Continue] when the error is a client-side timeout and the
21/// request is idempotent. Otherwise it returns the result from the inner retry policy.
22///
23/// # Parameters
24/// * `P` - the inner retry policy.
25#[derive(Debug)]
26pub struct ClientTimeout<P>
27where
28    P: RetryPolicy,
29{
30    inner: P,
31}
32
33impl<P> ClientTimeout<P>
34where
35    P: RetryPolicy,
36{
37    /// Decorate an existing retry policy to ignore client-side timeouts.
38    ///
39    /// # Example
40    /// ```
41    /// # use google_cloud_gax::retry_policy::{ClientTimeout, RetryPolicy};
42    /// use google_cloud_gax::retry_policy::Aip194Strict;
43    /// use google_cloud_gax::retry_state::RetryState;
44    /// let policy = ClientTimeout::new(Aip194Strict);
45    /// assert!(policy.on_error(&RetryState::new(true).set_attempt_count(1_u32), timeout()).is_continue());
46    /// assert!(policy.on_error(&RetryState::new(true).set_attempt_count(2_u32), permanent()).is_permanent());
47    ///
48    /// use google_cloud_gax::error::Error;
49    /// # use google_cloud_gax::error::rpc::{Code, Status};
50    /// fn timeout() -> Error {
51    /// # Error::timeout("test-only")
52    /// }
53    /// fn permanent() -> Error {
54    /// # Error::service(Status::default().set_code(Code::PermissionDenied))
55    /// }
56    /// ```
57    pub fn new(inner: P) -> Self {
58        Self { inner }
59    }
60}
61
62impl<P> RetryPolicy for ClientTimeout<P>
63where
64    P: RetryPolicy,
65{
66    fn on_error(&self, state: &RetryState, error: Error) -> RetryResult {
67        if state.idempotent && error.is_timeout() {
68            return RetryResult::Continue(error);
69        }
70        self.inner.on_error(state, error)
71    }
72
73    fn on_throttle(&self, state: &RetryState, error: Error) -> ThrottleResult {
74        self.inner.on_throttle(state, error)
75    }
76
77    fn remaining_time(&self, state: &RetryState) -> Option<Duration> {
78        self.inner.remaining_time(state)
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::super::tests::{MockPolicy, idempotent_state, non_idempotent_state};
85    use super::*;
86    use crate::error::rpc::{Code, Status};
87    use crate::retry_policy::NeverRetry;
88    use std::time::Instant;
89
90    fn timeout() -> Error {
91        Error::timeout("test-only")
92    }
93
94    fn permanent() -> Error {
95        Error::service(Status::default().set_code(Code::PermissionDenied))
96    }
97
98    fn transient() -> Error {
99        Error::service(Status::default().set_code(Code::Unavailable))
100    }
101
102    #[test]
103    fn on_error() {
104        let policy = ClientTimeout::new(NeverRetry);
105        let result = policy.on_error(&idempotent_state(Instant::now()), timeout());
106        assert!(matches!(result, RetryResult::Continue(_)), "{result:?}");
107        let result = policy.on_error(&non_idempotent_state(Instant::now()), timeout());
108        assert!(matches!(result, RetryResult::Exhausted(_)), "{result:?}");
109        let result = policy.on_error(&idempotent_state(Instant::now()), permanent());
110        assert!(matches!(result, RetryResult::Exhausted(_)), "{result:?}");
111    }
112
113    #[test]
114    fn ext() {
115        use super::super::RetryPolicyExt;
116        let policy = NeverRetry.continue_on_client_timeout();
117        let result = policy.on_error(&idempotent_state(Instant::now()), timeout());
118        assert!(matches!(result, RetryResult::Continue(_)), "{result:?}");
119        let result = policy.on_error(&non_idempotent_state(Instant::now()), timeout());
120        assert!(matches!(result, RetryResult::Exhausted(_)), "{result:?}");
121        let result = policy.on_error(&idempotent_state(Instant::now()), permanent());
122        assert!(matches!(result, RetryResult::Exhausted(_)), "{result:?}");
123    }
124
125    #[test]
126    fn forwards() {
127        let mut mock = MockPolicy::new();
128        mock.expect_on_error()
129            .times(1..)
130            .returning(|_, e| RetryResult::Permanent(e));
131        mock.expect_on_throttle()
132            .times(1..)
133            .returning(|_, e| ThrottleResult::Exhausted(e));
134        mock.expect_remaining_time().times(1).returning(|_| None);
135
136        let policy = ClientTimeout::new(mock);
137        let result = policy.on_error(&idempotent_state(Instant::now()), transient());
138        assert!(matches!(result, RetryResult::Permanent(_)), "{result:?}");
139        let result = policy.on_error(&idempotent_state(Instant::now()), timeout());
140        assert!(matches!(result, RetryResult::Continue(_)), "{result:?}");
141
142        let result = policy.on_throttle(&idempotent_state(Instant::now()), transient());
143        assert!(matches!(result, ThrottleResult::Exhausted(_)));
144        let result = policy.on_throttle(&idempotent_state(Instant::now()), timeout());
145        assert!(matches!(result, ThrottleResult::Exhausted(_)));
146
147        let result = policy.remaining_time(&idempotent_state(Instant::now()));
148        assert!(result.is_none(), "{result:?}");
149    }
150}