Skip to main content

google_cloud_spanner/
retry_policy.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
15//! RPC retry policies used by the Spanner client.
16
17use google_cloud_gax::error::Error;
18use google_cloud_gax::retry_policy::{Aip194Strict, RetryPolicy};
19use google_cloud_gax::retry_result::RetryResult;
20use google_cloud_gax::retry_state::RetryState;
21use google_cloud_gax::throttle_result::ThrottleResult;
22use std::time::Duration;
23
24/// The retry policy the Spanner client applies to RPCs that do not configure
25/// their own. It decorates/extends [google_cloud_gax::retry_policy::Aip194Strict].
26///
27/// Like `Aip194Strict`, this policy retries `UNAVAILABLE` errors and transient
28/// failures that occur before the request reaches the service, but only for
29/// idempotent requests. In addition — because Spanner allows transport and
30/// connection errors to be retried on idempotent operations — it also retries
31/// transport/network and I/O errors that `Aip194Strict` would classify as
32/// permanent (such as a connection dropped after the request was sent), again
33/// only if the request is idempotent.
34///
35/// The policy places no limit on the number of attempts or the elapsed time.
36/// Applications that want to bound the client's default retry behavior can
37/// decorate this policy with
38/// [RetryPolicyExt][google_cloud_gax::retry_policy::RetryPolicyExt] instead of
39/// re-implementing its error classification:
40///
41/// # Example
42/// ```
43/// # use std::time::Duration;
44/// # use google_cloud_spanner::retry_policy::SpannerRetryPolicy;
45/// # use google_cloud_spanner::statement::Statement;
46/// # use google_cloud_gax::retry_policy::RetryPolicyExt;
47/// let statement = Statement::builder("SELECT * FROM Users")
48///     .with_retry_policy(
49///         SpannerRetryPolicy::new()
50///             .with_attempt_limit(5)
51///             .with_time_limit(Duration::from_secs(30)),
52///     )
53///     .build();
54/// ```
55#[derive(Clone, Debug)]
56pub struct SpannerRetryPolicy {
57    inner: Aip194Strict,
58}
59
60impl SpannerRetryPolicy {
61    /// Creates a new Spanner retry policy.
62    pub fn new() -> Self {
63        Self {
64            inner: Aip194Strict,
65        }
66    }
67}
68
69impl Default for SpannerRetryPolicy {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl RetryPolicy for SpannerRetryPolicy {
76    fn on_error(&self, state: &RetryState, error: Error) -> RetryResult {
77        // 1. Strict AIP-194 checks (Unavailable, is_transient_and_before_rpc)
78        let result = self.inner.on_error(state, error);
79        match result {
80            // If the strict AIP-194 checks classified the error as permanent (such as a transport
81            // error that occurred post-headers), we override it to Continue if the request is idempotent.
82            RetryResult::Permanent(error)
83                if state.idempotent && (error.is_transport() || error.is_io()) =>
84            {
85                RetryResult::Continue(error)
86            }
87            res => res,
88        }
89    }
90
91    fn on_throttle(&self, state: &RetryState, error: Error) -> ThrottleResult {
92        self.inner.on_throttle(state, error)
93    }
94
95    fn remaining_time(&self, state: &RetryState) -> Option<Duration> {
96        self.inner.remaining_time(state)
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use google_cloud_gax::error::Error as GaxError;
104    use google_cloud_gax::error::rpc::{Code, Status};
105    use http::HeaderMap;
106
107    #[test]
108    fn test_spanner_retry_policy_idempotent() {
109        let policy = SpannerRetryPolicy::new();
110        let state = RetryState::new(true); // idempotent = true
111
112        // 1. Service UNAVAILABLE error should be retried (via inner AIP-194)
113        let status = Status::default()
114            .set_code(Code::Unavailable)
115            .set_message("Service Unavailable");
116        let err = GaxError::service(status);
117        assert!(
118            policy.on_error(&state, err).is_continue(),
119            "Expected UNAVAILABLE to be retried when idempotent"
120        );
121
122        // 2. Service PERMISSION_DENIED error should not be retried
123        let status = Status::default()
124            .set_code(Code::PermissionDenied)
125            .set_message("Denied");
126        let err = GaxError::service(status);
127        assert!(
128            policy.on_error(&state, err).is_permanent(),
129            "Expected PERMISSION_DENIED to not be retried"
130        );
131
132        // 3. IO/Transport error should be retried when idempotent
133        let err = GaxError::transport(
134            HeaderMap::new(),
135            std::io::Error::new(std::io::ErrorKind::ConnectionReset, "connection closed"),
136        );
137        assert!(
138            policy.on_error(&state, err).is_continue(),
139            "Expected transport connection reset to be retried when idempotent"
140        );
141    }
142
143    #[test]
144    fn test_spanner_retry_policy_non_idempotent() {
145        let policy = SpannerRetryPolicy::new();
146        let state = RetryState::new(false); // idempotent = false
147
148        // 1. Service UNAVAILABLE error should NOT be retried (AIP-194 requires idempotency)
149        let status = Status::default()
150            .set_code(Code::Unavailable)
151            .set_message("Service Unavailable");
152        let err = GaxError::service(status);
153        assert!(
154            policy.on_error(&state, err).is_permanent(),
155            "Expected UNAVAILABLE to be permanent when non-idempotent"
156        );
157
158        // 2. IO/Transport error should NOT be retried when non-idempotent
159        let err = GaxError::transport(
160            HeaderMap::new(),
161            std::io::Error::new(std::io::ErrorKind::ConnectionReset, "connection closed"),
162        );
163        assert!(
164            policy.on_error(&state, err).is_permanent(),
165            "Expected transport connection reset to not be retried when non-idempotent"
166        );
167    }
168}