Ctrl K
ComunidadGuías…

src/http/body.rs

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
use axum::body::{Body, Bytes};
use futures::task::AtomicWaker;
use http_body::{Body as HttpBody, Frame, SizeHint};
use nyro_kernel::Lease;
use std::{
    pin::Pin,
    sync::{Arc, Mutex},
    task::{Context, Poll},
};
use tokio::{task::JoinHandle, time::Instant};
use tokio_util::task::TaskTracker;

enum Terminal {
    Active,
    Failed,
    Complete,
}

struct State<T: Send + Sync + 'static> {
    body: Option<Body>,
    lease: Option<Lease<T>>,
    cancellation: tokio_util::sync::CancellationToken,
    terminal: Terminal,
}

fn finish<T: Send + Sync + 'static>(state: &Arc<Mutex<State<T>>>, terminal: Terminal) -> bool {
    let (body, lease) = {
        let mut state = state.lock().unwrap();
        if !matches!(state.terminal, Terminal::Active) {
            return false;
        }
        state.terminal = terminal;
        (state.body.take(), state.lease.take())
    };
    drop(body);
    drop(lease);
    true
}

struct Retained<T: Send + Sync + 'static> {
    state: Arc<Mutex<State<T>>>,
    cancellation: tokio_util::sync::CancellationToken,
    deadline: Instant,
    waker: Arc<AtomicWaker>,
    watchdog: JoinHandle<()>,
}

impl<T: Send + Sync + 'static> Retained<T> {
    fn stop(&self, terminal: Terminal) {
        self.watchdog.abort();
        if finish(&self.state, terminal) {
            self.waker.wake();
        }
    }
}

impl<T: Send + Sync + 'static> Drop for Retained<T> {
    fn drop(&mut self) {
        self.stop(Terminal::Complete);
    }
}

impl<T: Send + Sync + 'static> HttpBody for Retained<T> {
    type Data = Bytes;
    type Error = axum::Error;

    fn poll_frame(
        self: Pin<&mut Self>,
        context: &mut Context<'_>,
    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
        let this = self.get_mut();
        this.waker.register(context.waker());
        if this.cancellation.is_cancelled() || Instant::now() >= this.deadline {
            this.stop(Terminal::Failed);
        }

        let frame = {
            let mut state = this.state.lock().unwrap();
            match state.terminal {
                Terminal::Active => {
                    Some(Pin::new(state.body.as_mut().unwrap()).poll_frame(context))
                }
                Terminal::Failed => {
                    state.terminal = Terminal::Complete;
                    None
                }
                Terminal::Complete => return Poll::Ready(None),
            }
        };

        match frame {
            None => Poll::Ready(Some(Err(axum::Error::new(std::io::Error::other(
                "response cancelled or deadline exceeded",
            ))))),
            Some(Poll::Pending) => Poll::Pending,
            Some(Poll::Ready(Some(Ok(frame)))) => Poll::Ready(Some(Ok(frame))),
            Some(Poll::Ready(Some(Err(error)))) => {
                this.stop(Terminal::Complete);
                Poll::Ready(Some(Err(error)))
            }
            Some(Poll::Ready(None)) => {
                this.stop(Terminal::Complete);
                Poll::Ready(None)
            }
        }
    }

    fn is_end_stream(&self) -> bool {
        matches!(self.state.lock().unwrap().terminal, Terminal::Complete)
    }

    fn size_hint(&self) -> SizeHint {
        self.state
            .lock()
            .unwrap()
            .body
            .as_ref()
            .map_or_else(SizeHint::default, HttpBody::size_hint)
    }
}

pub(super) fn retain<T: Send + Sync + 'static>(
    body: Body,
    lease: Lease<T>,
    deadline: Instant,
    tracker: &TaskTracker,
) -> Body {
    let cancellation = lease.cancellation();
    let state = Arc::new(Mutex::new(State {
        body: Some(body),
        lease: Some(lease),
        cancellation: cancellation.clone(),
        terminal: Terminal::Active,
    }));
    let waker = Arc::new(AtomicWaker::new());
    let watchdog_state = Arc::clone(&state);
    let watchdog_waker = Arc::clone(&waker);
    let watchdog = tracker.spawn(async move {
        let cancelled = watchdog_state.lock().unwrap().cancellation.clone();
        tokio::select! {
            biased;
            _ = cancelled.cancelled() => {},
            _ = tokio::time::sleep_until(deadline) => {},
        }
        if finish(&watchdog_state, Terminal::Failed) {
            watchdog_waker.wake();
        }
    });

    Body::new(Retained {
        state,
        cancellation,
        deadline,
        waker,
        watchdog,
    })
}

#[cfg(test)]
mod tests {
    use super::retain;
    use async_trait::async_trait;
    use axum::body::{Body, Bytes, to_bytes};
    use http_body::Frame;
    use nyro_kernel::{Candidate, Component, Context, Host, HostOptions, Lease, Lifecycle};
    use std::{
        convert::Infallible,
        pin::Pin,
        sync::{Arc, Mutex},
        task::{Context as TaskContext, Poll},
        time::Duration,
    };
    use tokio::time::{Instant, timeout};
    use tokio_util::{sync::CancellationToken, task::TaskTracker};

    type Events = Arc<Mutex<Vec<&'static str>>>;

    struct Resource(Events);

    #[async_trait]
    impl Lifecycle for Resource {
        async fn start(&mut self, _: &Context) -> Result<(), String> {
            Ok(())
        }
        fn begin_stop(&mut self) {
            self.0.lock().unwrap().push("component-stop");
        }
        async fn wait_stopped(&mut self, _: &Context) -> Result<(), String> {
            Ok(())
        }
    }

    struct Source {
        events: Events,
    }

