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