Skip to main content

xet_data/file_reconstruction/data_writer/
download_stream.rs

1use std::sync::Arc;
2
3use bytes::Bytes;
4use tokio::sync::Notify;
5use tokio::sync::mpsc::UnboundedReceiver;
6#[cfg(target_family = "wasm")]
7use tokio_with_wasm::alias as tokio;
8use tracing::info;
9
10use super::super::error::{FileReconstructionError, Result};
11use super::super::file_reconstructor::FileReconstructor;
12use super::super::run_state::RunState;
13use super::sequential_writer::{SequentialRetrievalItem, SequentialWriter};
14
15/// A streaming download handle that yields data chunks as they are reconstructed.
16///
17/// Created by [`FileReconstructor::reconstruct_to_stream`].  The reconstruction
18/// task is spawned immediately but pauses until [`start`](Self::start) is
19/// called (or the first [`next`](Self::next) / [`blocking_next`](Self::blocking_next)).
20/// Because the `tokio::spawn` happens at construction time, subsequent calls to
21/// `start()`, `next()`, and `blocking_next()` do **not** require a tokio runtime
22/// context.
23///
24/// Data is delivered by pulling items directly from the sequential writer's
25/// internal queue, bypassing the synchronous writer thread entirely. Each call
26/// to [`blocking_next`](Self::blocking_next) / [`next`](Self::next) returns the
27/// next sequential chunk, or `None` when the download is complete. Any
28/// reconstruction error is surfaced on the call that would have returned the
29/// next chunk (or on the final `None` boundary) via the shared run state.
30pub struct DownloadStream {
31    /// Channel receiver for sequential retrieval items from the writer queue.
32    receiver: UnboundedReceiver<SequentialRetrievalItem>,
33    /// Whether the stream has finished (no more data).
34    finished: bool,
35    /// Shared run state with the `FileReconstructor`. When cancelled,
36    /// the reconstruction loop aborts promptly at its next check point or
37    /// `select!` branch. Also used for progress reporting and error propagation.
38    run_state: Arc<RunState>,
39    /// Signal to unblock the spawned reconstruction task. `Some` means
40    /// `start()` has not yet been called; the spawned task is waiting.
41    start_signal: Option<Arc<Notify>>,
42}
43
44impl DownloadStream {
45    /// Creates a new `DownloadStream`, immediately spawning the reconstruction
46    /// task on the current tokio runtime.  The task blocks on an internal
47    /// [`Notify`] until [`start`](Self::start) is called.
48    ///
49    /// # Panics
50    ///
51    /// Panics if called outside a tokio runtime context.
52    pub(crate) fn new(reconstructor: FileReconstructor, run_state: Arc<RunState>) -> Self {
53        let (data_writer, receiver) = SequentialWriter::new_streaming(run_state.clone());
54        let start_signal = Arc::new(Notify::new());
55
56        let signal = start_signal.clone();
57        let rs = run_state.clone();
58        tokio::task::spawn(async move {
59            signal.notified().await;
60            info!(file_hash = %rs.file_hash(), "Starting download stream");
61            let _ = reconstructor.run(data_writer, rs, true).await;
62        });
63
64        Self {
65            receiver,
66            finished: false,
67            run_state,
68            start_signal: Some(start_signal),
69        }
70    }
71
72    pub(crate) fn abort_callback(&self) -> Box<dyn Fn() + Send + Sync> {
73        let run_state = self.run_state.clone();
74        let start_signal = self.start_signal.clone();
75        Box::new(move || {
76            run_state.cancel();
77            if let Some(signal) = start_signal.as_ref() {
78                signal.notify_one();
79            }
80        })
81    }
82
83    /// Unblocks the reconstruction task so it begins producing data.
84    ///
85    /// If already started, this is a no-op. Called automatically on the first
86    /// [`next`](Self::next) / [`blocking_next`](Self::blocking_next).
87    ///
88    /// This method is non-async and does not require a tokio runtime context.
89    pub fn start(&mut self) {
90        if let Some(signal) = self.start_signal.take() {
91            signal.notify_one();
92        }
93    }
94
95    fn ensure_started(&mut self) {
96        if self.start_signal.is_some() {
97            self.start();
98        }
99    }
100
101    fn cancel_reconstruction(&self) {
102        self.run_state.cancel();
103        if let Some(signal) = self.start_signal.as_ref() {
104            signal.notify_one();
105        }
106    }
107
108    /// Returns the next chunk of downloaded data, blocking the current thread
109    /// until data is available.
110    ///
111    /// Returns `Ok(None)` when the download is complete.
112    ///
113    /// # Panics
114    ///
115    /// Panics if called from within an async runtime context (e.g. inside a
116    /// `tokio::spawn` or `async fn`). Use from a regular thread or from
117    /// [`tokio::task::spawn_blocking`] instead. For the async-safe variant,
118    /// use [`next`](Self::next).
119    #[cfg(not(target_family = "wasm"))]
120    pub fn blocking_next(&mut self) -> Result<Option<Bytes>> {
121        if self.finished {
122            return Ok(None);
123        }
124        self.ensure_started();
125
126        match self.receiver.blocking_recv() {
127            Some(SequentialRetrievalItem::Data { receiver, permit }) => {
128                let data = match receiver.blocking_recv() {
129                    Ok(data) => data,
130                    Err(_) => {
131                        self.run_state.check_error()?;
132                        return Err(FileReconstructionError::InternalWriterError(
133                            "Data sender was dropped before sending data.".to_string(),
134                        ));
135                    },
136                };
137                self.run_state.report_bytes_written(data.len() as u64);
138                drop(permit);
139                Ok(Some(data))
140            },
141            Some(SequentialRetrievalItem::Finish) | None => {
142                self.finished = true;
143                self.run_state.check_error()?;
144                Ok(None)
145            },
146        }
147    }
148
149    /// Returns the next chunk of downloaded data asynchronously.
150    ///
151    /// Returns `Ok(None)` when the download is complete or cancelled.
152    pub async fn next(&mut self) -> Result<Option<Bytes>> {
153        if self.finished {
154            return Ok(None);
155        }
156        self.ensure_started();
157
158        let item = if let Ok(item) = self.receiver.try_recv() {
159            Some(item)
160        } else {
161            tokio::select! {
162                biased;
163                recv = self.receiver.recv() => recv,
164                _ = self.run_state.cancelled() => None,
165            }
166        };
167
168        match item {
169            Some(SequentialRetrievalItem::Data { receiver, permit }) => {
170                let data = match receiver.await {
171                    Ok(data) => data,
172                    Err(_) => {
173                        self.run_state.check_error()?;
174                        return Err(FileReconstructionError::InternalWriterError(
175                            "Data sender was dropped before sending data.".to_string(),
176                        ));
177                    },
178                };
179                self.run_state.report_bytes_written(data.len() as u64);
180                drop(permit);
181                Ok(Some(data))
182            },
183            Some(SequentialRetrievalItem::Finish) | None => {
184                self.finished = true;
185                self.run_state.check_error()?;
186                Ok(None)
187            },
188        }
189    }
190
191    /// Cancels the in-progress (or not-yet-started) download.
192    ///
193    /// Signals the shared run state so the reconstruction loop aborts at its
194    /// next check point or `select!` branch, and closes the channel receiver.
195    /// After calling this, subsequent calls to [`blocking_next`](Self::blocking_next)
196    /// / [`next`](Self::next) will return `Ok(None)`.
197    pub fn cancel(&mut self) {
198        self.cancel_reconstruction();
199        let _ = self.start_signal.take();
200        self.receiver.close();
201        self.finished = true;
202    }
203}
204
205impl Drop for DownloadStream {
206    fn drop(&mut self) {
207        self.cancel_reconstruction();
208        self.receiver.close();
209    }
210}