1#[cfg(not(target_family = "wasm"))]
2use std::fs::OpenOptions;
3use std::io::Write;
4#[cfg(not(target_family = "wasm"))]
5use std::io::{Seek, SeekFrom};
6#[cfg(not(target_family = "wasm"))]
7use std::path::Path;
8use std::sync::Arc;
9use std::sync::atomic::Ordering;
10
11use tokio_util::sync::CancellationToken;
12use tracing::{debug, info};
13use xet_client::cas_client::Client;
14use xet_client::cas_types::FileRange;
15use xet_client::chunk_cache::ChunkCache;
16use xet_core_structures::merklehash::MerkleHash;
17use xet_runtime::config::ReconstructionConfig;
18use xet_runtime::core::XetContext;
19use xet_runtime::utils::ClosureGuard;
20use xet_runtime::utils::adjustable_semaphore::AdjustableSemaphore;
21
22use super::data_writer::{DataWriter, DownloadStream, SequentialWriter, UnorderedDownloadStream};
23use super::error::{FileReconstructionError, Result};
24use super::reconstruction_terms::ReconstructionTermManager;
25use super::run_state::{RunError, RunState};
26use crate::progress_tracking::ItemProgressUpdater;
27
28pub struct FileReconstructor {
32 ctx: XetContext,
33 client: Arc<dyn Client>,
34 file_hash: MerkleHash,
35 byte_range: Option<FileRange>,
36 progress_updater: Option<Arc<ItemProgressUpdater>>,
37 config: Arc<ReconstructionConfig>,
38
39 chunk_cache: Option<Arc<dyn ChunkCache>>,
41
42 custom_buffer_semaphore: Option<Arc<AdjustableSemaphore>>,
44
45 cancellation_token: CancellationToken,
49}
50
51impl FileReconstructor {
52 pub fn new(ctx: &XetContext, client: &Arc<dyn Client>, file_hash: MerkleHash) -> Self {
53 Self {
54 ctx: ctx.clone(),
55 client: client.clone(),
56 file_hash,
57 byte_range: None,
58 progress_updater: default_progress_updater(),
59 config: Arc::new(ctx.config.reconstruction.clone()),
60 chunk_cache: None,
61 custom_buffer_semaphore: None,
62 cancellation_token: CancellationToken::new(),
63 }
64 }
65
66 pub fn with_byte_range(self, byte_range: FileRange) -> Self {
67 Self {
68 byte_range: Some(byte_range),
69 ..self
70 }
71 }
72
73 pub fn with_progress_updater(self, progress_updater: Arc<ItemProgressUpdater>) -> Self {
74 Self {
75 progress_updater: Some(progress_updater),
76 ..self
77 }
78 }
79
80 pub fn with_chunk_cache(self, cache: Arc<dyn ChunkCache>) -> Self {
81 Self {
82 chunk_cache: Some(cache),
83 ..self
84 }
85 }
86
87 pub fn with_config(self, config: impl AsRef<ReconstructionConfig>) -> Self {
88 Self {
89 config: Arc::new(config.as_ref().clone()),
90 ..self
91 }
92 }
93
94 pub fn with_buffer_semaphore(self, semaphore: Arc<AdjustableSemaphore>) -> Self {
98 Self {
99 custom_buffer_semaphore: Some(semaphore),
100 ..self
101 }
102 }
103
104 pub fn with_cancellation_token(self, token: CancellationToken) -> Self {
108 Self {
109 cancellation_token: token,
110 ..self
111 }
112 }
113
114 #[cfg(not(target_family = "wasm"))]
125 pub async fn reconstruct_to_file(self, path: &Path, write_offset: Option<u64>, truncate_file: bool) -> Result<u64> {
126 info!(
127 file_hash = %self.file_hash,
128 byte_range = ?self.byte_range,
129 path = %path.display(),
130 write_offset = ?write_offset,
131 "Reconstructing file to disk"
132 );
133
134 if let Some(parent) = path.parent() {
135 std::fs::create_dir_all(parent)?;
136 }
137
138 let mut file = OpenOptions::new().write(true).create(true).truncate(truncate_file).open(path)?;
139
140 let default_write_position = self.byte_range.map_or(0, |r| r.start);
141 let seek_position = write_offset.unwrap_or(default_write_position);
142 if seek_position > 0 {
143 file.seek(SeekFrom::Start(seek_position))?;
144 }
145
146 let run_state = RunState::new(self.cancellation_token.clone(), self.file_hash, self.progress_updater.clone());
147
148 let data_writer = SequentialWriter::new(&self.ctx, file, self.config.use_vectored_write, run_state.clone());
149
150 self.run(data_writer, run_state, false).await
151 }
152
153 #[cfg(not(target_family = "wasm"))]
158 pub async fn reconstruct_to_writer<W: Write + Send + 'static>(self, writer: W) -> Result<u64> {
159 info!(
160 file_hash = %self.file_hash,
161 byte_range = ?self.byte_range,
162 "Reconstructing file to writer"
163 );
164
165 let run_state = RunState::new(self.cancellation_token.clone(), self.file_hash, self.progress_updater.clone());
166 let data_writer = SequentialWriter::new(&self.ctx, writer, self.config.use_vectored_write, run_state.clone());
167 self.run(data_writer, run_state, false).await
168 }
169
170 #[cfg(target_family = "wasm")]
179 pub async fn reconstruct_to_writer<W: Write + Send + 'static>(self, mut writer: W) -> Result<u64> {
180 info!(
181 file_hash = %self.file_hash,
182 byte_range = ?self.byte_range,
183 "Reconstructing file to writer"
184 );
185
186 let mut stream = self.reconstruct_to_stream();
187 let mut total: u64 = 0;
188 while let Some(chunk) = stream.next().await? {
189 writer
190 .write_all(&chunk)
191 .map_err(|e| FileReconstructionError::IoError(Arc::new(e)))?;
192 total += chunk.len() as u64;
193 }
194 writer.flush().map_err(|e| FileReconstructionError::IoError(Arc::new(e)))?;
195 Ok(total)
196 }
197
198 pub fn reconstruct_to_stream(self) -> DownloadStream {
211 let run_state = RunState::new(self.cancellation_token.clone(), self.file_hash, self.progress_updater.clone());
212
213 DownloadStream::new(self, run_state)
214 }
215
216 pub fn reconstruct_to_unordered_stream(self) -> UnorderedDownloadStream {
231 let run_state = RunState::new(self.cancellation_token.clone(), self.file_hash, self.progress_updater.clone());
232
233 UnorderedDownloadStream::new(self, run_state)
234 }
235
236 pub(crate) async fn run(
243 self,
244 data_writer: Box<dyn DataWriter>,
245 run_state: Arc<RunState>,
246 is_streaming: bool,
247 ) -> Result<u64> {
248 match self.run_impl(data_writer, &run_state, is_streaming).await {
249 Ok(v) => Ok(v),
250 Err(RunError::Cancelled) => {
251 run_state.check_error()?;
252 Ok(0)
253 },
254 Err(RunError::Error(e)) => {
255 run_state.set_error(e.clone());
256 Err(e)
257 },
258 }
259 }
260
261 async fn run_impl(
262 self,
263 mut data_writer: Box<dyn DataWriter>,
264 run_state: &RunState,
265 _is_streaming: bool,
266 ) -> std::result::Result<u64, RunError> {
267 let Self {
268 ctx,
269 client,
270 byte_range,
271 config,
272 chunk_cache,
273 custom_buffer_semaphore,
274 ..
275 } = self;
276
277 run_state.check_run_state()?;
278
279 let file_hash = *run_state.file_hash();
280 let requested_range = byte_range.unwrap_or_else(FileRange::full);
281
282 let mut term_manager = ReconstructionTermManager::new(
283 ctx.clone(),
284 config.clone(),
285 client.clone(),
286 file_hash,
287 requested_range,
288 run_state.progress_updater().cloned(),
289 )
290 .await?;
291
292 let using_global_memory_limit = custom_buffer_semaphore.is_none();
293 let download_buffer_semaphore =
294 custom_buffer_semaphore.unwrap_or_else(|| ctx.common.reconstruction_download_buffer.clone());
295
296 let mut seed_buffer_permit;
302 let _download_count_decrement_guard;
303
304 if using_global_memory_limit {
305 let active_downloads = ctx.common.active_downloads.clone();
306 let n = active_downloads.fetch_add(1, Ordering::Relaxed) + 1;
307
308 let base = config.download_buffer_size.as_u64();
309 let perfile = config.download_buffer_perfile_size.as_u64();
310 let limit = config.download_buffer_limit.as_u64();
311
312 let target = base.saturating_add(n.saturating_mul(perfile)).min(limit);
313 seed_buffer_permit = download_buffer_semaphore.increment_permits_to_target(target);
314
315 let buffer_sem = download_buffer_semaphore.clone();
316 _download_count_decrement_guard = Some(ClosureGuard::new(move || {
317 let n = active_downloads.fetch_sub(1, Ordering::Relaxed).saturating_sub(1);
318 let target = base.saturating_add(n.saturating_mul(perfile)).min(limit);
319 buffer_sem.decrement_permits_to_target(target);
320 }));
321 } else {
322 seed_buffer_permit = None;
323 _download_count_decrement_guard = None;
324 }
325
326 let range_start_offset = requested_range.start;
328
329 loop {
333 let maybe_file_terms = tokio::select! {
334 biased;
335 _ = run_state.cancelled() => {
336 return run_state.check_run_state().map(|_| 0);
337 }
338 result = term_manager.next_file_terms() => result?
339 };
340
341 let Some(file_terms) = maybe_file_terms else {
342 break;
343 };
344
345 run_state.check_run_state()?;
346
347 run_state.record_new_block();
348
349 for file_term in file_terms {
351 run_state.check_run_state()?;
352
353 let term_size = file_term.byte_range.end - file_term.byte_range.start;
354
355 debug!(
356 file_hash = %file_hash,
357 xorb_hash = %file_term.xorb_block.xorb_hash,
358 term_byte_range = ?(file_term.byte_range.start, file_term.byte_range.end),
359 term_size,
360 "Processing file term"
361 );
362
363 let buffer_permit = match seed_buffer_permit.as_mut().and_then(|rp| rp.split(term_size)) {
367 Some(split) => split,
368 None => {
369 seed_buffer_permit = None;
370
371 tokio::select! {
373 biased;
374 _ = run_state.cancelled() => {
375 return run_state.check_run_state().map(|_| 0);
376 }
377 result = download_buffer_semaphore.acquire_many(term_size) => {
378 result.map_err(|e| {
379 FileReconstructionError::InternalError(format!(
380 "Error acquiring download buffer permit: {e}"
381 ))
382 })?
383 }
384 }
385 },
386 };
387
388 let data_future = file_term
389 .get_data_task(
390 ctx.clone(),
391 client.clone(),
392 run_state.progress_updater().cloned(),
393 chunk_cache.clone(),
394 )
395 .await?;
396
397 #[cfg(debug_assertions)]
398 {
399 let refs = &file_term.xorb_block.references;
400 assert!(refs.iter().any(|r| r.term_chunks == file_term.xorb_chunk_range));
401 }
402
403 let relative_byte_range = FileRange::new(
405 file_term.byte_range.start - range_start_offset,
406 file_term.byte_range.end - range_start_offset,
407 );
408
409 data_writer
410 .set_next_term_data_source(relative_byte_range, Some(buffer_permit), data_future)
411 .await?;
412
413 run_state.record_new_term(term_size);
414 }
415 }
416
417 run_state.log_progress("All term blocks received and scheduled for writing");
418
419 let bytes_written = data_writer.finish().await?;
421 let total_bytes_scheduled = run_state.total_bytes_scheduled();
422
423 debug_assert_eq!(
424 bytes_written, total_bytes_scheduled,
425 "Bytes written ({bytes_written}) should match total bytes scheduled ({total_bytes_scheduled})"
426 );
427
428 run_state.log_progress("File reconstruction completed successfully");
429
430 #[cfg(debug_assertions)]
431 if !_is_streaming && let Some(updater) = run_state.progress_updater() {
432 updater.assert_complete();
433 if let Some(byte_range) = byte_range
434 && byte_range.end < u64::MAX
435 {
436 assert_eq!(updater.total_bytes_completed(), byte_range.end - byte_range.start);
437 }
438 }
439
440 Ok(total_bytes_scheduled)
441 }
442}
443
444#[cfg(test)]
445fn default_progress_updater() -> Option<Arc<ItemProgressUpdater>> {
446 Some(ItemProgressUpdater::new_standalone("test"))
447}
448
449#[cfg(not(test))]
450fn default_progress_updater() -> Option<Arc<ItemProgressUpdater>> {
451 None
452}
453
454#[cfg(test)]
455mod tests {
456 use std::io::{Cursor, Write};
457 use std::sync::Arc;
458 use std::sync::atomic::{AtomicUsize, Ordering};
459 use std::time::Duration;
460
461 use tokio::runtime::Handle;
462 use xet_client::cas_client::{ClientTestingUtils, DirectAccessClient, LocalClient, RandomFileContents};
463 use xet_client::cas_types::FileRange;
464 use xet_runtime::config::XetConfig;
465 use xet_runtime::core::XetContext;
466
467 use super::*;
468 use crate::progress_tracking::ItemProgressUpdater;
469
470 const TEST_CHUNK_SIZE: usize = 101;
471
472 fn test_config() -> ReconstructionConfig {
474 let mut config = ReconstructionConfig::default();
475 config.min_reconstruction_fetch_size = xet_runtime::utils::ByteSize::from("100");
477 config.max_reconstruction_fetch_size = xet_runtime::utils::ByteSize::from("400");
478 config.min_prefetch_buffer = xet_runtime::utils::ByteSize::from("800");
479 config
480 }
481
482 async fn setup_test_file(term_spec: &[(u64, (u64, u64))]) -> (Arc<LocalClient>, RandomFileContents) {
484 let client = LocalClient::temporary(XetContext::default().unwrap()).await.unwrap();
485 let file_contents = client.upload_random_file(term_spec, TEST_CHUNK_SIZE).await.unwrap();
486 (client, file_contents)
487 }
488
489 async fn reconstruct_to_vec(
491 client: &Arc<LocalClient>,
492 file_hash: MerkleHash,
493 byte_range: Option<FileRange>,
494 config: &ReconstructionConfig,
495 semaphore: Option<Arc<AdjustableSemaphore>>,
496 ) -> Result<Vec<u8>> {
497 let buffer = Arc::new(std::sync::Mutex::new(Cursor::new(Vec::new())));
498 let writer = StaticCursorWriter(buffer.clone());
499
500 let mut reconstructor =
501 FileReconstructor::new(&XetContext::default().unwrap(), &(client.clone() as Arc<dyn Client>), file_hash)
502 .with_config(config);
503
504 if let Some(range) = byte_range {
505 reconstructor = reconstructor.with_byte_range(range);
506 }
507 if let Some(sem) = semaphore {
508 reconstructor = reconstructor.with_buffer_semaphore(sem);
509 }
510
511 reconstructor.reconstruct_to_writer(writer).await?;
512
513 let data = buffer.lock().unwrap().get_ref().clone();
514 Ok(data)
515 }
516
517 async fn reconstruct_to_file(
520 client: &Arc<LocalClient>,
521 file_hash: MerkleHash,
522 byte_range: Option<FileRange>,
523 config: &ReconstructionConfig,
524 ) -> Result<Vec<u8>> {
525 let temp_dir = tempfile::tempdir().unwrap();
526 let file_path = temp_dir.path().join("output.bin");
527
528 let mut reconstructor =
529 FileReconstructor::new(&XetContext::default().unwrap(), &(client.clone() as Arc<dyn Client>), file_hash)
530 .with_config(config);
531
532 if let Some(range) = byte_range {
533 reconstructor = reconstructor.with_byte_range(range);
534 }
535
536 reconstructor.reconstruct_to_file(&file_path, None, false).await?;
537
538 let file_data = std::fs::read(&file_path)?;
540 let start = byte_range.map(|r| r.start as usize).unwrap_or(0);
541 Ok(file_data[start..].to_vec())
542 }
543
544 async fn reconstruct_to_file_at_specific_offset(
546 client: &Arc<LocalClient>,
547 file_hash: MerkleHash,
548 byte_range: Option<FileRange>,
549 config: &ReconstructionConfig,
550 ) -> Result<Vec<u8>> {
551 let offset = 9u64;
552
553 let temp_dir = tempfile::tempdir().unwrap();
554 let file_path = temp_dir.path().join("output.bin");
555
556 let mut reconstructor =
557 FileReconstructor::new(&XetContext::default().unwrap(), &(client.clone() as Arc<dyn Client>), file_hash)
558 .with_config(config);
559
560 if let Some(range) = byte_range {
561 reconstructor = reconstructor.with_byte_range(range);
562 }
563
564 reconstructor.reconstruct_to_file(&file_path, Some(offset), false).await?;
565
566 let file_data = std::fs::read(&file_path)?;
568 Ok(file_data[offset as usize..].to_vec())
569 }
570
571 async fn reconstruct_to_file_at_offset_zero(
574 client: &Arc<LocalClient>,
575 file_hash: MerkleHash,
576 byte_range: Option<FileRange>,
577 config: &ReconstructionConfig,
578 ) -> Result<Vec<u8>> {
579 let temp_dir = tempfile::tempdir().unwrap();
580 let file_path = temp_dir.path().join("output.bin");
581
582 let mut reconstructor =
583 FileReconstructor::new(&XetContext::default().unwrap(), &(client.clone() as Arc<dyn Client>), file_hash)
584 .with_config(config);
585
586 if let Some(range) = byte_range {
587 reconstructor = reconstructor.with_byte_range(range);
588 }
589
590 reconstructor.reconstruct_to_file(&file_path, Some(0), false).await?;
591
592 let file_data = std::fs::read(&file_path)?;
594 Ok(file_data)
595 }
596
597 struct StaticCursorWriter(Arc<std::sync::Mutex<Cursor<Vec<u8>>>>);
600
601 impl std::io::Write for StaticCursorWriter {
602 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
603 self.0.lock().unwrap().write(buf)
604 }
605
606 fn flush(&mut self) -> std::io::Result<()> {
607 self.0.lock().unwrap().flush()
608 }
609 }
610
611 async fn reconstruct_and_verify_full(
613 client: &Arc<LocalClient>,
614 file_contents: &RandomFileContents,
615 base_config: ReconstructionConfig,
616 ) {
617 let expected = &file_contents.data;
618 let h = file_contents.file_hash;
619
620 for use_vectored in [false, true] {
622 let mut config = base_config.clone();
623 config.use_vectored_write = use_vectored;
624
625 let vec_result = reconstruct_to_vec(&client, h, None, &config, None).await.unwrap();
627 assert_eq!(vec_result, *expected, "vec failed (vectored={use_vectored})");
628
629 let file_result = reconstruct_to_file(client, h, None, &config).await.unwrap();
631 assert_eq!(file_result, *expected, "file failed (vectored={use_vectored})");
632
633 let file_offset_result = reconstruct_to_file_at_offset_zero(client, h, None, &config).await.unwrap();
635 assert_eq!(file_offset_result, *expected, "file_at_offset_zero failed (vectored={use_vectored})");
636
637 let file_specific_result = reconstruct_to_file_at_specific_offset(client, h, None, &config).await.unwrap();
639 assert_eq!(file_specific_result, *expected, "file_at_specific_offset failed (vectored={use_vectored})");
640 }
641 }
642
643 async fn reconstruct_and_verify_range(
645 client: &Arc<LocalClient>,
646 file_contents: &RandomFileContents,
647 range: FileRange,
648 base_config: ReconstructionConfig,
649 ) {
650 let expected = &file_contents.data[range.start as usize..range.end as usize];
651
652 for use_vectored in [false, true] {
654 let mut config = base_config.clone();
655 config.use_vectored_write = use_vectored;
656
657 let vec_result = reconstruct_to_vec(&client, file_contents.file_hash, Some(range), &config, None)
659 .await
660 .expect("reconstruct_to_vec should succeed");
661 assert_eq!(vec_result, expected, "vec failed (vectored={use_vectored})");
662
663 let file_result = reconstruct_to_file(client, file_contents.file_hash, Some(range), &config)
665 .await
666 .expect("reconstruct_to_file should succeed");
667 assert_eq!(file_result, expected, "file failed (vectored={use_vectored})");
668
669 let file_offset_result =
671 reconstruct_to_file_at_offset_zero(client, file_contents.file_hash, Some(range), &config)
672 .await
673 .expect("reconstruct_to_file_at_offset_zero should succeed");
674 assert_eq!(file_offset_result, expected, "file_at_offset failed (vectored={use_vectored})");
675 }
676 }
677
678 #[tokio::test]
681 async fn test_single_term_full_reconstruction() {
682 let (client, file_contents) = setup_test_file(&[(1, (0, 3))]).await;
683 reconstruct_and_verify_full(&client, &file_contents, test_config()).await;
684 }
685
686 #[tokio::test]
687 async fn test_multiple_terms_same_xorb_full_reconstruction() {
688 let (client, file_contents) = setup_test_file(&[(1, (0, 2)), (1, (2, 4)), (1, (4, 6))]).await;
689 reconstruct_and_verify_full(&client, &file_contents, test_config()).await;
690 }
691
692 #[tokio::test]
693 async fn test_multiple_xorbs_full_reconstruction() {
694 let (client, file_contents) = setup_test_file(&[(1, (0, 3)), (2, (0, 2)), (3, (0, 4))]).await;
695 reconstruct_and_verify_full(&client, &file_contents, test_config()).await;
696 }
697
698 #[tokio::test]
699 async fn test_large_file_many_terms_full_reconstruction() {
700 let term_spec: Vec<(u64, (u64, u64))> = (1..=10).map(|i| (i, (0, 5))).collect();
702 let (client, file_contents) = setup_test_file(&term_spec).await;
703 reconstruct_and_verify_full(&client, &file_contents, test_config()).await;
704 }
705
706 #[tokio::test]
707 async fn test_interleaved_xorbs_full_reconstruction() {
708 let (client, file_contents) = setup_test_file(&[(1, (0, 2)), (2, (0, 2)), (1, (2, 4)), (2, (2, 4))]).await;
709 reconstruct_and_verify_full(&client, &file_contents, test_config()).await;
710 }
711
712 #[tokio::test]
713 async fn test_single_chunk_file() {
714 let (client, file_contents) = setup_test_file(&[(1, (0, 1))]).await;
715 reconstruct_and_verify_full(&client, &file_contents, test_config()).await;
716 }
717
718 #[tokio::test]
719 async fn test_many_small_terms_different_xorbs() {
720 let term_spec: Vec<(u64, (u64, u64))> = (1..=20).map(|i| (i, (0, 1))).collect();
721 let (client, file_contents) = setup_test_file(&term_spec).await;
722 reconstruct_and_verify_full(&client, &file_contents, test_config()).await;
723 }
724
725 #[tokio::test]
728 async fn test_progress_tracker_records_full_reconstruction_bytes() {
729 let (client, file_contents) = setup_test_file(&[(1, (0, 3)), (2, (0, 2))]).await;
730 let config = test_config();
731 let buffer = Arc::new(std::sync::Mutex::new(Cursor::new(Vec::new())));
732 let writer = StaticCursorWriter(buffer.clone());
733
734 let progress_updater = ItemProgressUpdater::new_standalone("file");
735 let bytes_written = FileReconstructor::new(
736 &XetContext::default().unwrap(),
737 &(client.clone() as Arc<dyn Client>),
738 file_contents.file_hash,
739 )
740 .with_config(&config)
741 .with_progress_updater(progress_updater.clone())
742 .reconstruct_to_writer(writer)
743 .await
744 .unwrap();
745
746 assert_eq!(bytes_written, file_contents.data.len() as u64);
747 }
748
749 #[tokio::test]
750 async fn test_progress_tracker_records_partial_range_bytes() {
751 let (client, file_contents) = setup_test_file(&[(1, (0, 10))]).await;
752 let config = test_config();
753 let file_len = file_contents.data.len() as u64;
754 let range = FileRange::new(file_len / 4, file_len * 3 / 4);
755 let expected_bytes = range.end - range.start;
756
757 let buffer = Arc::new(std::sync::Mutex::new(Cursor::new(Vec::new())));
758 let writer = StaticCursorWriter(buffer.clone());
759
760 let progress_updater = ItemProgressUpdater::new_standalone("file");
761 let bytes_written = FileReconstructor::new(
762 &XetContext::default().unwrap(),
763 &(client.clone() as Arc<dyn Client>),
764 file_contents.file_hash,
765 )
766 .with_config(&config)
767 .with_byte_range(range)
768 .with_progress_updater(progress_updater.clone())
769 .reconstruct_to_writer(writer)
770 .await
771 .unwrap();
772
773 assert_eq!(bytes_written, expected_bytes);
774 }
775
776 #[tokio::test]
779 async fn test_external_progress_tracker_incremental_discovery() {
780 let term_spec: Vec<(u64, (u64, u64))> = (1..=5).map(|i| (i, (0, 3))).collect();
781 let (client, file_contents) = setup_test_file(&term_spec).await;
782 let config = test_config();
783
784 let task = ItemProgressUpdater::new_standalone("test_file.bin");
785
786 let buffer = Arc::new(std::sync::Mutex::new(Cursor::new(Vec::new())));
787 let writer = StaticCursorWriter(buffer.clone());
788
789 let bytes_written = FileReconstructor::new(
790 &XetContext::default().unwrap(),
791 &(client.clone() as Arc<dyn Client>),
792 file_contents.file_hash,
793 )
794 .with_config(&config)
795 .with_progress_updater(task.clone())
796 .reconstruct_to_writer(writer)
797 .await
798 .unwrap();
799
800 assert_eq!(bytes_written, file_contents.data.len() as u64);
801
802 task.assert_complete();
803 assert_eq!(task.total_bytes_completed(), file_contents.data.len() as u64);
804 }
805
806 #[tokio::test]
810 async fn test_external_progress_tracker_final_size_upfront() {
811 let term_spec: Vec<(u64, (u64, u64))> = (1..=5).map(|i| (i, (0, 3))).collect();
812 let (client, file_contents) = setup_test_file(&term_spec).await;
813 let config = test_config();
814 let file_size = file_contents.data.len() as u64;
815
816 let task = ItemProgressUpdater::new_standalone("test_file.bin");
817
818 task.update_item_size(file_size, true);
819
820 let buffer = Arc::new(std::sync::Mutex::new(Cursor::new(Vec::new())));
821 let writer = StaticCursorWriter(buffer.clone());
822
823 let bytes_written = FileReconstructor::new(
824 &XetContext::default().unwrap(),
825 &(client.clone() as Arc<dyn Client>),
826 file_contents.file_hash,
827 )
828 .with_config(&config)
829 .with_progress_updater(task.clone())
830 .reconstruct_to_writer(writer)
831 .await
832 .unwrap();
833
834 assert_eq!(bytes_written, file_size);
835
836 assert_eq!(task.total_bytes_completed(), file_size);
837
838 task.assert_complete();
839 }
840
841 #[tokio::test]
844 async fn test_range_first_half() {
845 let (client, file_contents) = setup_test_file(&[(1, (0, 10))]).await;
846 let file_len = file_contents.data.len() as u64;
847 let range = FileRange::new(0, file_len / 2);
848 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
849 }
850
851 #[tokio::test]
852 async fn test_range_second_half() {
853 let (client, file_contents) = setup_test_file(&[(1, (0, 10))]).await;
854 let file_len = file_contents.data.len() as u64;
855 let range = FileRange::new(file_len / 2, file_len);
856 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
857 }
858
859 #[tokio::test]
860 async fn test_range_middle() {
861 let (client, file_contents) = setup_test_file(&[(1, (0, 10))]).await;
862 let file_len = file_contents.data.len() as u64;
863 let range = FileRange::new(file_len / 4, file_len * 3 / 4);
864 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
865 }
866
867 #[tokio::test]
868 async fn test_range_single_byte_start() {
869 let (client, file_contents) = setup_test_file(&[(1, (0, 5))]).await;
870 let range = FileRange::new(0, 1);
871 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
872 }
873
874 #[tokio::test]
875 async fn test_range_single_byte_end() {
876 let (client, file_contents) = setup_test_file(&[(1, (0, 5))]).await;
877 let file_len = file_contents.data.len() as u64;
878 let range = FileRange::new(file_len - 1, file_len);
879 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
880 }
881
882 #[tokio::test]
883 async fn test_range_single_byte_middle() {
884 let (client, file_contents) = setup_test_file(&[(1, (0, 5))]).await;
885 let file_len = file_contents.data.len() as u64;
886 let mid = file_len / 2;
887 let range = FileRange::new(mid, mid + 1);
888 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
889 }
890
891 #[tokio::test]
892 async fn test_range_few_bytes_from_start() {
893 let (client, file_contents) = setup_test_file(&[(1, (0, 5))]).await;
894 let file_len = file_contents.data.len() as u64;
895 let range = FileRange::new(3, file_len);
896 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
897 }
898
899 #[tokio::test]
900 async fn test_range_few_bytes_before_end() {
901 let (client, file_contents) = setup_test_file(&[(1, (0, 5))]).await;
902 let file_len = file_contents.data.len() as u64;
903 let range = FileRange::new(0, file_len - 3);
904 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
905 }
906
907 #[tokio::test]
908 async fn test_range_small_slice_in_middle() {
909 let (client, file_contents) = setup_test_file(&[(1, (0, 10))]).await;
910 let file_len = file_contents.data.len() as u64;
911 let range = FileRange::new(file_len / 3, file_len / 3 + 10);
912 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
913 }
914
915 #[tokio::test]
918 async fn test_range_spanning_multiple_terms() {
919 let (client, file_contents) = setup_test_file(&[(1, (0, 3)), (2, (0, 3)), (3, (0, 3))]).await;
920 let file_len = file_contents.data.len() as u64;
921 let range = FileRange::new(10, file_len - 10);
923 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
924 }
925
926 #[tokio::test]
927 async fn test_range_within_single_term() {
928 let (client, file_contents) = setup_test_file(&[(1, (0, 10)), (2, (0, 10))]).await;
929 let first_term_size = file_contents.terms[0].data.len() as u64;
931 let range = FileRange::new(5, first_term_size - 5);
933 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
934 }
935
936 #[tokio::test]
937 async fn test_range_crossing_term_boundary() {
938 let (client, file_contents) = setup_test_file(&[(1, (0, 5)), (2, (0, 5))]).await;
939 let first_term_size = file_contents.terms[0].data.len() as u64;
940 let range = FileRange::new(first_term_size - 10, first_term_size + 10);
942 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
943 }
944
945 #[tokio::test]
948 async fn test_large_file_range_first_portion() {
949 let term_spec: Vec<(u64, (u64, u64))> = (1..=15).map(|i| (i, (0, 4))).collect();
951 let (client, file_contents) = setup_test_file(&term_spec).await;
952 let file_len = file_contents.data.len() as u64;
953 let range = FileRange::new(0, file_len / 3);
954 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
955 }
956
957 #[tokio::test]
958 async fn test_large_file_range_last_portion() {
959 let term_spec: Vec<(u64, (u64, u64))> = (1..=15).map(|i| (i, (0, 4))).collect();
960 let (client, file_contents) = setup_test_file(&term_spec).await;
961 let file_len = file_contents.data.len() as u64;
962 let range = FileRange::new(file_len * 2 / 3, file_len);
963 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
964 }
965
966 #[tokio::test]
967 async fn test_large_file_range_middle_portion() {
968 let term_spec: Vec<(u64, (u64, u64))> = (1..=15).map(|i| (i, (0, 4))).collect();
969 let (client, file_contents) = setup_test_file(&term_spec).await;
970 let file_len = file_contents.data.len() as u64;
971 let range = FileRange::new(file_len / 3, file_len * 2 / 3);
972 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
973 }
974
975 #[tokio::test]
978 async fn test_complex_mixed_pattern_full() {
979 let term_spec = &[
980 (1, (0, 3)),
981 (2, (0, 2)),
982 (1, (3, 5)),
983 (3, (1, 4)),
984 (2, (4, 6)),
985 (1, (0, 2)),
986 ];
987 let (client, file_contents) = setup_test_file(term_spec).await;
988 reconstruct_and_verify_full(&client, &file_contents, test_config()).await;
989 }
990
991 #[tokio::test]
992 async fn test_complex_mixed_pattern_partial_range() {
993 let term_spec = &[
994 (1, (0, 3)),
995 (2, (0, 2)),
996 (1, (3, 5)),
997 (3, (1, 4)),
998 (2, (4, 6)),
999 (1, (0, 2)),
1000 ];
1001 let (client, file_contents) = setup_test_file(term_spec).await;
1002 let file_len = file_contents.data.len() as u64;
1003 let range = FileRange::new(file_len / 4, file_len * 3 / 4);
1004 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
1005 }
1006
1007 #[tokio::test]
1008 async fn test_overlapping_chunk_ranges() {
1009 let (client, file_contents) = setup_test_file(&[(1, (0, 5)), (1, (1, 3)), (1, (2, 4))]).await;
1010 reconstruct_and_verify_full(&client, &file_contents, test_config()).await;
1011 }
1012
1013 #[tokio::test]
1014 async fn test_non_contiguous_chunks() {
1015 let (client, file_contents) = setup_test_file(&[(1, (0, 2)), (1, (4, 6))]).await;
1016 let config = test_config();
1017 let result = reconstruct_to_vec(&client, file_contents.file_hash, None, &config, None)
1018 .await
1019 .unwrap();
1020 assert_eq!(result, file_contents.data);
1021 }
1022
1023 #[tokio::test]
1026 async fn test_default_config_full_reconstruction() {
1027 let (client, file_contents) = setup_test_file(&[(1, (0, 5)), (2, (0, 3))]).await;
1028 reconstruct_and_verify_full(&client, &file_contents, ReconstructionConfig::default()).await;
1030 }
1031
1032 #[tokio::test]
1033 async fn test_default_config_partial_range() {
1034 let (client, file_contents) = setup_test_file(&[(1, (0, 5)), (2, (0, 3))]).await;
1035 let file_len = file_contents.data.len() as u64;
1036 let range = FileRange::new(file_len / 4, file_len * 3 / 4);
1037 reconstruct_and_verify_range(&client, &file_contents, range, ReconstructionConfig::default()).await;
1038 }
1039
1040 struct TimeAdvancingWriter {
1048 buffer: Arc<std::sync::Mutex<Vec<u8>>>,
1049 advance_duration: Duration,
1050 write_count: Arc<AtomicUsize>,
1051 }
1052
1053 impl TimeAdvancingWriter {
1054 fn new(advance_duration: Duration) -> Self {
1055 Self {
1056 buffer: Arc::new(std::sync::Mutex::new(Vec::new())),
1057 advance_duration,
1058 write_count: Arc::new(AtomicUsize::new(0)),
1059 }
1060 }
1061 }
1062
1063 impl Write for TimeAdvancingWriter {
1064 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1065 let bytes_written = self.buffer.lock().unwrap().write(buf)?;
1066
1067 self.write_count.fetch_add(1, Ordering::Relaxed);
1069
1070 let advance_duration = self.advance_duration;
1074 tokio::runtime::Handle::current().block_on(async {
1075 tokio::time::advance(advance_duration).await;
1076 });
1077
1078 Ok(bytes_written)
1079 }
1080
1081 fn flush(&mut self) -> std::io::Result<()> {
1082 Ok(())
1083 }
1084 }
1085
1086 fn url_refresh_test_config() -> ReconstructionConfig {
1088 let mut config = ReconstructionConfig::default();
1089 config.min_reconstruction_fetch_size = xet_runtime::utils::ByteSize::from("50");
1091 config.max_reconstruction_fetch_size = xet_runtime::utils::ByteSize::from("100");
1092 config.min_prefetch_buffer = xet_runtime::utils::ByteSize::from("50");
1093 config
1094 }
1095
1096 #[tokio::test(start_paused = true)]
1100 async fn test_url_refresh_on_expiration() {
1101 let term_spec = &[(1, (0, 2)), (2, (0, 2)), (3, (0, 2))];
1103 let (client, file_contents) = setup_test_file(term_spec).await;
1104
1105 let url_expiration = Duration::from_secs(1);
1107 client.set_fetch_term_url_expiration(url_expiration);
1108
1109 let time_advance = Duration::from_secs(2);
1111 let writer = TimeAdvancingWriter::new(time_advance);
1112 let writer_buffer = writer.buffer.clone();
1113 let write_count = writer.write_count.clone();
1114
1115 let tiny_semaphore = AdjustableSemaphore::new(1, (1, 1));
1118 let ctx = XetContext::from_external(Handle::current(), XetConfig::new());
1119
1120 FileReconstructor::new(&ctx, &(client.clone() as Arc<dyn Client>), file_contents.file_hash)
1121 .with_config(url_refresh_test_config())
1122 .with_buffer_semaphore(tiny_semaphore)
1123 .reconstruct_to_writer(writer)
1124 .await
1125 .expect("Reconstruction should succeed with URL refresh");
1126
1127 let reconstructed = writer_buffer.lock().unwrap().clone();
1129 assert_eq!(reconstructed.len(), file_contents.data.len());
1130 assert_eq!(reconstructed, file_contents.data);
1131
1132 assert!(write_count.load(Ordering::Relaxed) >= term_spec.len());
1134 }
1135
1136 #[tokio::test(start_paused = true)]
1140 async fn test_url_refresh_same_xorb_multiple_terms() {
1141 let term_spec = &[(1, (0, 2)), (1, (2, 4)), (1, (4, 6))];
1143 let (client, file_contents) = setup_test_file(term_spec).await;
1144
1145 client.set_fetch_term_url_expiration(Duration::from_secs(1));
1147
1148 let writer = TimeAdvancingWriter::new(Duration::from_secs(2));
1150 let writer_buffer = writer.buffer.clone();
1151
1152 let tiny_semaphore = AdjustableSemaphore::new(1, (1, 1));
1153 let ctx = XetContext::from_external(Handle::current(), XetConfig::new());
1154
1155 FileReconstructor::new(&ctx, &(client.clone() as Arc<dyn Client>), file_contents.file_hash)
1156 .with_config(url_refresh_test_config())
1157 .with_buffer_semaphore(tiny_semaphore)
1158 .reconstruct_to_writer(writer)
1159 .await
1160 .expect("Reconstruction should succeed");
1161
1162 let reconstructed = writer_buffer.lock().unwrap().clone();
1163 assert_eq!(reconstructed, file_contents.data);
1164 }
1165
1166 #[tokio::test(start_paused = true)]
1168 async fn test_url_refresh_large_file_multiple_blocks() {
1169 let term_spec: Vec<(u64, (u64, u64))> = (1..=5).map(|i| (i, (0, 3))).collect();
1171 let (client, file_contents) = setup_test_file(&term_spec).await;
1172
1173 client.set_fetch_term_url_expiration(Duration::from_secs(1));
1175
1176 let writer = TimeAdvancingWriter::new(Duration::from_secs(2));
1177 let writer_buffer = writer.buffer.clone();
1178
1179 let tiny_semaphore = AdjustableSemaphore::new(1, (1, 1));
1180 let ctx = XetContext::from_external(Handle::current(), XetConfig::new());
1181
1182 FileReconstructor::new(&ctx, &(client.clone() as Arc<dyn Client>), file_contents.file_hash)
1183 .with_config(url_refresh_test_config())
1184 .with_buffer_semaphore(tiny_semaphore)
1185 .reconstruct_to_writer(writer)
1186 .await
1187 .expect("Reconstruction should succeed");
1188
1189 let reconstructed = writer_buffer.lock().unwrap().clone();
1190 assert_eq!(reconstructed, file_contents.data);
1191 }
1192
1193 #[tokio::test(start_paused = true)]
1195 async fn test_no_url_expiration_control() {
1196 let term_spec = &[(1, (0, 2)), (2, (0, 2)), (3, (0, 2))];
1197 let (client, file_contents) = setup_test_file(term_spec).await;
1198
1199 client.set_fetch_term_url_expiration(Duration::from_secs(3600));
1201
1202 let writer = TimeAdvancingWriter::new(Duration::from_millis(100));
1204 let writer_buffer = writer.buffer.clone();
1205
1206 let tiny_semaphore = AdjustableSemaphore::new(1, (1, 1));
1207 let ctx = XetContext::from_external(Handle::current(), XetConfig::new());
1208
1209 FileReconstructor::new(&ctx, &(client.clone() as Arc<dyn Client>), file_contents.file_hash)
1210 .with_config(url_refresh_test_config())
1211 .with_buffer_semaphore(tiny_semaphore)
1212 .reconstruct_to_writer(writer)
1213 .await
1214 .expect("Reconstruction should succeed");
1215
1216 let reconstructed = writer_buffer.lock().unwrap().clone();
1217 assert_eq!(reconstructed, file_contents.data);
1218 }
1219
1220 #[tokio::test(start_paused = true)]
1222 async fn test_url_refresh_partial_range() {
1223 let term_spec = &[(1, (0, 5)), (2, (0, 5))];
1224 let (client, file_contents) = setup_test_file(term_spec).await;
1225 let file_len = file_contents.data.len() as u64;
1226
1227 client.set_fetch_term_url_expiration(Duration::from_secs(1));
1228
1229 let writer = TimeAdvancingWriter::new(Duration::from_secs(2));
1230 let writer_buffer = writer.buffer.clone();
1231
1232 let tiny_semaphore = AdjustableSemaphore::new(1, (0, 1));
1233 let ctx = XetContext::from_external(Handle::current(), XetConfig::new());
1234
1235 let range = FileRange::new(file_len / 4, file_len * 3 / 4);
1236
1237 FileReconstructor::new(&ctx, &(client.clone() as Arc<dyn Client>), file_contents.file_hash)
1238 .with_byte_range(range)
1239 .with_config(url_refresh_test_config())
1240 .with_buffer_semaphore(tiny_semaphore)
1241 .reconstruct_to_writer(writer)
1242 .await
1243 .expect("Reconstruction should succeed");
1244
1245 let reconstructed = writer_buffer.lock().unwrap().clone();
1246 let expected = &file_contents.data[range.start as usize..range.end as usize];
1247 assert_eq!(reconstructed, expected);
1248 }
1249
1250 #[test]
1251 fn test_dynamic_buffer_scaling_noop_increment_preserves_total_permits() {
1252 let mut runtime_config = xet_runtime::config::XetConfig::new();
1253 runtime_config.reconstruction.download_buffer_size = xet_runtime::utils::ByteSize::from("1kb");
1254 runtime_config.reconstruction.download_buffer_limit = xet_runtime::utils::ByteSize::from("4kb");
1255 let expected_total = runtime_config.reconstruction.download_buffer_limit.as_u64();
1256
1257 let ctx = XetContext::with_config(runtime_config).unwrap();
1258 let runtime = ctx.runtime.clone();
1259
1260 runtime
1261 .bridge_sync(async move {
1262 let ctx = ctx.clone();
1263 let (client, file_contents) = setup_test_file(&[(1, (0, 2)), (2, (0, 2)), (3, (0, 2))]).await;
1264 let sem = ctx.common.reconstruction_download_buffer.clone();
1265
1266 let p = sem.increment_total_permits(u64::MAX).unwrap();
1268 drop(p);
1269 assert_eq!(sem.total_permits(), expected_total);
1270
1271 let mut config = test_config();
1272 config.download_buffer_perfile_size = xet_runtime::utils::ByteSize::from("8kb");
1273
1274 let reconstructed = reconstruct_to_vec(&client, file_contents.file_hash, None, &config, None)
1275 .await
1276 .unwrap();
1277 assert_eq!(reconstructed, file_contents.data);
1278
1279 assert_eq!(sem.total_permits(), expected_total);
1280 assert_eq!(ctx.common.active_downloads.load(Ordering::Relaxed), 0);
1281 })
1282 .unwrap();
1283 }
1284
1285 async fn reconstruct_range_to_file_path(
1291 client: &Arc<LocalClient>,
1292 file_hash: MerkleHash,
1293 file_path: &std::path::Path,
1294 range: FileRange,
1295 config: ReconstructionConfig,
1296 ) -> Result<u64> {
1297 FileReconstructor::new(&XetContext::default().unwrap(), &(client.clone() as Arc<dyn Client>), file_hash)
1298 .with_byte_range(range)
1299 .with_config(config)
1300 .reconstruct_to_file(file_path, None, false)
1301 .await
1302 }
1303
1304 #[tokio::test]
1305 async fn test_file_concurrent_non_overlapping_range_writes() {
1306 const NUM_WRITERS: usize = 16;
1308 const LARGE_CHUNK_SIZE: usize = 4096;
1309
1310 let term_spec: Vec<(u64, (u64, u64))> = (1..=16).map(|i| (i, (0, 16))).collect();
1313
1314 let client = LocalClient::temporary(XetContext::default().unwrap()).await.unwrap();
1315 let file_contents = client.upload_random_file(&term_spec, LARGE_CHUNK_SIZE).await.unwrap();
1316 let file_len = file_contents.data.len() as u64;
1317
1318 let temp_dir = tempfile::tempdir().unwrap();
1319 let file_path = temp_dir.path().join("output.bin");
1320
1321 std::fs::write(&file_path, vec![0u8; file_len as usize]).unwrap();
1323
1324 let mut config = ReconstructionConfig::default();
1326 config.min_reconstruction_fetch_size = xet_runtime::utils::ByteSize::from("32kb");
1327 config.max_reconstruction_fetch_size = xet_runtime::utils::ByteSize::from("128kb");
1328
1329 let chunk_size = file_len / NUM_WRITERS as u64;
1331 let ranges: Vec<FileRange> = (0..NUM_WRITERS)
1332 .map(|i| {
1333 let start = i as u64 * chunk_size;
1334 let end = if i == NUM_WRITERS - 1 {
1335 file_len
1336 } else {
1337 (i as u64 + 1) * chunk_size
1338 };
1339 FileRange::new(start, end)
1340 })
1341 .collect();
1342
1343 let mut join_set = tokio::task::JoinSet::new();
1345
1346 for range in ranges {
1347 let client = client.clone();
1348 let file_hash = file_contents.file_hash;
1349 let file_path = file_path.clone();
1350 let config = config.clone();
1351
1352 join_set.spawn(async move {
1353 FileReconstructor::new(&XetContext::default().unwrap(), &(client as Arc<dyn Client>), file_hash)
1354 .with_byte_range(range)
1355 .with_config(config)
1356 .reconstruct_to_file(&file_path, None, false)
1357 .await
1358 });
1359 }
1360
1361 while let Some(result) = join_set.join_next().await {
1363 result.unwrap().unwrap();
1364 }
1365
1366 let reconstructed = std::fs::read(&file_path).unwrap();
1368 assert_eq!(reconstructed.len(), file_contents.data.len());
1369 assert_eq!(reconstructed, file_contents.data);
1370 }
1371
1372 #[tokio::test]
1373 async fn test_file_writes_preserve_existing_content() {
1374 let (client, file_contents) = setup_test_file(&[(1, (0, 10))]).await;
1376 let file_len = file_contents.data.len() as u64;
1377
1378 let temp_dir = tempfile::tempdir().unwrap();
1379 let file_path = temp_dir.path().join("output.bin");
1380
1381 let pattern: Vec<u8> = (0..file_len).map(|i| (i % 251) as u8).collect();
1383 std::fs::write(&file_path, &pattern).unwrap();
1384
1385 let start = file_len / 3;
1387 let end = 2 * file_len / 3;
1388 let range = FileRange::new(start, end);
1389
1390 reconstruct_range_to_file_path(&client, file_contents.file_hash, &file_path, range, test_config())
1391 .await
1392 .unwrap();
1393
1394 let result = std::fs::read(&file_path).unwrap();
1395
1396 assert_eq!(&result[..start as usize], &pattern[..start as usize]);
1398 assert_eq!(&result[end as usize..], &pattern[end as usize..]);
1399
1400 assert_eq!(&result[start as usize..end as usize], &file_contents.data[start as usize..end as usize]);
1402 }
1403
1404 #[tokio::test]
1412 async fn test_triple_disjoint_ranges_full() {
1413 let (client, file_contents) = setup_test_file(&[(1, (0, 2)), (1, (4, 6)), (1, (8, 10))]).await;
1414 reconstruct_and_verify_full(&client, &file_contents, test_config()).await;
1415 }
1416
1417 #[tokio::test]
1419 async fn test_triple_disjoint_ranges_partial() {
1420 let (client, file_contents) = setup_test_file(&[(1, (0, 2)), (1, (4, 6)), (1, (8, 10))]).await;
1421 let file_len = file_contents.data.len() as u64;
1422 let range = FileRange::new(file_len / 4, file_len * 3 / 4);
1423 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
1424 }
1425
1426 #[tokio::test]
1428 async fn test_multi_xorb_interleaved_disjoint() {
1429 let term_spec = &[
1430 (1, (0, 2)),
1431 (2, (0, 2)),
1432 (1, (4, 6)),
1433 (2, (4, 6)),
1434 (1, (8, 10)),
1435 (2, (8, 10)),
1436 ];
1437 let (client, file_contents) = setup_test_file(term_spec).await;
1438 reconstruct_and_verify_full(&client, &file_contents, test_config()).await;
1439 }
1440
1441 #[tokio::test]
1443 async fn test_multi_xorb_interleaved_disjoint_partial() {
1444 let term_spec = &[
1445 (1, (0, 2)),
1446 (2, (0, 2)),
1447 (1, (4, 6)),
1448 (2, (4, 6)),
1449 (1, (8, 10)),
1450 (2, (8, 10)),
1451 ];
1452 let (client, file_contents) = setup_test_file(term_spec).await;
1453 let file_len = file_contents.data.len() as u64;
1454 let range = FileRange::new(file_len / 3, file_len * 2 / 3);
1455 reconstruct_and_verify_range(&client, &file_contents, range, test_config()).await;
1456 }
1457
1458 #[tokio::test]
1460 async fn test_four_disjoint_ranges() {
1461 let term_spec = &[(1, (0, 2)), (1, (4, 6)), (1, (8, 10)), (1, (12, 14))];
1462 let (client, file_contents) = setup_test_file(term_spec).await;
1463 reconstruct_and_verify_full(&client, &file_contents, test_config()).await;
1464 }
1465
1466 #[tokio::test]
1468 async fn test_mixed_contiguous_and_disjoint() {
1469 let term_spec = &[
1470 (1, (0, 3)), (1, (3, 5)), (1, (8, 10)), ];
1474 let (client, file_contents) = setup_test_file(term_spec).await;
1475 reconstruct_and_verify_full(&client, &file_contents, test_config()).await;
1476 }
1477
1478 #[tokio::test]
1480 async fn test_complex_three_xorb_disjoint() {
1481 let term_spec = &[
1482 (1, (0, 2)),
1483 (2, (0, 3)),
1484 (3, (2, 5)),
1485 (1, (5, 8)),
1486 (2, (6, 8)),
1487 (3, (0, 2)),
1488 ];
1489 let (client, file_contents) = setup_test_file(term_spec).await;
1490 reconstruct_and_verify_full(&client, &file_contents, test_config()).await;
1491 }
1492
1493 #[tokio::test]
1495 async fn test_local_client_max_ranges_2_disjoint() {
1496 let client = LocalClient::temporary(XetContext::default().unwrap()).await.unwrap();
1497 client.set_max_ranges_per_fetch(2);
1498
1499 let term_spec = &[(1, (0, 2)), (1, (4, 6)), (1, (8, 10)), (1, (12, 14))];
1500 let file_contents = client.upload_random_file(term_spec, TEST_CHUNK_SIZE).await.unwrap();
1501
1502 let config = test_config();
1503 let result = reconstruct_to_vec(&client, file_contents.file_hash, None, &config, None)
1504 .await
1505 .unwrap();
1506 assert_eq!(result, file_contents.data.as_ref());
1507 }
1508
1509 #[tokio::test]
1511 async fn test_local_client_max_ranges_1_multi_xorb() {
1512 let client = LocalClient::temporary(XetContext::default().unwrap()).await.unwrap();
1513 client.set_max_ranges_per_fetch(1);
1514
1515 let term_spec = &[(1, (0, 2)), (2, (0, 2)), (1, (4, 6)), (2, (4, 6))];
1516 let file_contents = client.upload_random_file(term_spec, TEST_CHUNK_SIZE).await.unwrap();
1517
1518 let config = test_config();
1519 let result = reconstruct_to_vec(&client, file_contents.file_hash, None, &config, None)
1520 .await
1521 .unwrap();
1522 assert_eq!(result, file_contents.data.as_ref());
1523 }
1524
1525 mod cancellation_tests {
1528 use tokio_util::sync::CancellationToken;
1529
1530 use super::*;
1531
1532 #[tokio::test]
1533 async fn test_cancellation_token_before_start() {
1534 let (client, file_contents) = setup_test_file(&[(1, (0, 3))]).await;
1535 let config = test_config();
1536
1537 let token = CancellationToken::new();
1538 token.cancel();
1539 let buffer = Arc::new(std::sync::Mutex::new(Cursor::new(Vec::new())));
1540 let writer = StaticCursorWriter(buffer.clone());
1541
1542 let bytes_written = FileReconstructor::new(
1543 &XetContext::default().unwrap(),
1544 &(client.clone() as Arc<dyn Client>),
1545 file_contents.file_hash,
1546 )
1547 .with_config(&config)
1548 .with_cancellation_token(token)
1549 .reconstruct_to_writer(writer)
1550 .await
1551 .unwrap();
1552
1553 assert_eq!(bytes_written, 0);
1554 }
1555
1556 struct CancellingWriter {
1559 buffer: Arc<std::sync::Mutex<Vec<u8>>>,
1560 cancel_token: CancellationToken,
1561 write_count: AtomicUsize,
1562 cancel_after_writes: usize,
1563 }
1564
1565 impl Write for CancellingWriter {
1566 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1567 let n = self.buffer.lock().unwrap().write(buf)?;
1568 let count = self.write_count.fetch_add(1, Ordering::Relaxed) + 1;
1569 if count >= self.cancel_after_writes {
1570 self.cancel_token.cancel();
1571 }
1572 Ok(n)
1573 }
1574
1575 fn flush(&mut self) -> std::io::Result<()> {
1576 Ok(())
1577 }
1578 }
1579
1580 #[tokio::test]
1581 async fn test_cancellation_token_during_reconstruction() {
1582 let term_spec: Vec<(u64, (u64, u64))> = (1..=10).map(|i| (i, (0, 5))).collect();
1583 let (client, file_contents) = setup_test_file(&term_spec).await;
1584 let config = test_config();
1585
1586 let token = CancellationToken::new();
1587 let buffer = Arc::new(std::sync::Mutex::new(Vec::new()));
1588
1589 let writer = CancellingWriter {
1590 buffer: buffer.clone(),
1591 cancel_token: token.clone(),
1592 write_count: AtomicUsize::new(0),
1593 cancel_after_writes: 1,
1594 };
1595
1596 let tiny_semaphore = AdjustableSemaphore::new(1, (1, 1));
1598
1599 let bytes_written = FileReconstructor::new(
1600 &XetContext::default().unwrap(),
1601 &(client.clone() as Arc<dyn Client>),
1602 file_contents.file_hash,
1603 )
1604 .with_config(&config)
1605 .with_cancellation_token(token)
1606 .with_buffer_semaphore(tiny_semaphore)
1607 .reconstruct_to_writer(writer)
1608 .await
1609 .unwrap();
1610
1611 assert_eq!(bytes_written, 0);
1613 let written = buffer.lock().unwrap().len();
1614 assert!(written < file_contents.data.len());
1615 }
1616
1617 #[tokio::test]
1618 async fn test_cancellation_token_not_set_completes_normally() {
1619 let (client, file_contents) = setup_test_file(&[(1, (0, 3)), (2, (0, 2))]).await;
1620 let config = test_config();
1621
1622 let token = CancellationToken::new();
1623 let buffer = Arc::new(std::sync::Mutex::new(Cursor::new(Vec::new())));
1624 let writer = StaticCursorWriter(buffer.clone());
1625
1626 let bytes_written = FileReconstructor::new(
1627 &XetContext::default().unwrap(),
1628 &(client.clone() as Arc<dyn Client>),
1629 file_contents.file_hash,
1630 )
1631 .with_config(&config)
1632 .with_cancellation_token(token)
1633 .reconstruct_to_writer(writer)
1634 .await
1635 .unwrap();
1636
1637 assert_eq!(bytes_written, file_contents.data.len() as u64);
1638 assert_eq!(buffer.lock().unwrap().get_ref().clone(), file_contents.data);
1639 }
1640 }
1641
1642 mod multirange_tests {
1645 use super::*;
1646
1647 fn with_multirange_config(enable: bool) -> XetContext {
1648 let mut config = xet_runtime::config::XetConfig::new();
1649 config.client.enable_multirange_fetching = enable;
1650 XetContext::with_config(config).unwrap()
1651 }
1652
1653 #[test]
1656 fn test_multirange_local_client() {
1657 for enable in [false, true] {
1658 let ctx = with_multirange_config(enable);
1659 ctx.runtime
1660 .bridge_sync(async move {
1661 let scenarios: Vec<Vec<(u64, (u64, u64))>> = vec![
1662 vec![(1, (0, 2)), (1, (4, 6)), (1, (8, 10))],
1663 vec![
1664 (1, (0, 2)),
1665 (2, (0, 2)),
1666 (1, (4, 6)),
1667 (2, (4, 6)),
1668 (1, (8, 10)),
1669 (2, (8, 10)),
1670 ],
1671 vec![
1672 (1, (0, 2)),
1673 (2, (0, 3)),
1674 (3, (2, 5)),
1675 (1, (5, 8)),
1676 (2, (6, 8)),
1677 (3, (0, 2)),
1678 ],
1679 ];
1680 let config = test_config();
1681 for term_spec in &scenarios {
1682 let (client, fc) = setup_test_file(term_spec).await;
1683 reconstruct_and_verify_full(&client, &fc, config.clone()).await;
1684
1685 let file_len = fc.data.len() as u64;
1686 let range = FileRange::new(file_len / 4, file_len * 3 / 4);
1687 reconstruct_and_verify_range(&client, &fc, range, config.clone()).await;
1688 }
1689 })
1690 .unwrap();
1691 }
1692 }
1693
1694 #[test]
1696 fn test_multirange_max_ranges() {
1697 for enable in [false, true] {
1698 let ctx = with_multirange_config(enable);
1699 ctx.runtime
1700 .bridge_sync(async {
1701 let client = LocalClient::temporary(XetContext::default().unwrap()).await.unwrap();
1702 client.set_max_ranges_per_fetch(2);
1703
1704 let term_spec = &[(1, (0, 2)), (1, (4, 6)), (1, (8, 10)), (1, (12, 14))];
1705 let fc = client.upload_random_file(term_spec, TEST_CHUNK_SIZE).await.unwrap();
1706
1707 let config = test_config();
1708 let result = reconstruct_to_vec(&client, fc.file_hash, None, &config, None).await.unwrap();
1709 assert_eq!(result, fc.data.as_ref());
1710 })
1711 .unwrap();
1712 }
1713 }
1714 }
1715
1716 #[cfg(feature = "simulation")]
1718 mod server_tests {
1719 use super::*;
1720
1721 async fn reconstruct_via_server(
1728 server: &xet_client::cas_client::LocalTestServer,
1729 file_hash: MerkleHash,
1730 byte_range: Option<FileRange>,
1731 config: &ReconstructionConfig,
1732 ) -> Result<Vec<u8>> {
1733 let buffer = Arc::new(std::sync::Mutex::new(Cursor::new(Vec::new())));
1734 let writer = StaticCursorWriter(buffer.clone());
1735
1736 let client: Arc<dyn Client> = server.remote_client().clone();
1737 let mut reconstructor =
1738 FileReconstructor::new(&XetContext::default().unwrap(), &client, file_hash).with_config(config);
1739
1740 if let Some(range) = byte_range {
1741 reconstructor = reconstructor.with_byte_range(range);
1742 }
1743
1744 reconstructor.reconstruct_to_writer(writer).await?;
1745
1746 let data = buffer.lock().unwrap().get_ref().clone();
1747 Ok(data)
1748 }
1749
1750 #[tokio::test]
1751 async fn test_v1_fallback_full_reconstruction() {
1752 let server = xet_client::cas_client::LocalTestServerBuilder::new().start().await;
1753 let file_contents = server
1754 .remote_client()
1755 .upload_random_file(&[(1, (0, 3)), (2, (0, 2))], TEST_CHUNK_SIZE)
1756 .await
1757 .unwrap();
1758
1759 server.disable_v2_reconstruction(404);
1760
1761 let config = test_config();
1762 let result = reconstruct_via_server(&server, file_contents.file_hash, None, &config)
1763 .await
1764 .unwrap();
1765 assert_eq!(result, file_contents.data.as_ref());
1766 }
1767
1768 #[tokio::test]
1769 async fn test_v1_fallback_partial_range() {
1770 let server = xet_client::cas_client::LocalTestServerBuilder::new().start().await;
1771 let file_contents = server
1772 .remote_client()
1773 .upload_random_file(&[(1, (0, 5)), (2, (0, 3))], TEST_CHUNK_SIZE)
1774 .await
1775 .unwrap();
1776
1777 server.disable_v2_reconstruction(404);
1778
1779 let file_len = file_contents.data.len() as u64;
1780 let range = FileRange::new(file_len / 4, file_len * 3 / 4);
1781
1782 let config = test_config();
1783 let result = reconstruct_via_server(&server, file_contents.file_hash, Some(range), &config)
1784 .await
1785 .unwrap();
1786 assert_eq!(result, &file_contents.data[range.start as usize..range.end as usize]);
1787 }
1788
1789 #[tokio::test]
1790 async fn test_v1_fallback_non_contiguous_chunks() {
1791 let server = xet_client::cas_client::LocalTestServerBuilder::new().start().await;
1792 let file_contents = server
1793 .remote_client()
1794 .upload_random_file(&[(1, (0, 2)), (1, (4, 6))], TEST_CHUNK_SIZE)
1795 .await
1796 .unwrap();
1797
1798 server.disable_v2_reconstruction(404);
1799
1800 let config = test_config();
1801 let result = reconstruct_via_server(&server, file_contents.file_hash, None, &config)
1802 .await
1803 .unwrap();
1804 assert_eq!(result, file_contents.data.as_ref());
1805 }
1806
1807 #[tokio::test]
1808 async fn test_v1_fallback_multiple_xorbs() {
1809 let server = xet_client::cas_client::LocalTestServerBuilder::new().start().await;
1810 let file_contents = server
1811 .remote_client()
1812 .upload_random_file(&[(1, (0, 2)), (2, (0, 3)), (3, (0, 2)), (1, (2, 4))], TEST_CHUNK_SIZE)
1813 .await
1814 .unwrap();
1815
1816 server.disable_v2_reconstruction(404);
1817
1818 let config = test_config();
1819 let result = reconstruct_via_server(&server, file_contents.file_hash, None, &config)
1820 .await
1821 .unwrap();
1822 assert_eq!(result, file_contents.data.as_ref());
1823 }
1824
1825 #[tokio::test]
1827 async fn test_v1_fallback_triple_disjoint_ranges() {
1828 let server = xet_client::cas_client::LocalTestServerBuilder::new().start().await;
1829 let file_contents = server
1830 .remote_client()
1831 .upload_random_file(&[(1, (0, 2)), (1, (4, 6)), (1, (8, 10))], TEST_CHUNK_SIZE)
1832 .await
1833 .unwrap();
1834
1835 server.disable_v2_reconstruction(404);
1836
1837 let config = test_config();
1838 let result = reconstruct_via_server(&server, file_contents.file_hash, None, &config)
1839 .await
1840 .unwrap();
1841 assert_eq!(result, file_contents.data.as_ref());
1842 }
1843
1844 async fn reconstruct_via_server_with_max_ranges(
1848 term_spec: &[(u64, (u64, u64))],
1849 max_ranges: usize,
1850 byte_range: Option<FileRange>,
1851 ) -> (Vec<u8>, RandomFileContents) {
1852 let server = xet_client::cas_client::LocalTestServerBuilder::new().start().await;
1853 let file_contents = server
1854 .remote_client()
1855 .upload_random_file(term_spec, TEST_CHUNK_SIZE)
1856 .await
1857 .unwrap();
1858
1859 server.set_max_ranges_per_fetch(max_ranges);
1860
1861 let config = test_config();
1862 let result = reconstruct_via_server(&server, file_contents.file_hash, byte_range, &config)
1863 .await
1864 .unwrap();
1865 (result, file_contents)
1866 }
1867
1868 #[tokio::test]
1869 async fn test_max_ranges_simple() {
1870 let (result, file_contents) =
1871 reconstruct_via_server_with_max_ranges(&[(1, (0, 3)), (2, (0, 2))], 2, None).await;
1872 assert_eq!(result, file_contents.data.as_ref());
1873 }
1874
1875 #[tokio::test]
1876 async fn test_max_ranges_1_disjoint() {
1877 let (result, file_contents) =
1878 reconstruct_via_server_with_max_ranges(&[(1, (0, 2)), (1, (4, 6))], 1, None).await;
1879 assert_eq!(result, file_contents.data.as_ref());
1880 }
1881
1882 #[tokio::test]
1883 async fn test_max_ranges_2_triple_disjoint() {
1884 let (result, file_contents) =
1885 reconstruct_via_server_with_max_ranges(&[(1, (0, 2)), (1, (4, 6)), (1, (8, 10))], 2, None).await;
1886 assert_eq!(result, file_contents.data.as_ref());
1887 }
1888
1889 #[tokio::test]
1890 async fn test_max_ranges_2_multi_xorb_disjoint() {
1891 let term_spec = &[
1892 (1, (0, 2)),
1893 (2, (0, 2)),
1894 (1, (4, 6)),
1895 (2, (4, 6)),
1896 (1, (8, 10)),
1897 (2, (8, 10)),
1898 ];
1899 let (result, file_contents) = reconstruct_via_server_with_max_ranges(term_spec, 2, None).await;
1900 assert_eq!(result, file_contents.data.as_ref());
1901 }
1902
1903 #[tokio::test]
1904 async fn test_max_ranges_2_partial_range() {
1905 let term_spec = &[
1906 (1, (0, 3)),
1907 (2, (0, 2)),
1908 (1, (3, 5)),
1909 (3, (1, 4)),
1910 (2, (4, 6)),
1911 (1, (0, 2)),
1912 ];
1913 let server = xet_client::cas_client::LocalTestServerBuilder::new().start().await;
1914 let file_contents = server
1915 .remote_client()
1916 .upload_random_file(term_spec, TEST_CHUNK_SIZE)
1917 .await
1918 .unwrap();
1919
1920 server.set_max_ranges_per_fetch(2);
1921
1922 let file_len = file_contents.data.len() as u64;
1923 let range = FileRange::new(file_len / 4, file_len * 3 / 4);
1924
1925 let config = test_config();
1926 let result = reconstruct_via_server(&server, file_contents.file_hash, Some(range), &config)
1927 .await
1928 .unwrap();
1929 assert_eq!(result, &file_contents.data[range.start as usize..range.end as usize]);
1930 }
1931
1932 fn with_multirange_config(enable: bool) -> XetContext {
1935 let mut config = xet_runtime::config::XetConfig::new();
1936 config.client.enable_multirange_fetching = enable;
1937 XetContext::with_config(config).unwrap()
1938 }
1939
1940 #[test]
1943 fn test_multirange_via_server() {
1944 for enable in [false, true] {
1945 let ctx = with_multirange_config(enable);
1946 ctx.runtime
1947 .bridge_sync(async {
1948 let config = test_config();
1949
1950 let server = xet_client::cas_client::LocalTestServerBuilder::new().start().await;
1952 let fc = server
1953 .remote_client()
1954 .upload_random_file(&[(1, (0, 2)), (1, (4, 6)), (1, (8, 10))], TEST_CHUNK_SIZE)
1955 .await
1956 .unwrap();
1957 let result = reconstruct_via_server(&server, fc.file_hash, None, &config).await.unwrap();
1958 assert_eq!(result, fc.data.as_ref());
1959
1960 let server = xet_client::cas_client::LocalTestServerBuilder::new().start().await;
1962 let fc = server
1963 .remote_client()
1964 .upload_random_file(
1965 &[(1, (0, 2)), (2, (0, 2)), (1, (4, 6)), (2, (4, 6)), (1, (8, 10))],
1966 TEST_CHUNK_SIZE,
1967 )
1968 .await
1969 .unwrap();
1970 server.set_max_ranges_per_fetch(2);
1971 let result = reconstruct_via_server(&server, fc.file_hash, None, &config).await.unwrap();
1972 assert_eq!(result, fc.data.as_ref());
1973
1974 let server = xet_client::cas_client::LocalTestServerBuilder::new().start().await;
1976 let fc = server
1977 .remote_client()
1978 .upload_random_file(&[(1, (0, 3)), (2, (0, 2)), (1, (3, 5)), (2, (4, 6))], TEST_CHUNK_SIZE)
1979 .await
1980 .unwrap();
1981 let file_len = fc.data.len() as u64;
1982 let range = FileRange::new(file_len / 4, file_len * 3 / 4);
1983 let result = reconstruct_via_server(&server, fc.file_hash, Some(range), &config)
1984 .await
1985 .unwrap();
1986 assert_eq!(result, &fc.data[range.start as usize..range.end as usize]);
1987 })
1988 .unwrap();
1989 }
1990 }
1991 } }