aws_smithy_runtime/client/http/body/
minimum_throughput.rs1#[cfg(feature = "http-02x")]
14pub mod http_body_0_4_x;
15
16pub mod http_body_1_x;
18
19pub 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#[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 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 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#[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
183trait 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 ThroughputReport::Complete => return (false, ZERO_THROUGHPUT),
192 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 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 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 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 *this.check_interval = Some(this.sleep_impl.sleep(*this.resolution));
289
290 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 !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 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 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}