    impl Drop for Source {
        fn drop(&mut self) {
            self.events.lock().unwrap().push("source-drop");
        }
    }

    impl http_body::Body for Source {
        type Data = Bytes;
        type Error = Infallible;

        fn poll_frame(
            self: Pin<&mut Self>,
            _: &mut TaskContext<'_>,
        ) -> Poll<Option<Result<Frame<Bytes>, Infallible>>> {
            Poll::Pending
        }
    }

    struct EofSource {
        events: Events,
        sent: bool,
    }

    impl Drop for EofSource {
        fn drop(&mut self) {
            self.events.lock().unwrap().push("source-drop");
        }
    }

    impl http_body::Body for EofSource {
        type Data = Bytes;
        type Error = Infallible;

        fn poll_frame(
            mut self: Pin<&mut Self>,
            _: &mut TaskContext<'_>,
        ) -> Poll<Option<Result<Frame<Bytes>, Infallible>>> {
            if self.sent {
                Poll::Ready(None)
            } else {
                self.sent = true;
                Poll::Ready(Some(Ok(Frame::data(Bytes::from_static(b"ok")))))
            }
        }
    }

    async fn host_with_retiring_lease(events: Events) -> (Arc<Host<()>>, Lease<()>) {
        host_with_retiring_lease_with_options(events, HostOptions::default()).await
    }

    async fn host_with_retiring_lease_with_options(
        events: Events,
        options: HostOptions,
    ) -> (Arc<Host<()>>, Lease<()>) {
        let host = Arc::new(Host::new(options));
        let context = Context {
            deadline: Instant::now() + Duration::from_secs(1),
            cancellation: CancellationToken::new(),
        };
        host.activate(
            Candidate {
                version: "old".into(),
                fingerprint: None,
                value: (),
                components: vec![Component {
                    id: "resource".into(),
                    after: vec![],
                    lifecycle: Box::new(Resource(events)),
                }],
            },
            context,
        )
        .await
        .unwrap();
        let lease = host.acquire().unwrap();
        host.activate(
            Candidate {
                version: "new".into(),
                fingerprint: None,
                value: (),
                components: vec![],
            },
            Context {
                deadline: Instant::now() + Duration::from_secs(1),
                cancellation: CancellationToken::new(),
            },
        )
        .await
        .unwrap();
        (host, lease)
    }

    async fn until(mut predicate: impl FnMut() -> bool) {
        timeout(Duration::from_secs(1), async {
            while !predicate() {
                tokio::task::yield_now().await;
            }
        })
        .await
        .unwrap();
    }

    #[tokio::test]
    async fn retains_source_and_lease_until_response_drop() {
        let events = Events::default();
        let (host, lease) = host_with_retiring_lease(Arc::clone(&events)).await;
        let tracker = TaskTracker::new();
        let body = retain(
            Body::new(Source {
                events: Arc::clone(&events),
            }),
            lease,
            Instant::now() + Duration::from_secs(1),
            &tracker,
        );

        assert_eq!(host.status().retiring[0].leases, 1);
        assert!(events.lock().unwrap().is_empty());
        drop(body);
        until(|| host.status().retiring.is_empty()).await;
        assert_eq!(*events.lock().unwrap(), ["source-drop", "component-stop"]);
        tracker.close();
        tracker.wait().await;
    }

    #[tokio::test]
    async fn eof_drops_the_source_before_retiring_the_component() {
        let events = Events::default();
        let (host, lease) = host_with_retiring_lease(Arc::clone(&events)).await;
        let tracker = TaskTracker::new();
        let body = retain(
            Body::new(EofSource {
                events: Arc::clone(&events),
                sent: false,
            }),
            lease,
            Instant::now() + Duration::from_secs(1),
            &tracker,
        );

        assert_eq!(to_bytes(body, 1024).await.unwrap(), "ok");
        until(|| host.status().retiring.is_empty()).await;
        assert_eq!(*events.lock().unwrap(), ["source-drop", "component-stop"]);
        tracker.close();
        tracker.wait().await;
    }

    #[tokio::test]
    async fn deadline_releases_an_unpolled_response_and_then_yields_one_error() {
        let events = Events::default();
        let (host, lease) = host_with_retiring_lease(Arc::clone(&events)).await;
        let tracker = TaskTracker::new();
        let mut body = retain(
            Body::new(Source {
                events: Arc::clone(&events),
            }),
            lease,
            Instant::now(),
            &tracker,
        );

        until(|| host.status().retiring.is_empty()).await;
        assert_eq!(*events.lock().unwrap(), ["source-drop", "component-stop"]);
        assert!(
            futures::future::poll_fn(|cx| http_body::Body::poll_frame(Pin::new(&mut body), cx))
                .await
                .unwrap()
                .is_err()
        );
        assert!(
            futures::future::poll_fn(|cx| http_body::Body::poll_frame(Pin::new(&mut body), cx))
                .await
                .is_none()
        );
        tracker.close();
        tracker.wait().await;
    }

    #[tokio::test]
    async fn host_shutdown_cancellation_releases_an_unpolled_response() {
        let events = Events::default();
        let (host, lease) = host_with_retiring_lease_with_options(
            Arc::clone(&events),
            HostOptions {
                shutdown_grace: Duration::ZERO,
                cleanup_timeout: Duration::from_secs(1),
            },
        )
        .await;
        let tracker = TaskTracker::new();
        let body = retain(
            Body::new(Source {
                events: Arc::clone(&events),
            }),
            lease,
            Instant::now() + Duration::from_secs(60),
            &tracker,
        );

        let _ = host.shutdown().await;
        until(|| events.lock().unwrap().contains(&"source-drop")).await;
        drop(body);
        tracker.close();
        tracker.wait().await;
    }
}