xet_data/file_reconstruction/reconstruction_terms/
manager.rs1use std::collections::VecDeque;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicU64, Ordering};
4#[cfg(not(target_family = "wasm"))]
5use std::time::Instant;
6
7use more_asserts::*;
8use tokio::task::JoinHandle;
9#[cfg(target_family = "wasm")]
10use tokio_with_wasm::alias as tokio;
11use tracing::{debug, info};
12#[cfg(target_family = "wasm")]
13use web_time::Instant;
14use xet_client::cas_client::Client;
15use xet_client::cas_types::FileRange;
16use xet_core_structures::ExpWeightedMovingAvg;
17use xet_core_structures::merklehash::MerkleHash;
18use xet_runtime::config::ReconstructionConfig;
19use xet_runtime::core::XetContext;
20
21use super::super::FileReconstructionError;
22use super::super::error::Result;
23use super::file_term::{FileTerm, retrieve_file_term_block};
24use crate::progress_tracking::ItemProgressUpdater;
25
26type RawFetchedFileTerms = Result<Option<(Vec<FileTerm>, u64, u64)>>;
27
28pub struct ReconstructionTermManager {
32 ctx: XetContext,
33 config: Arc<ReconstructionConfig>,
34 client: Arc<dyn Client>,
35 file_hash: MerkleHash,
36 requested_byte_range: FileRange,
37 last_block_info: Option<(Instant, FileRange)>,
38 known_final_byte_position: Arc<AtomicU64>,
39 prefetched_byte_position: u64,
40 current_active_byte_position: u64,
41 prefetch_queue: VecDeque<JoinHandle<RawFetchedFileTerms>>,
42 completion_rate_estimator: ExpWeightedMovingAvg,
43 progress_updater: Option<Arc<ItemProgressUpdater>>,
44 total_bytes_reported: u64,
45 total_transfer_bytes_reported: u64,
46}
47
48impl ReconstructionTermManager {
49 pub async fn new(
50 ctx: XetContext,
51 config: Arc<ReconstructionConfig>,
52 client: Arc<dyn Client>,
53 file_hash: MerkleHash,
54 file_byte_range: FileRange,
55 progress_updater: Option<Arc<ItemProgressUpdater>>,
56 ) -> Result<Self> {
57 let completion_rate_estimator =
58 ExpWeightedMovingAvg::new_count_decay(config.completion_rate_estimator_half_life);
59
60 let requested_byte_range = file_byte_range;
61
62 let mut s = Self {
63 ctx,
64 config,
65 client,
66 file_hash,
67 requested_byte_range,
68 last_block_info: None,
69 prefetched_byte_position: requested_byte_range.start,
70 current_active_byte_position: requested_byte_range.start,
71 prefetch_queue: VecDeque::new(),
72 known_final_byte_position: Arc::new(AtomicU64::new(requested_byte_range.end)),
73 completion_rate_estimator,
74 progress_updater,
75 total_bytes_reported: 0,
76 total_transfer_bytes_reported: 0,
77 };
78
79 let initial_fetch_size = s.config.min_reconstruction_fetch_size.as_u64();
84 s.prefetch_block(initial_fetch_size).await?;
85 s.prefetch_block(2 * initial_fetch_size).await?;
86
87 debug!(
88 %file_hash,
89 prefetch_queue_size = s.prefetch_queue.len(),
90 "Initial prefetch blocks queued"
91 );
92
93 Ok(s)
94 }
95
96 pub async fn next_file_terms(&mut self) -> Result<Option<Vec<FileTerm>>> {
99 if let Some((block_start_time, block_range)) = self.last_block_info.take() {
101 let completion_time = Instant::now().duration_since(block_start_time).as_secs_f64();
102 let block_size = block_range.end - block_range.start;
103
104 if block_size != 0 {
105 self.completion_rate_estimator
106 .update((block_size as f64) / completion_time.max(1e-6));
107 }
108
109 info!(
110 file_hash = %self.file_hash,
111 block_start = block_range.start,
112 block_end = block_range.end,
113 block_size = block_size,
114 completion_time = completion_time,
115 "Updated completion rate estimate based on previous block completion time (seconds)."
116 );
117 }
118
119 self.check_prefetch_buffer().await?;
121
122 let Some(next_block_jh) = self.prefetch_queue.pop_front() else {
123 debug_assert_ge!(self.prefetched_byte_position, self.known_final_byte_position.load(Ordering::Relaxed));
127 return Ok(None);
128 };
129
130 let maybe_next_block = next_block_jh
131 .await
132 .map_err(|e| FileReconstructionError::InternalError(format!("Join error: {e}")))??;
133
134 if let Some((file_terms, new_bytes, new_transfer_bytes)) = maybe_next_block {
135 let domain = file_terms
137 .first()
138 .and_then(|t| t.url_info.xorb_block_retrieval_urls.try_read().ok())
139 .and_then(|urls| {
140 urls.1
141 .first()
142 .and_then(|(url, _)| url::Url::parse(url).ok())
143 .and_then(|u| u.host_str().map(str::to_owned))
144 });
145
146 let block_start = file_terms.first().map(|t| t.byte_range.start).unwrap_or(0);
148 let block_end = file_terms.last().map(|t| t.byte_range.end).unwrap_or(0);
149
150 self.last_block_info = Some((Instant::now(), FileRange::new(block_start, block_end)));
152
153 self.current_active_byte_position = block_end;
155
156 info!(
157 file_hash = %self.file_hash,
158 domain = domain.as_deref().unwrap_or("unknown"),
159 block_start = block_start,
160 block_end = block_end,
161 block_size = file_terms.len(),
162 "Received block of file terms from prefetch queue"
163 );
164
165 if let Some(progress_updater) = &self.progress_updater {
166 self.total_bytes_reported = self.total_bytes_reported.saturating_add(new_bytes);
167 self.total_transfer_bytes_reported =
168 self.total_transfer_bytes_reported.saturating_add(new_transfer_bytes);
169 progress_updater.update_item_size(self.total_bytes_reported, false);
170 progress_updater.update_transfer_size(self.total_transfer_bytes_reported);
171 }
172
173 Ok(Some(file_terms))
174 } else {
175 self.known_final_byte_position
177 .store(self.prefetched_byte_position, Ordering::Relaxed);
178
179 if let Some(progress_updater) = &self.progress_updater {
180 progress_updater.update_item_size(self.total_bytes_reported, true);
181 }
182
183 info!(
184 file_hash = %self.file_hash,
185 prefetched_byte_position = self.prefetched_byte_position,
186 "Completed prefetch queue; end of file reached."
187 );
188
189 Ok(None)
190 }
191 }
192
193 fn is_done_fetching(&self) -> bool {
194 self.prefetched_byte_position >= self.known_final_byte_position.load(Ordering::Relaxed)
195 }
196
197 async fn check_prefetch_buffer(&mut self) -> Result<()> {
199 if self.is_done_fetching() {
201 return Ok(());
202 }
203
204 let target_completion_time = self.config.target_block_completion_time.as_secs_f64();
206
207 let completion_rate = self.completion_rate_estimator.value();
210
211 let prefetch_buffer_target_size = target_completion_time * completion_rate;
214
215 let min_prefetch_buffer_size = self.config.min_prefetch_buffer.as_u64() as f64;
217 let prefetch_buffer_size = prefetch_buffer_target_size.max(min_prefetch_buffer_size);
218
219 let current_prefetch_buffer_size = self.prefetched_byte_position - self.current_active_byte_position;
221
222 if !self.prefetch_queue.is_empty() && prefetch_buffer_size <= current_prefetch_buffer_size as f64 {
225 return Ok(());
226 }
227
228 let next_prefetch_target_block_size = (prefetch_buffer_size - current_prefetch_buffer_size as f64) as u64;
230
231 let min_fetch_size = self.config.min_reconstruction_fetch_size.as_u64();
232 let max_fetch_size = self.config.max_reconstruction_fetch_size.as_u64().max(min_fetch_size);
233 let next_prefetch_block_size = next_prefetch_target_block_size.clamp(min_fetch_size, max_fetch_size);
234
235 self.prefetch_block(next_prefetch_block_size).await
237 }
238
239 async fn prefetch_block(&mut self, block_size: u64) -> Result<()> {
240 let block_size = block_size.clamp(
241 self.config.min_reconstruction_fetch_size.as_u64(),
242 self.config.max_reconstruction_fetch_size.as_u64(),
243 );
244
245 let mut prefetch_block_range =
247 FileRange::new(self.prefetched_byte_position, self.prefetched_byte_position + block_size);
248
249 let last_byte_position = self
251 .known_final_byte_position
252 .load(Ordering::Relaxed)
253 .min(self.requested_byte_range.end);
254
255 if prefetch_block_range.end > last_byte_position {
257 prefetch_block_range.end = last_byte_position;
258 }
259
260 let min_fetch_size = self.config.min_reconstruction_fetch_size.as_u64();
262 if prefetch_block_range.end + min_fetch_size > self.requested_byte_range.end {
263 prefetch_block_range.end = self.requested_byte_range.end;
264 }
265
266 if prefetch_block_range.start >= prefetch_block_range.end {
269 debug!(
270 file_hash = %self.file_hash,
271 "Prefetch block skipped - already at or past end of requested range"
272 );
273 return Ok(());
274 }
275
276 let actual_block_size = prefetch_block_range.end - prefetch_block_range.start;
277 info!(
278 file_hash = %self.file_hash,
279 prefetch_range = ?(prefetch_block_range.start, prefetch_block_range.end),
280 requested_block_size = block_size,
281 actual_block_size,
282 queue_depth = self.prefetch_queue.len() + 1,
283 "Scheduling prefetch block"
284 );
285
286 self.prefetched_byte_position = prefetch_block_range.end;
288
289 let known_final_byte_position = self.known_final_byte_position.clone();
291 let client = self.client.clone();
292 let file_hash = self.file_hash;
293 let runtime = self.ctx.clone();
294
295 let jh = tokio::task::spawn(async move {
296 let result = retrieve_file_term_block(&runtime, client, file_hash, prefetch_block_range).await;
297
298 if let Ok(Some((ref returned_range, transfer_bytes, ref file_terms))) = result {
300 debug_assert_eq!(returned_range.start, prefetch_block_range.start);
303
304 if returned_range.end < prefetch_block_range.end {
305 known_final_byte_position.store(returned_range.end, Ordering::Relaxed);
306 }
307
308 let new_bytes = returned_range.end.saturating_sub(returned_range.start);
309 Ok(Some((file_terms.clone(), new_bytes, transfer_bytes)))
310 } else if let Ok(None) = result {
311 known_final_byte_position.fetch_min(prefetch_block_range.start, Ordering::Relaxed);
315 Ok(None)
316 } else {
317 result.map(|r| {
318 r.map(|(returned_range, transfer_bytes, file_terms)| {
319 let new_bytes = returned_range.end.saturating_sub(returned_range.start);
320 (file_terms, new_bytes, transfer_bytes)
321 })
322 })
323 }
324 });
325
326 self.prefetch_queue.push_back(jh);
327
328 Ok(())
329 }
330}