LCOV - code coverage report
Current view: top level - libs/remote_storage/src - support.rs (source / functions) Coverage Total Hit
Test: 8b13a09a5c233d98abd4a0d3e59157e7db16d6fd.info Lines: 95.7 % 138 132
Test Date: 2024-11-21 10:53:51 Functions: 68.8 % 32 22

            Line data    Source code
       1              : use std::{
       2              :     future::Future,
       3              :     pin::Pin,
       4              :     task::{Context, Poll},
       5              :     time::Duration,
       6              : };
       7              : 
       8              : use bytes::Bytes;
       9              : use futures_util::Stream;
      10              : use tokio_util::sync::CancellationToken;
      11              : 
      12              : use crate::TimeoutOrCancel;
      13              : 
      14              : pin_project_lite::pin_project! {
      15              :     /// An `AsyncRead` adapter which carries a permit for the lifetime of the value.
      16              :     pub(crate) struct PermitCarrying<S> {
      17              :         permit: tokio::sync::OwnedSemaphorePermit,
      18              :         #[pin]
      19              :         inner: S,
      20              :     }
      21              : }
      22              : 
      23              : impl<S> PermitCarrying<S> {
      24           28 :     pub(crate) fn new(permit: tokio::sync::OwnedSemaphorePermit, inner: S) -> Self {
      25           28 :         Self { permit, inner }
      26           28 :     }
      27              : }
      28              : 
      29              : impl<S: Stream> Stream for PermitCarrying<S> {
      30              :     type Item = <S as Stream>::Item;
      31              : 
      32           51 :     fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
      33           51 :         self.project().inner.poll_next(cx)
      34           51 :     }
      35              : 
      36            0 :     fn size_hint(&self) -> (usize, Option<usize>) {
      37            0 :         self.inner.size_hint()
      38            0 :     }
      39              : }
      40              : 
      41              : pin_project_lite::pin_project! {
      42              :     pub(crate) struct DownloadStream<F, S> {
      43              :         hit: bool,
      44              :         #[pin]
      45              :         cancellation: F,
      46              :         #[pin]
      47              :         inner: S,
      48              :     }
      49              : }
      50              : 
      51              : impl<F, S> DownloadStream<F, S> {
      52          107 :     pub(crate) fn new(cancellation: F, inner: S) -> Self {
      53          107 :         Self {
      54          107 :             cancellation,
      55          107 :             hit: false,
      56          107 :             inner,
      57          107 :         }
      58          107 :     }
      59              : }
      60              : 
      61              : /// See documentation on [`crate::DownloadStream`] on rationale why `std::io::Error` is used.
      62              : impl<E, F, S> Stream for DownloadStream<F, S>
      63              : where
      64              :     std::io::Error: From<E>,
      65              :     F: Future<Output = E>,
      66              :     S: Stream<Item = std::io::Result<Bytes>>,
      67              : {
      68              :     type Item = <S as Stream>::Item;
      69              : 
      70         1577 :     fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
      71         1577 :         let this = self.project();
      72         1577 : 
      73         1577 :         if !*this.hit {
      74         1562 :             if let Poll::Ready(e) = this.cancellation.poll(cx) {
      75           13 :                 *this.hit = true;
      76           13 : 
      77           13 :                 // most likely this will be a std::io::Error wrapping a DownloadError
      78           13 :                 let e = Err(std::io::Error::from(e));
      79           13 :                 return Poll::Ready(Some(e));
      80         1549 :             }
      81              :         } else {
      82              :             // this would be perfectly valid behaviour for doing a graceful completion on the
      83              :             // download for example, but not one we expect to do right now.
      84           15 :             tracing::warn!("continuing polling after having cancelled or timeouted");
      85              :         }
      86              : 
      87         1564 :         this.inner.poll_next(cx)
      88         1577 :     }
      89              : 
      90            0 :     fn size_hint(&self) -> (usize, Option<usize>) {
      91            0 :         self.inner.size_hint()
      92            0 :     }
      93              : }
      94              : 
      95              : /// Fires only on the first cancel or timeout, not on both.
      96          120 : pub(crate) fn cancel_or_timeout(
      97          120 :     timeout: Duration,
      98          120 :     cancel: CancellationToken,
      99          120 : ) -> impl std::future::Future<Output = TimeoutOrCancel> + 'static {
     100          120 :     // futures are lazy, they don't do anything before being polled.
     101          120 :     //
     102          120 :     // "precalculate" the wanted deadline before returning the future, so that we can use pause
     103          120 :     // failpoint to trigger a timeout in test.
     104          120 :     let deadline = tokio::time::Instant::now() + timeout;
     105          110 :     async move {
     106          110 :         tokio::select! {
     107          110 :             _ = tokio::time::sleep_until(deadline) => TimeoutOrCancel::Timeout,
     108          110 :             _ = cancel.cancelled() => {
     109            8 :                 TimeoutOrCancel::Cancel
     110              :             },
     111              :         }
     112           13 :     }
     113          120 : }
     114              : 
     115              : #[cfg(test)]
     116              : mod tests {
     117              :     use super::*;
     118              :     use crate::DownloadError;
     119              :     use futures::stream::StreamExt;
     120              : 
     121              :     #[tokio::test(start_paused = true)]
     122            3 :     async fn cancelled_download_stream() {
     123            3 :         let inner = futures::stream::pending();
     124            3 :         let timeout = Duration::from_secs(120);
     125            3 :         let cancel = CancellationToken::new();
     126            3 : 
     127            3 :         let stream = DownloadStream::new(cancel_or_timeout(timeout, cancel.clone()), inner);
     128            3 :         let mut stream = std::pin::pin!(stream);
     129            3 : 
     130            3 :         let mut first = stream.next();
     131            3 : 
     132            3 :         tokio::select! {
     133            3 :             _ = &mut first => unreachable!("we haven't yet cancelled nor is timeout passed"),
     134            3 :             _ = tokio::time::sleep(Duration::from_secs(1)) => {},
     135            3 :         }
     136            3 : 
     137            3 :         cancel.cancel();
     138            3 : 
     139            3 :         let e = first.await.expect("there must be some").unwrap_err();
     140            3 :         assert!(matches!(e.kind(), std::io::ErrorKind::Other), "{e:?}");
     141            3 :         let inner = e.get_ref().expect("inner should be set");
     142            3 :         assert!(
     143            3 :             inner
     144            3 :                 .downcast_ref::<DownloadError>()
     145            3 :                 .is_some_and(|e| matches!(e, DownloadError::Cancelled)),
     146            3 :             "{inner:?}"
     147            3 :         );
     148            3 :         let e = DownloadError::from(e);
     149            3 :         assert!(matches!(e, DownloadError::Cancelled), "{e:?}");
     150            3 : 
     151            3 :         tokio::select! {
     152            3 :             _ = stream.next() => unreachable!("no timeout ever happens as we were already cancelled"),
     153            3 :             _ = tokio::time::sleep(Duration::from_secs(121)) => {},
     154            3 :         }
     155            3 :     }
     156              : 
     157              :     #[tokio::test(start_paused = true)]
     158            3 :     async fn timeouted_download_stream() {
     159            3 :         let inner = futures::stream::pending();
     160            3 :         let timeout = Duration::from_secs(120);
     161            3 :         let cancel = CancellationToken::new();
     162            3 : 
     163            3 :         let stream = DownloadStream::new(cancel_or_timeout(timeout, cancel.clone()), inner);
     164            3 :         let mut stream = std::pin::pin!(stream);
     165            3 : 
     166            3 :         // because the stream uses 120s timeout and we are paused, we advance to 120s right away.
     167            3 :         let first = stream.next();
     168            3 : 
     169            3 :         let e = first.await.expect("there must be some").unwrap_err();
     170            3 :         assert!(matches!(e.kind(), std::io::ErrorKind::Other), "{e:?}");
     171            3 :         let inner = e.get_ref().expect("inner should be set");
     172            3 :         assert!(
     173            3 :             inner
     174            3 :                 .downcast_ref::<DownloadError>()
     175            3 :                 .is_some_and(|e| matches!(e, DownloadError::Timeout)),
     176            3 :             "{inner:?}"
     177            3 :         );
     178            3 :         let e = DownloadError::from(e);
     179            3 :         assert!(matches!(e, DownloadError::Timeout), "{e:?}");
     180            3 : 
     181            3 :         cancel.cancel();
     182            3 : 
     183            3 :         tokio::select! {
     184            3 :             _ = stream.next() => unreachable!("no cancellation ever happens because we already timed out"),
     185            3 :             _ = tokio::time::sleep(Duration::from_secs(121)) => {},
     186            3 :         }
     187            3 :     }
     188              : 
     189              :     #[tokio::test]
     190            3 :     async fn notified_but_pollable_after() {
     191            3 :         let inner = futures::stream::once(futures::future::ready(Ok(bytes::Bytes::from_static(
     192            3 :             b"hello world",
     193            3 :         ))));
     194            3 :         let timeout = Duration::from_secs(120);
     195            3 :         let cancel = CancellationToken::new();
     196            3 : 
     197            3 :         cancel.cancel();
     198            3 :         let stream = DownloadStream::new(cancel_or_timeout(timeout, cancel.clone()), inner);
     199            3 :         let mut stream = std::pin::pin!(stream);
     200            3 : 
     201            3 :         let next = stream.next().await;
     202            3 :         let ioe = next.unwrap().unwrap_err();
     203            3 :         assert!(
     204            3 :             matches!(
     205            3 :                 ioe.get_ref().unwrap().downcast_ref::<DownloadError>(),
     206            3 :                 Some(&DownloadError::Cancelled)
     207            3 :             ),
     208            3 :             "{ioe:?}"
     209            3 :         );
     210            3 : 
     211            3 :         let next = stream.next().await;
     212            3 :         let bytes = next.unwrap().unwrap();
     213            3 :         assert_eq!(&b"hello world"[..], bytes);
     214            3 :     }
     215              : }
        

Generated by: LCOV version 2.1-beta