aws_smithy_types/
timeout.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! This module defines types that describe timeouts that can be applied to various stages of the
7//! Smithy networking stack.
8
9use crate::config_bag::value::Value;
10use crate::config_bag::{ItemIter, Storable, Store, StoreReplace};
11use std::time::Duration;
12
13#[derive(Clone, Debug, Default, PartialEq, Copy)]
14enum CanDisable<T> {
15    Disabled,
16    #[default]
17    Unset,
18    Set(T),
19}
20
21impl<T> CanDisable<T> {
22    fn none_implies_disabled(value: Option<T>) -> Self {
23        match value {
24            Some(t) => CanDisable::Set(t),
25            None => CanDisable::Disabled,
26        }
27    }
28
29    fn is_some(&self) -> bool {
30        matches!(self, CanDisable::Set(_))
31    }
32
33    fn value(self) -> Option<T> {
34        match self {
35            CanDisable::Set(v) => Some(v),
36            _ => None,
37        }
38    }
39
40    fn merge_from_lower_priority(self, other: Self) -> Self {
41        match (self, other) {
42            // if we are unset. take the value from the other
43            (CanDisable::Unset, value) => value,
44            (us, _) => us,
45        }
46    }
47}
48
49impl<T> From<T> for CanDisable<T> {
50    fn from(value: T) -> Self {
51        Self::Set(value)
52    }
53}
54
55/// Builder for [`TimeoutConfig`].
56#[non_exhaustive]
57#[derive(Clone, Debug, Default)]
58pub struct TimeoutConfigBuilder {
59    connect_timeout: CanDisable<Duration>,
60    read_timeout: CanDisable<Duration>,
61    operation_timeout: CanDisable<Duration>,
62    operation_attempt_timeout: CanDisable<Duration>,
63}
64
65impl TimeoutConfigBuilder {
66    /// Creates a new builder with no timeouts set.
67    pub fn new() -> Self {
68        Default::default()
69    }
70
71    /// Sets the connect timeout.
72    ///
73    /// The connect timeout is a limit on the amount of time it takes to initiate a socket connection.
74    pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self {
75        self.connect_timeout = connect_timeout.into();
76        self
77    }
78
79    /// Sets the connect timeout.
80    ///
81    /// If `None` is passed, this will explicitly disable the connection timeout.
82    ///
83    /// The connect timeout is a limit on the amount of time it takes to initiate a socket connection.
84    pub fn set_connect_timeout(&mut self, connect_timeout: Option<Duration>) -> &mut Self {
85        self.connect_timeout = CanDisable::none_implies_disabled(connect_timeout);
86        self
87    }
88
89    /// Disables the connect timeout
90    pub fn disable_connect_timeout(mut self) -> Self {
91        self.connect_timeout = CanDisable::Disabled;
92        self
93    }
94
95    /// Sets the read timeout.
96    ///
97    /// The read timeout is the limit on the amount of time it takes to read the first byte of a response
98    /// from the time the request is initiated.
99    pub fn read_timeout(mut self, read_timeout: Duration) -> Self {
100        self.read_timeout = read_timeout.into();
101        self
102    }
103
104    /// Sets the read timeout.
105    ///
106    /// If `None` is passed, this will explicitly disable the read timeout. To disable all timeouts use [`TimeoutConfig::disabled`].
107    ///
108    /// The read timeout is the limit on the amount of time it takes to read the first byte of a response
109    /// from the time the request is initiated.
110    pub fn set_read_timeout(&mut self, read_timeout: Option<Duration>) -> &mut Self {
111        self.read_timeout = CanDisable::none_implies_disabled(read_timeout);
112        self
113    }
114
115    /// Disables the read timeout
116    pub fn disable_read_timeout(mut self) -> Self {
117        self.read_timeout = CanDisable::Disabled;
118        self
119    }
120
121    /// Sets the operation timeout.
122    ///
123    /// An operation represents the full request/response lifecycle of a call to a service.
124    /// The operation timeout is a limit on the total amount of time it takes for an operation to be
125    /// fully serviced, including the time for all retries that may have been attempted for it.
126    ///
127    /// If you want to set a timeout on individual retry attempts, then see [`Self::operation_attempt_timeout`]
128    /// or [`Self::set_operation_attempt_timeout`].
129    pub fn operation_timeout(mut self, operation_timeout: Duration) -> Self {
130        self.operation_timeout = operation_timeout.into();
131        self
132    }
133
134    /// Sets the operation timeout.
135    ///
136    /// If `None` is passed, this will explicitly disable the read timeout. To disable all timeouts use [`TimeoutConfig::disabled`].
137    ///
138    /// An operation represents the full request/response lifecycle of a call to a service.
139    /// The operation timeout is a limit on the total amount of time it takes for an operation to be
140    /// fully serviced, including the time for all retries that may have been attempted for it.
141    ///
142    /// If you want to set a timeout on individual retry attempts, then see [`Self::operation_attempt_timeout`]
143    /// or [`Self::set_operation_attempt_timeout`].
144    pub fn set_operation_timeout(&mut self, operation_timeout: Option<Duration>) -> &mut Self {
145        self.operation_timeout = CanDisable::none_implies_disabled(operation_timeout);
146        self
147    }
148
149    /// Disables the operation timeout
150    pub fn disable_operation_timeout(mut self) -> Self {
151        self.operation_timeout = CanDisable::Disabled;
152        self
153    }
154
155    /// Sets the operation attempt timeout.
156    ///
157    /// An operation represents the full request/response lifecycle of a call to a service.
158    /// When retries are enabled, then this setting makes it possible to set a timeout for individual
159    /// retry attempts (including the initial attempt) for an operation.
160    ///
161    /// If you want to set a timeout on the total time for an entire request including all of its retries,
162    /// then see [`Self::operation_timeout`] /// or [`Self::set_operation_timeout`].
163    pub fn operation_attempt_timeout(mut self, operation_attempt_timeout: Duration) -> Self {
164        self.operation_attempt_timeout = operation_attempt_timeout.into();
165        self
166    }
167
168    /// Sets the operation attempt timeout.
169    ///
170    /// If `None` is passed, this will explicitly disable the operation timeout. To disable all timeouts use [`TimeoutConfig::disabled`].
171    ///
172    /// An operation represents the full request/response lifecycle of a call to a service.
173    /// When retries are enabled, then this setting makes it possible to set a timeout for individual
174    /// retry attempts (including the initial attempt) for an operation.
175    ///
176    /// If you want to set a timeout on individual retry attempts, then see [`Self::operation_attempt_timeout`]
177    /// or [`Self::set_operation_attempt_timeout`].
178    pub fn set_operation_attempt_timeout(
179        &mut self,
180        operation_attempt_timeout: Option<Duration>,
181    ) -> &mut Self {
182        self.operation_attempt_timeout =
183            CanDisable::none_implies_disabled(operation_attempt_timeout);
184        self
185    }
186
187    /// Disables the operation_attempt timeout
188    pub fn disable_operation_attempt_timeout(mut self) -> Self {
189        self.operation_attempt_timeout = CanDisable::Disabled;
190        self
191    }
192
193    /// Merges two timeout config builders together.
194    ///
195    /// Values from `other` will only be used as a fallback for values
196    /// from `self`. Useful for merging configs from different sources together when you want to
197    /// handle "precedence" per value instead of at the config level
198    ///
199    /// # Example
200    ///
201    /// ```rust
202    /// # use std::time::Duration;
203    /// # use aws_smithy_types::timeout::TimeoutConfig;
204    /// let a = TimeoutConfig::builder()
205    ///     .connect_timeout(Duration::from_secs(3));
206    /// let b = TimeoutConfig::builder()
207    ///     .connect_timeout(Duration::from_secs(5))
208    ///     .operation_timeout(Duration::from_secs(3));
209    /// let timeout_config = a.take_unset_from(b).build();
210    ///
211    /// // A's value take precedence over B's value
212    /// assert_eq!(timeout_config.connect_timeout(), Some(Duration::from_secs(3)));
213    /// // A never set an operation timeout so B's value is used
214    /// assert_eq!(timeout_config.operation_timeout(), Some(Duration::from_secs(3)));
215    /// ```
216    pub fn take_unset_from(self, other: Self) -> Self {
217        Self {
218            connect_timeout: self
219                .connect_timeout
220                .merge_from_lower_priority(other.connect_timeout),
221            read_timeout: self
222                .read_timeout
223                .merge_from_lower_priority(other.read_timeout),
224            operation_timeout: self
225                .operation_timeout
226                .merge_from_lower_priority(other.operation_timeout),
227            operation_attempt_timeout: self
228                .operation_attempt_timeout
229                .merge_from_lower_priority(other.operation_attempt_timeout),
230        }
231    }
232
233    /// Builds a `TimeoutConfig`.
234    pub fn build(self) -> TimeoutConfig {
235        TimeoutConfig {
236            connect_timeout: self.connect_timeout,
237            read_timeout: self.read_timeout,
238            operation_timeout: self.operation_timeout,
239            operation_attempt_timeout: self.operation_attempt_timeout,
240        }
241    }
242}
243
244impl From<TimeoutConfig> for TimeoutConfigBuilder {
245    fn from(timeout_config: TimeoutConfig) -> Self {
246        TimeoutConfigBuilder {
247            connect_timeout: timeout_config.connect_timeout,
248            read_timeout: timeout_config.read_timeout,
249            operation_timeout: timeout_config.operation_timeout,
250            operation_attempt_timeout: timeout_config.operation_attempt_timeout,
251        }
252    }
253}
254
255/// Top-level configuration for timeouts
256///
257/// # Example
258///
259/// ```rust
260/// # use std::time::Duration;
261///
262/// # fn main() {
263/// use aws_smithy_types::timeout::TimeoutConfig;
264///
265/// let timeout_config = TimeoutConfig::builder()
266///     .operation_timeout(Duration::from_secs(30))
267///     .operation_attempt_timeout(Duration::from_secs(10))
268///     .connect_timeout(Duration::from_secs(3))
269///     .build();
270///
271/// assert_eq!(
272///     timeout_config.operation_timeout(),
273///     Some(Duration::from_secs(30))
274/// );
275/// assert_eq!(
276///     timeout_config.operation_attempt_timeout(),
277///     Some(Duration::from_secs(10))
278/// );
279/// assert_eq!(
280///     timeout_config.connect_timeout(),
281///     Some(Duration::from_secs(3))
282/// );
283/// # }
284/// ```
285#[non_exhaustive]
286#[derive(Clone, PartialEq, Debug)]
287pub struct TimeoutConfig {
288    connect_timeout: CanDisable<Duration>,
289    read_timeout: CanDisable<Duration>,
290    operation_timeout: CanDisable<Duration>,
291    operation_attempt_timeout: CanDisable<Duration>,
292}
293
294impl Storable for TimeoutConfig {
295    type Storer = StoreReplace<TimeoutConfig>;
296}
297
298/// Merger which merges timeout config settings when loading.
299///
300/// If no timeouts are set, `TimeoutConfig::disabled()` will be returned.
301///
302/// This API is not meant to be used externally.
303#[derive(Debug)]
304pub struct MergeTimeoutConfig;
305
306impl Storable for MergeTimeoutConfig {
307    type Storer = MergeTimeoutConfig;
308}
309impl Store for MergeTimeoutConfig {
310    type ReturnedType<'a> = TimeoutConfig;
311    type StoredType = <StoreReplace<TimeoutConfig> as Store>::StoredType;
312
313    fn merge_iter(iter: ItemIter<'_, Self>) -> Self::ReturnedType<'_> {
314        let mut result: Option<TimeoutConfig> = None;
315        // The item iterator iterates "backwards" over the config bags, starting at the highest
316        // priority layers and works backwards
317        for tc in iter {
318            match (result.as_mut(), tc) {
319                (Some(result), Value::Set(tc)) => {
320                    // This maintains backwards compatible behavior where setting an EMPTY timeout config is equivalent to `TimeoutConfig::disabled()`
321                    if result.has_timeouts() {
322                        result.take_defaults_from(tc);
323                    }
324                }
325                (None, Value::Set(tc)) => {
326                    result = Some(tc.clone());
327                }
328                (_, Value::ExplicitlyUnset(_)) => result = Some(TimeoutConfig::disabled()),
329            }
330        }
331        result.unwrap_or(TimeoutConfig::disabled())
332    }
333}
334
335impl TimeoutConfig {
336    /// Returns a builder to create a `TimeoutConfig`.
337    pub fn builder() -> TimeoutConfigBuilder {
338        TimeoutConfigBuilder::new()
339    }
340
341    /// Returns a builder equivalent of this `TimeoutConfig`.
342    pub fn to_builder(&self) -> TimeoutConfigBuilder {
343        TimeoutConfigBuilder::from(self.clone())
344    }
345
346    /// Converts this `TimeoutConfig` into a builder.
347    pub fn into_builder(self) -> TimeoutConfigBuilder {
348        TimeoutConfigBuilder::from(self)
349    }
350
351    /// Fill any unfilled values in `self` from `other`.
352    pub fn take_defaults_from(&mut self, other: &TimeoutConfig) -> &mut Self {
353        self.connect_timeout = self
354            .connect_timeout
355            .merge_from_lower_priority(other.connect_timeout);
356        self.read_timeout = self
357            .read_timeout
358            .merge_from_lower_priority(other.read_timeout);
359        self.operation_timeout = self
360            .operation_timeout
361            .merge_from_lower_priority(other.operation_timeout);
362        self.operation_attempt_timeout = self
363            .operation_attempt_timeout
364            .merge_from_lower_priority(other.operation_attempt_timeout);
365        self
366    }
367
368    /// Returns a timeout config with all timeouts disabled.
369    pub fn disabled() -> TimeoutConfig {
370        TimeoutConfig {
371            connect_timeout: CanDisable::Disabled,
372            read_timeout: CanDisable::Disabled,
373            operation_timeout: CanDisable::Disabled,
374            operation_attempt_timeout: CanDisable::Disabled,
375        }
376    }
377
378    /// Returns this config's connect timeout.
379    ///
380    /// The connect timeout is a limit on the amount of time it takes to initiate a socket connection.
381    pub fn connect_timeout(&self) -> Option<Duration> {
382        self.connect_timeout.value()
383    }
384
385    /// Returns this config's read timeout.
386    ///
387    /// The read timeout is the limit on the amount of time it takes to read the first byte of a response
388    /// from the time the request is initiated.
389    pub fn read_timeout(&self) -> Option<Duration> {
390        self.read_timeout.value()
391    }
392
393    /// Returns this config's operation timeout.
394    ///
395    /// An operation represents the full request/response lifecycle of a call to a service.
396    /// The operation timeout is a limit on the total amount of time it takes for an operation to be
397    /// fully serviced, including the time for all retries that may have been attempted for it.
398    pub fn operation_timeout(&self) -> Option<Duration> {
399        self.operation_timeout.value()
400    }
401
402    /// Returns this config's operation attempt timeout.
403    ///
404    /// An operation represents the full request/response lifecycle of a call to a service.
405    /// When retries are enabled, then this setting makes it possible to set a timeout for individual
406    /// retry attempts (including the initial attempt) for an operation.
407    pub fn operation_attempt_timeout(&self) -> Option<Duration> {
408        self.operation_attempt_timeout.value()
409    }
410
411    /// Returns true if any of the possible timeouts are set.
412    pub fn has_timeouts(&self) -> bool {
413        self.connect_timeout.is_some()
414            || self.read_timeout.is_some()
415            || self.operation_timeout.is_some()
416            || self.operation_attempt_timeout.is_some()
417    }
418}
419
420/// Configuration subset of [`TimeoutConfig`] for operation timeouts
421#[non_exhaustive]
422#[derive(Clone, PartialEq, Debug)]
423pub struct OperationTimeoutConfig {
424    operation_timeout: Option<Duration>,
425    operation_attempt_timeout: Option<Duration>,
426}
427
428impl OperationTimeoutConfig {
429    /// Returns this config's operation timeout.
430    ///
431    /// An operation represents the full request/response lifecycle of a call to a service.
432    /// The operation timeout is a limit on the total amount of time it takes for an operation to be
433    /// fully serviced, including the time for all retries that may have been attempted for it.
434    pub fn operation_timeout(&self) -> Option<Duration> {
435        self.operation_timeout
436    }
437
438    /// Returns this config's operation attempt timeout.
439    ///
440    /// An operation represents the full request/response lifecycle of a call to a service.
441    /// When retries are enabled, then this setting makes it possible to set a timeout for individual
442    /// retry attempts (including the initial attempt) for an operation.
443    pub fn operation_attempt_timeout(&self) -> Option<Duration> {
444        self.operation_attempt_timeout
445    }
446
447    /// Returns true if any of the possible timeouts are set.
448    pub fn has_timeouts(&self) -> bool {
449        self.operation_timeout.is_some() || self.operation_attempt_timeout.is_some()
450    }
451}
452
453impl From<&TimeoutConfig> for OperationTimeoutConfig {
454    fn from(cfg: &TimeoutConfig) -> Self {
455        OperationTimeoutConfig {
456            operation_timeout: cfg.operation_timeout.value(),
457            operation_attempt_timeout: cfg.operation_attempt_timeout.value(),
458        }
459    }
460}
461
462impl From<TimeoutConfig> for OperationTimeoutConfig {
463    fn from(cfg: TimeoutConfig) -> Self {
464        OperationTimeoutConfig::from(&cfg)
465    }
466}
467
468#[cfg(test)]
469mod test {
470    use crate::config_bag::{CloneableLayer, ConfigBag};
471    use crate::timeout::{MergeTimeoutConfig, TimeoutConfig};
472    use std::time::Duration;
473
474    #[test]
475    fn timeout_configs_merged_in_config_bag() {
476        let mut read_timeout = CloneableLayer::new("timeout");
477        read_timeout.store_put(
478            TimeoutConfig::builder()
479                .read_timeout(Duration::from_secs(3))
480                .connect_timeout(Duration::from_secs(1))
481                .build(),
482        );
483        let mut operation_timeout = CloneableLayer::new("timeout");
484        operation_timeout.store_put(
485            TimeoutConfig::builder()
486                .operation_timeout(Duration::from_secs(5))
487                .connect_timeout(Duration::from_secs(10))
488                .build(),
489        );
490        let cfg = ConfigBag::of_layers(vec![read_timeout.into(), operation_timeout.into()]);
491        let loaded = cfg.load::<MergeTimeoutConfig>();
492        // set by base layer
493        assert_eq!(loaded.read_timeout(), Some(Duration::from_secs(3)));
494
495        // set by higher layer
496        assert_eq!(loaded.operation_timeout(), Some(Duration::from_secs(5)));
497
498        // overridden by higher layer
499        assert_eq!(loaded.connect_timeout(), Some(Duration::from_secs(10)));
500        let mut next = cfg.add_layer("disabled");
501        next.interceptor_state()
502            .store_put(TimeoutConfig::disabled());
503
504        assert_eq!(next.load::<MergeTimeoutConfig>().read_timeout(), None);
505
506        // builder().build() acts equivalently to disabled
507        next.interceptor_state()
508            .store_put(TimeoutConfig::builder().build());
509        assert_eq!(next.load::<MergeTimeoutConfig>().read_timeout(), None);
510
511        // But if instead, you set a field of the timeout config, it will merge as expected.
512        next.interceptor_state().store_put(
513            TimeoutConfig::builder()
514                .operation_attempt_timeout(Duration::from_secs(1))
515                .build(),
516        );
517        assert_eq!(
518            next.load::<MergeTimeoutConfig>().read_timeout(),
519            Some(Duration::from_secs(3))
520        );
521    }
522}