Skip to main content

aws_smithy_runtime/client/http/body/
minimum_throughput.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! A body-wrapping type that ensures data is being streamed faster than some lower limit.
7//!
8//! If data is being streamed too slowly, this body type will emit an error next time it's polled.
9
10/// An implementation of v0.4 `http_body::Body` for `MinimumThroughputBody` and related code.
11///
12/// Requires the `http-02x` feature.
13#[cfg(feature = "http-02x")]
14pub mod http_body_0_4_x;
15
16/// An implementation of v1.0 `http_body::Body` for `MinimumThroughputBody` and related code.
17pub mod http_body_1_x;
18
19/// Options for a [`MinimumThroughputBody`].
20pub mod options;
21pub use throughput::Throughput;
22mod throughput;
23
24use crate::client::http::body::minimum_throughput::throughput::ThroughputReport;
25use aws_smithy_async::rt::sleep::Sleep;
26use aws_smithy_async::rt::sleep::{AsyncSleep, SharedAsyncSleep};
27use aws_smithy_async::time::{SharedTimeSource, TimeSource};
28use aws_smithy_runtime_api::{
29    box_error::BoxError,
30    client::{
31        http::HttpConnectorFuture, result::ConnectorError, runtime_components::RuntimeComponents,
32        stalled_stream_protection::StalledStreamProtectionConfig,
33    },
34};
35use aws_smithy_runtime_api::{client::orchestrator::HttpResponse, shared::IntoShared};
36use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace};
37use options::MinimumThroughputBodyOptions;
38use std::{
39    fmt,
40    sync::{Arc, Mutex},
41    task::Poll,
42};
43use std::{future::Future, pin::Pin};
44use std::{
45    task::Context,
46    time::{Duration, SystemTime},
47};
48use throughput::ThroughputLogs;
49
50/// Use [`MinimumThroughputDownloadBody`] instead.
51#[deprecated(note = "Renamed to MinimumThroughputDownloadBody since it doesn't work for uploads")]
52pub type MinimumThroughputBody<B> = MinimumThroughputDownloadBody<B>;
53
54pin_project_lite::pin_project! {
55    /// A body-wrapping type that ensures data is being streamed faster than some lower limit.
56    ///
57    /// If data is being streamed too slowly, this body type will emit an error next time it's polled.
58    pub struct MinimumThroughputDownloadBody<B> {
59        async_sleep: SharedAsyncSleep,
60        time_source: SharedTimeSource,
61        options: MinimumThroughputBodyOptions,
62        throughput_logs: ThroughputLogs,
63        resolution: Duration,
64        #[pin]
65        sleep_fut: Option<Sleep>,
66        #[pin]
67        grace_period_fut: Option<Sleep>,
68        #[pin]
69        inner: B,
70    }
71}
72
73impl<B> MinimumThroughputDownloadBody<B> {
74    /// Create a new minimum throughput body.
75    pub fn new(
76        time_source: impl TimeSource + 'static,
77        async_sleep: impl AsyncSleep + 'static,
78        body: B,
79        options: MinimumThroughputBodyOptions,
80    ) -> Self {
81        let time_source: SharedTimeSource = time_source.into_shared();
82        let now = time_source.now();
83        let throughput_logs = ThroughputLogs::new(options.check_window(), now);
84        let resolution = throughput_logs.resolution();
85        Self {
86            throughput_logs,
87            resolution,
88            async_sleep: async_sleep.into_shared(),
89            time_source,
90            inner: body,
91            sleep_fut: None,
92            grace_period_fut: None,
93            options,
94        }
95    }
96}
97
98#[derive(Debug, PartialEq)]
99enum Error {
100    ThroughputBelowMinimum {
101        expected: Throughput,
102        actual: Throughput,
103    },
104}
105
106impl fmt::Display for Error {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            Self::ThroughputBelowMinimum { expected, actual } => {
110                write!(
111                    f,
112                    "minimum throughput was specified at {expected}, but throughput of {actual} was observed",
113                )
114            }
115        }
116    }
117}
118
119impl std::error::Error for Error {}
120
121/// Used to store the upload throughput in the interceptor context.
122#[derive(Clone, Debug)]
123pub(crate) struct UploadThroughput {
124    logs: Arc<Mutex<ThroughputLogs>>,
125}
126
127impl UploadThroughput {
128    pub(crate) fn new(time_window: Duration, now: SystemTime) -> Self {
129        Self {
130            logs: Arc::new(Mutex::new(ThroughputLogs::new(time_window, now))),
131        }
132    }
133
134    pub(crate) fn resolution(&self) -> Duration {
135        self.logs.lock().unwrap().resolution()
136    }
137
138    pub(crate) fn push_pending(&self, now: SystemTime) {
139        self.logs.lock().unwrap().push_pending(now);
140    }
141    pub(crate) fn push_bytes_transferred(&self, now: SystemTime, bytes: u64) {
142        self.logs.lock().unwrap().push_bytes_transferred(now, bytes);
143    }
144
145    pub(crate) fn mark_complete(&self) -> bool {
146        self.logs.lock().unwrap().mark_complete()
147    }
148
149    pub(crate) fn report(&self, now: SystemTime) -> ThroughputReport {
150        self.logs.lock().unwrap().report(now)
151    }
152}
153
154impl Storable for UploadThroughput {
155    type Storer = StoreReplace<Self>;
156}
157
158pin_project_lite::pin_project! {
159    pub(crate) struct ThroughputReadingBody<B> {
160        time_source: SharedTimeSource,
161        throughput: UploadThroughput,
162        #[pin]
163        inner: B,
164    }
165}
166
167impl<B> ThroughputReadingBody<B> {
168    pub(crate) fn new(
169        time_source: SharedTimeSource,
170        throughput: UploadThroughput,
171        body: B,
172    ) -> Self {
173        Self {
174            time_source,
175            throughput,
176            inner: body,
177        }
178    }
179}
180
181const ZERO_THROUGHPUT: Throughput = Throughput::new_bytes_per_second(0);
182
183// Helper trait for interpretting the throughput report.
184trait UploadReport {
185    fn minimum_throughput_violated(self, minimum_throughput: Throughput) -> (bool, Throughput);
186}
187impl UploadReport for ThroughputReport {
188    fn minimum_throughput_violated(self, minimum_throughput: Throughput) -> (bool, Throughput) {
189        let throughput = match self {
190            // stream has been exhausted, stop tracking violations
191            ThroughputReport::Complete => return (false, ZERO_THROUGHPUT),
192            // If the report is incomplete, then we don't have enough data yet to
193            // decide if minimum throughput was violated.
194            ThroughputReport::Incomplete => {
195                tracing::trace!(
196                    "not enough data to decide if minimum throughput has been violated"
197                );
198                return (false, ZERO_THROUGHPUT);
199            }
200            // If most of the datapoints are Poll::Pending, then the user has stalled.
201            // In this case, we don't want to say minimum throughput was violated.
202            ThroughputReport::Pending => {
203                tracing::debug!(
204                    "the user has stalled; this will not become a minimum throughput violation"
205                );
206                return (false, ZERO_THROUGHPUT);
207            }
208            // If there has been no polling, then the server has stalled. Alternatively,
209            // if we're transferring data, but it's too slow, then we also want to say
210            // that the minimum throughput has been violated.
211            ThroughputReport::NoPolling => ZERO_THROUGHPUT,
212            ThroughputReport::Transferred(tp) => tp,
213        };
214        if throughput < minimum_throughput {
215            tracing::debug!(
216                "current throughput: {throughput} is below minimum: {minimum_throughput}"
217            );
218            (true, throughput)
219        } else {
220            (false, throughput)
221        }
222    }
223}
224
225pin_project_lite::pin_project! {
226    /// Future that pairs with [`UploadThroughput`] to add a minimum throughput
227    /// requirement to a request upload stream.
228    pub(crate) struct UploadThroughputCheckFuture {
229        #[pin]
230        response: HttpConnectorFuture,
231        #[pin]
232        check_interval: Option<Sleep>,
233        #[pin]
234        grace_period: Option<Sleep>,
235
236        time_source: SharedTimeSource,
237        sleep_impl: SharedAsyncSleep,
238        upload_throughput: UploadThroughput,
239        resolution: Duration,
240        options: MinimumThroughputBodyOptions,
241
242        failing_throughput: Option<Throughput>,
243    }
244}
245
246impl UploadThroughputCheckFuture {
247    fn new(
248        response: HttpConnectorFuture,
249        time_source: SharedTimeSource,
250        sleep_impl: SharedAsyncSleep,
251        upload_throughput: UploadThroughput,
252        options: MinimumThroughputBodyOptions,
253    ) -> Self {
254        let resolution = upload_throughput.resolution();
255        Self {
256            response,
257            check_interval: Some(sleep_impl.sleep(resolution)),
258            grace_period: None,
259            time_source,
260            sleep_impl,
261            upload_throughput,
262            resolution,
263            options,
264            failing_throughput: None,
265        }
266    }
267}
268
269impl Future for UploadThroughputCheckFuture {
270    type Output = Result<HttpResponse, ConnectorError>;
271
272    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
273        let mut this = self.project();
274
275        if let Poll::Ready(output) = this.response.poll(cx) {
276            return Poll::Ready(output);
277        } else {
278            let mut below_minimum_throughput = false;
279            let check_interval_expired = this
280                .check_interval
281                .as_mut()
282                .as_pin_mut()
283                .expect("always set")
284                .poll(cx)
285                .is_ready();
286            if check_interval_expired {
287                // Set up the next check interval
288                *this.check_interval = Some(this.sleep_impl.sleep(*this.resolution));
289
290                // Wake so that the check interval future gets polled
291                // next time this poll method is called. If it never gets polled,
292                // then this task won't be woken to check again.
293                cx.waker().wake_by_ref();
294            }
295
296            let should_check = check_interval_expired || this.grace_period.is_some();
297            if should_check {
298                let now = this.time_source.now();
299                let report = this.upload_throughput.report(now);
300                let (violated, current_throughput) =
301                    report.minimum_throughput_violated(this.options.minimum_throughput());
302                below_minimum_throughput = violated;
303                if below_minimum_throughput && !this.failing_throughput.is_some() {
304                    *this.failing_throughput = Some(current_throughput);
305                } else if !below_minimum_throughput {
306                    *this.failing_throughput = None;
307                }
308            }
309
310            // If we kicked off a grace period and are now satisfied, clear out the grace period
311            if !below_minimum_throughput && this.grace_period.is_some() {
312                tracing::debug!("upload minimum throughput recovered during grace period");
313                *this.grace_period = None;
314            }
315            if below_minimum_throughput {
316                // Start a grace period if below minimum throughput
317                if this.grace_period.is_none() {
318                    tracing::debug!(
319                        grace_period=?this.options.grace_period(),
320                        "upload minimum throughput below configured minimum; starting grace period"
321                    );
322                    *this.grace_period = Some(this.sleep_impl.sleep(this.options.grace_period()));
323                }
324                // Check the grace period if one is already set and we're not satisfied
325                if let Some(grace_period) = this.grace_period.as_pin_mut() {
326                    if grace_period.poll(cx).is_ready() {
327                        tracing::debug!("grace period ended; timing out request");
328                        return Poll::Ready(Err(ConnectorError::timeout(
329                            Error::ThroughputBelowMinimum {
330                                expected: this.options.minimum_throughput(),
331                                actual: this
332                                    .failing_throughput
333                                    .expect("always set if there's a grace period"),
334                            }
335                            .into(),
336                        )));
337                    }
338                }
339            }
340        }
341        Poll::Pending
342    }
343}
344
345pin_project_lite::pin_project! {
346    #[project = EnumProj]
347    pub(crate) enum MaybeUploadThroughputCheckFuture {
348        Direct { #[pin] future: HttpConnectorFuture },
349        Checked { #[pin] future: UploadThroughputCheckFuture },
350    }
351}
352
353impl MaybeUploadThroughputCheckFuture {
354    pub(crate) fn new(
355        cfg: &mut ConfigBag,
356        components: &RuntimeComponents,
357        connector_future: HttpConnectorFuture,
358    ) -> Self {
359        if let Some(sspcfg) = cfg.load::<StalledStreamProtectionConfig>().cloned() {
360            if sspcfg.is_enabled() {
361                let options = MinimumThroughputBodyOptions::from(sspcfg);
362                return Self::new_inner(
363                    connector_future,
364                    components.time_source(),
365                    components.sleep_impl(),
366                    cfg.interceptor_state().load::<UploadThroughput>().cloned(),
367                    Some(options),
368                );
369            }
370        }
371        tracing::debug!("no minimum upload throughput checks");
372        Self::new_inner(connector_future, None, None, None, None)
373    }
374
375    fn new_inner(
376        response: HttpConnectorFuture,
377        time_source: Option<SharedTimeSource>,
378        sleep_impl: Option<SharedAsyncSleep>,
379        upload_throughput: Option<UploadThroughput>,
380        options: Option<MinimumThroughputBodyOptions>,
381    ) -> Self {
382        match (time_source, sleep_impl, upload_throughput, options) {
383            (Some(time_source), Some(sleep_impl), Some(upload_throughput), Some(options)) => {
384                tracing::debug!(options=?options, "applying minimum upload throughput check future");
385                Self::Checked {
386                    future: UploadThroughputCheckFuture::new(
387                        response,
388                        time_source,
389                        sleep_impl,
390                        upload_throughput,
391                        options,
392                    ),
393                }
394            }
395            _ => Self::Direct { future: response },
396        }
397    }
398}
399
400impl Future for MaybeUploadThroughputCheckFuture {
401    type Output = Result<HttpResponse, ConnectorError>;
402
403    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
404        match self.project() {
405            EnumProj::Direct { future } => future.poll(cx),
406            EnumProj::Checked { future } => future.poll(cx),
407        }
408    }
409}