Skip to main content

xet_data/file_reconstruction/data_writer/
unordered_download_stream.rs

1use std::sync::Arc;
2use std::sync::atomic::Ordering;
3
4use bytes::Bytes;
5use tokio::sync::Notify;
6use tokio::sync::mpsc::UnboundedReceiver;
7#[cfg(target_family = "wasm")]
8use tokio_with_wasm::alias as tokio;
9use tracing::info;
10
11use super::super::error::Result;
12use super::super::file_reconstructor::FileReconstructor;
13use super::super::run_state::RunState;
14use super::unordered_writer::{CompletedTerm, UnorderedWriterProgress};
15
16/// A streaming download handle that yields data chunks in completion order,
17/// each tagged with its byte offset in the output file.
18///
19/// Created by [`FileReconstructor::reconstruct_to_unordered_stream`]. The
20/// reconstruction task is spawned immediately but pauses until
21/// [`start`](Self::start) is called (or the first [`next`](Self::next) /
22/// [`blocking_next`](Self::blocking_next)). Because the `tokio::spawn`
23/// happens at construction time, subsequent calls to `start()`, `next()`,
24/// and `blocking_next()` do **not** require a tokio runtime context.
25///
26/// Unlike [`DownloadStream`](super::download_stream::DownloadStream), data
27/// chunks may arrive out of order. Each chunk is returned as `(offset, Bytes)`
28/// so the consumer knows where it belongs. Progress can be monitored via
29/// the tracking methods which read shared atomic counters.
30///
31/// Holds only `Arc<WriterProgress>`, not the writer itself, so the channel
32/// sender is dropped naturally when the reconstruction task finishes.
33pub struct UnorderedDownloadStream {
34    /// Shared atomic progress counters (also held by the writer and its tasks).
35    progress: Arc<UnorderedWriterProgress>,
36
37    /// Channel receiver for completed terms from spawned tasks.
38    receiver: UnboundedReceiver<Result<CompletedTerm>>,
39
40    /// Whether the stream has finished (no more data).
41    finished: bool,
42
43    /// Shared run state with the `FileReconstructor`.
44    run_state: Arc<RunState>,
45
46    /// Signal to unblock the spawned reconstruction task. `Some` means
47    /// `start()` has not yet been called; the spawned task is waiting.
48    start_signal: Option<Arc<Notify>>,
49}
50
51impl UnorderedDownloadStream {
52    /// Creates a new `UnorderedDownloadStream`, immediately spawning the
53    /// reconstruction task on the current tokio runtime. The task blocks
54    /// on an internal [`Notify`] until [`start`](Self::start) is called.
55    ///
56    /// # Panics
57    ///
58    /// Panics if called outside a tokio runtime context.
59    pub(crate) fn new(reconstructor: FileReconstructor, run_state: Arc<RunState>) -> Self {
60        use super::unordered_writer::UnorderedWriter;
61
62        let (writer, receiver, progress) = UnorderedWriter::new_streaming(run_state.clone());
63        let start_signal = Arc::new(Notify::new());
64
65        let signal = start_signal.clone();
66        let rs = run_state.clone();
67        tokio::task::spawn(async move {
68            signal.notified().await;
69            info!(file_hash = %rs.file_hash(), "Starting unordered download stream");
70            let _ = reconstructor.run(writer, rs, true).await;
71        });
72
73        Self {
74            progress,
75            receiver,
76            finished: false,
77            run_state,
78            start_signal: Some(start_signal),
79        }
80    }
81
82    pub(crate) fn abort_callback(&self) -> Box<dyn Fn() + Send + Sync> {
83        let run_state = self.run_state.clone();
84        let start_signal = self.start_signal.clone();
85        Box::new(move || {
86            run_state.cancel();
87            if let Some(signal) = start_signal.as_ref() {
88                signal.notify_one();
89            }
90        })
91    }
92
93    /// Unblocks the reconstruction task so it begins producing data.
94    ///
95    /// If already started, this is a no-op. Called automatically on the first
96    /// [`next`](Self::next) / [`blocking_next`](Self::blocking_next).
97    ///
98    /// This method is non-async and does not require a tokio runtime context.
99    pub fn start(&mut self) {
100        if let Some(signal) = self.start_signal.take() {
101            signal.notify_one();
102        }
103    }
104
105    fn ensure_started(&mut self) {
106        if self.start_signal.is_some() {
107            self.start();
108        }
109    }
110
111    fn cancel_reconstruction(&self) {
112        self.run_state.cancel();
113        if let Some(signal) = self.start_signal.as_ref() {
114            signal.notify_one();
115        }
116    }
117
118    /// Returns the next chunk of downloaded data with its byte offset,
119    /// blocking the current thread until data is available.
120    ///
121    /// Returns `Ok(None)` when the download is complete.
122    ///
123    /// # Panics
124    ///
125    /// Panics if called from within an async runtime context. Use from a
126    /// regular thread or from [`tokio::task::spawn_blocking`] instead.
127    /// For the async-safe variant, use [`next`](Self::next).
128    #[cfg(not(target_family = "wasm"))]
129    pub fn blocking_next(&mut self) -> Result<Option<(u64, Bytes)>> {
130        if self.finished {
131            return Ok(None);
132        }
133        self.ensure_started();
134
135        match self.receiver.blocking_recv() {
136            Some(result) => self.process_term(result),
137            None => {
138                self.finished = true;
139                self.run_state.check_error()?;
140                Ok(None)
141            },
142        }
143    }
144
145    /// Returns the next chunk of downloaded data with its byte offset
146    /// asynchronously.
147    ///
148    /// Returns `Ok(None)` when the download is complete.
149    pub async fn next(&mut self) -> Result<Option<(u64, Bytes)>> {
150        if self.finished {
151            return Ok(None);
152        }
153        self.ensure_started();
154
155        if let Ok(result) = self.receiver.try_recv() {
156            return self.process_term(result);
157        }
158
159        let next_item = tokio::select! {
160            biased;
161            recv = self.receiver.recv() => recv,
162            _ = self.run_state.cancelled() => None,
163        };
164
165        match next_item {
166            Some(result) => self.process_term(result),
167            None => {
168                self.finished = true;
169                self.run_state.check_error()?;
170                Ok(None)
171            },
172        }
173    }
174
175    fn process_term(&mut self, result: Result<CompletedTerm>) -> Result<Option<(u64, Bytes)>> {
176        let term = result?;
177        self.run_state.report_bytes_written(term.data.len() as u64);
178        let offset = term.byte_range.start;
179        let data = term.data;
180        drop(term.permit);
181        Ok(Some((offset, data)))
182    }
183
184    /// Cancels the in-progress (or not-yet-started) download.
185    ///
186    /// Signals the shared run state so the reconstruction loop aborts at its
187    /// next check point. After calling this, subsequent calls to
188    /// [`blocking_next`](Self::blocking_next) / [`next`](Self::next) will
189    /// return `Ok(None)`.
190    pub fn cancel(&mut self) {
191        self.cancel_reconstruction();
192        let _ = self.start_signal.take();
193        self.receiver.close();
194        self.finished = true;
195    }
196
197    // ── Tracking methods ─────────────────────────────────────────────────
198
199    /// Total bytes expected for the reconstruction, read from the progress
200    /// updater. Returns 0 if not yet known or no progress updater is set.
201    pub fn total_bytes_expected(&self) -> u64 {
202        self.run_state
203            .progress_updater()
204            .map(|u| u.item().total_bytes.load(Ordering::Acquire))
205            .unwrap_or(0)
206    }
207
208    /// Bytes currently being fetched by in-progress tasks.
209    pub fn bytes_in_progress(&self) -> u64 {
210        self.progress.bytes_in_progress()
211    }
212
213    /// Bytes that have been delivered through the progress updater.
214    /// Returns 0 if no progress updater is set.
215    pub fn bytes_completed(&self) -> u64 {
216        self.run_state
217            .progress_updater()
218            .map(|u| u.total_bytes_completed())
219            .unwrap_or(0)
220    }
221
222    /// Number of tasks currently resolving data futures.
223    pub fn terms_in_progress(&self) -> u64 {
224        self.progress.terms_in_progress()
225    }
226
227    /// Returns `true` once the stream has reached terminal state.
228    ///
229    /// This flips to `true` after [`next`](Self::next) / [`blocking_next`](Self::blocking_next)
230    /// has observed the end-of-stream (`None`), or after [`cancel`](Self::cancel).
231    /// Buffered but unconsumed channel items do not count as complete.
232    pub fn is_complete(&self) -> bool {
233        self.finished
234    }
235}
236
237impl Drop for UnorderedDownloadStream {
238    fn drop(&mut self) {
239        self.cancel_reconstruction();
240        self.receiver.close();
241    }
242}