1use std::{
2 io,
3 marker::PhantomData,
4 path::{Path, PathBuf},
5 sync::Arc,
6};
7
8use chrono::{DateTime, NaiveDateTime, Utc};
9use fs4::{available_space, total_space};
10use rand::RngExt as _;
11use saluki_error::{generic_error, ErrorContext as _, GenericError};
12use serde::{de::DeserializeOwned, Serialize};
13use tracing::{debug, info, warn};
14
15use super::{EventContainer, PushResult};
16
17struct PersistedEntry {
21 path: PathBuf,
22 timestamp: u128,
23 size_bytes: u64,
24}
25
26impl PersistedEntry {
27 fn try_from_path(path: PathBuf, size_bytes: u64) -> Option<Self> {
31 let timestamp = decode_timestamped_filename(&path)?;
32 Some(Self {
33 path,
34 timestamp,
35 size_bytes,
36 })
37 }
38
39 fn from_parts(path: PathBuf, timestamp: u128, size_bytes: u64) -> Self {
40 Self {
41 path,
42 timestamp,
43 size_bytes,
44 }
45 }
46}
47
48pub trait DiskUsageRetriever {
49 fn total_space(&self) -> Result<u64, GenericError>;
50 fn available_space(&self) -> Result<u64, GenericError>;
51}
52
53pub struct DiskUsageRetrieverImpl {
54 root_path: PathBuf,
55}
56
57impl DiskUsageRetrieverImpl {
58 pub fn new(root_path: PathBuf) -> Self {
59 Self { root_path }
60 }
61}
62
63impl DiskUsageRetriever for DiskUsageRetrieverImpl {
64 fn total_space(&self) -> Result<u64, GenericError> {
65 total_space(&self.root_path)
66 .with_error_context(|| format!("Failed to get total space for '{}'.", self.root_path.display()))
67 }
68
69 fn available_space(&self) -> Result<u64, GenericError> {
70 available_space(&self.root_path)
71 .with_error_context(|| format!("Failed to get available space for '{}'.", self.root_path.display()))
72 }
73}
74
75#[derive(Clone)]
76pub struct DiskUsageRetrieverWrapper {
77 inner: Arc<dyn DiskUsageRetriever + Send + Sync>,
78}
79
80impl DiskUsageRetrieverWrapper {
81 pub fn new(disk_usage_retriever: Arc<dyn DiskUsageRetriever + Send + Sync>) -> Self {
82 Self {
83 inner: disk_usage_retriever,
84 }
85 }
86}
87
88pub struct PersistedQueueArgs {
90 pub root_path: PathBuf,
92 pub max_on_disk_bytes: u64,
94 pub storage_max_disk_ratio: f64,
96 pub disk_usage_retriever: Arc<dyn DiskUsageRetriever + Send + Sync>,
98 pub max_age_days: u32,
103}
104
105pub struct PersistedQueue<T> {
106 root_path: PathBuf,
107 entries: Vec<PersistedEntry>,
108 total_on_disk_bytes: u64,
109 max_on_disk_bytes: u64,
110 storage_max_disk_ratio: f64,
111 disk_usage_retriever: DiskUsageRetrieverWrapper,
112 max_age_days: u32,
113 entries_dropped: u64,
114 _entry: PhantomData<T>,
115}
116
117impl<T> PersistedQueue<T>
118where
119 T: EventContainer + DeserializeOwned + Serialize,
120{
121 pub async fn from_root_path(args: PersistedQueueArgs) -> Result<Self, GenericError> {
133 let PersistedQueueArgs {
134 root_path,
135 max_on_disk_bytes,
136 storage_max_disk_ratio,
137 disk_usage_retriever,
138 max_age_days,
139 } = args;
140
141 create_directory_recursive(root_path.clone())
143 .await
144 .with_error_context(|| format!("Failed to create retry directory '{}'.", root_path.display()))?;
145
146 let mut persisted_requests = Self {
147 root_path: root_path.clone(),
148 entries: Vec::new(),
149 total_on_disk_bytes: 0,
150 max_on_disk_bytes,
151 storage_max_disk_ratio,
152 disk_usage_retriever: DiskUsageRetrieverWrapper::new(disk_usage_retriever),
153 max_age_days,
154 entries_dropped: 0,
155 _entry: PhantomData,
156 };
157
158 persisted_requests.refresh_entry_state().await?;
159
160 info!(
161 "Persisted retry queue initialized. Transactions will be stored in '{}'.",
162 root_path.display()
163 );
164
165 Ok(persisted_requests)
166 }
167
168 pub fn is_empty(&self) -> bool {
170 self.entries.is_empty()
171 }
172
173 pub fn len(&self) -> usize {
175 self.entries.len()
176 }
177
178 pub async fn available_capacity_bytes(&self) -> Result<u64, GenericError> {
187 let disk_usage_retriever = self.disk_usage_retriever.clone();
188 let storage_max_disk_ratio = self.storage_max_disk_ratio;
189 let max_on_disk_bytes = self.max_on_disk_bytes;
190
191 let limit = tokio::task::spawn_blocking(move || {
192 on_disk_bytes_limit(disk_usage_retriever, storage_max_disk_ratio, max_on_disk_bytes)
193 })
194 .await
195 .error_context("Failed to run disk size limit check to completion.")??;
196
197 Ok(limit.saturating_sub(self.total_on_disk_bytes))
198 }
199
200 pub fn take_entries_dropped(&mut self) -> u64 {
203 std::mem::take(&mut self.entries_dropped)
204 }
205
206 pub async fn remove_stale_files(&mut self) -> Result<u32, GenericError> {
214 let removed = remove_outdated_retry_files(&self.root_path, self.max_age_days).await?;
215 self.refresh_entry_state().await.map_err(GenericError::from)?;
216 Ok(removed)
217 }
218
219 pub async fn push(&mut self, entry: T) -> Result<PushResult, GenericError> {
226 let (filename, timestamp) = generate_timestamped_filename();
228 let entry_path = self.root_path.join(filename);
229 let serialized = serde_json::to_vec(&entry)
230 .with_error_context(|| format!("Failed to serialize entry for '{}'.", entry_path.display()))?;
231
232 if serialized.len() as u64 > self.max_on_disk_bytes {
233 return Err(generic_error!("Entry is too large to persist."));
234 }
235
236 let push_result = self
238 .remove_until_available_space(serialized.len() as u64)
239 .await
240 .error_context(
241 "Failed to remove older persisted entries to make space for the incoming persisted entry.",
242 )?;
243
244 tokio::fs::write(&entry_path, &serialized)
246 .await
247 .with_error_context(|| format!("Failed to write entry to '{}'.", entry_path.display()))?;
248
249 self.entries.push(PersistedEntry::from_parts(
251 entry_path,
252 timestamp,
253 serialized.len() as u64,
254 ));
255 self.total_on_disk_bytes += serialized.len() as u64;
256
257 saluki_antithesis::always_le!(
259 self.total_on_disk_bytes,
260 self.max_on_disk_bytes,
261 "retry queue on-disk bytes within cap",
262 { "bytes": self.total_on_disk_bytes, "cap": self.max_on_disk_bytes }
263 );
264
265 debug!(entry.len = serialized.len(), "Enqueued persisted entry.");
266
267 Ok(push_result)
268 }
269
270 pub async fn pop(&mut self) -> Result<Option<T>, GenericError> {
276 loop {
277 if self.entries.is_empty() {
278 return Ok(None);
279 }
280
281 let entry = self.entries.remove(0);
282 match try_deserialize_entry(&entry).await {
283 Ok(Some(deserialized)) => {
284 self.total_on_disk_bytes -= entry.size_bytes;
286 debug!(entry.len = entry.size_bytes, "Dequeued persisted entry.");
287
288 return Ok(Some(deserialized));
289 }
290 Ok(None) => {
291 self.refresh_entry_state().await?;
294 continue;
295 }
296 Err(e) => {
297 warn!(
300 entry.path = %entry.path.display(),
301 entry.len = entry.size_bytes,
302 error = %e,
303 "Permanently dropping persisted entry that could not be consumed.",
304 );
305
306 self.total_on_disk_bytes -= entry.size_bytes;
307 self.entries_dropped += 1;
308
309 saluki_antithesis::sometimes!(true, "corrupt persisted retry entry dropped, recovery continues");
312
313 continue;
314 }
315 }
316 }
317 }
318
319 async fn refresh_entry_state(&mut self) -> io::Result<()> {
320 let mut entries = Vec::new();
322
323 let mut dir_reader = tokio::fs::read_dir(&self.root_path).await?;
324 while let Some(entry) = dir_reader.next_entry().await? {
325 let metadata = entry.metadata().await?;
326 if metadata.is_file() {
327 match PersistedEntry::try_from_path(entry.path(), metadata.len()) {
328 Some(entry) => entries.push(entry),
329 None => {
330 warn!(
331 file_size = metadata.len(),
332 "Ignoring unrecognized file '{}' in retry directory.",
333 entry.path().display()
334 );
335 continue;
336 }
337 }
338 }
339 }
340
341 entries.sort_by_key(|entry| entry.timestamp);
343 self.total_on_disk_bytes = entries.iter().map(|entry| entry.size_bytes).sum();
344 self.entries = entries;
345
346 Ok(())
347 }
348
349 async fn remove_until_available_space(&mut self, required_bytes: u64) -> Result<PushResult, GenericError> {
356 let mut push_result = PushResult::default();
357
358 let disk_usage_retriever = self.disk_usage_retriever.clone();
359 let storage_max_disk_ratio = self.storage_max_disk_ratio;
360 let max_on_disk_bytes = self.max_on_disk_bytes;
361
362 let limit = tokio::task::spawn_blocking(move || {
369 on_disk_bytes_limit(disk_usage_retriever, storage_max_disk_ratio, max_on_disk_bytes)
370 })
371 .await
372 .error_context("Failed to run disk size limit check to completion.")??;
373
374 while !self.entries.is_empty() && self.total_on_disk_bytes + required_bytes > limit {
375 let entry = self.entries.remove(0);
376
377 let deserialized = match try_deserialize_entry::<T>(&entry).await {
379 Ok(Some(deserialized)) => deserialized,
380 Ok(None) => {
381 warn!(entry.path = %entry.path.display(), "Failed to find entry on disk. Persisted entry state may be inconsistent.");
382 continue;
383 }
384 Err(e) => {
385 warn!(
388 entry.path = %entry.path.display(),
389 entry.len = entry.size_bytes,
390 error = %e,
391 "Permanently dropping persisted entry that could not be consumed during eviction.",
392 );
393
394 self.total_on_disk_bytes -= entry.size_bytes;
395 self.entries_dropped += 1;
396
397 saluki_antithesis::sometimes!(true, "corrupt persisted retry entry dropped, recovery continues");
400
401 continue;
402 }
403 };
404
405 self.total_on_disk_bytes -= entry.size_bytes;
407 push_result.track_dropped_item(&deserialized);
408
409 warn!(entry.path = %entry.path.display(), entry.len = entry.size_bytes, "Dropped persisted entry.");
410 }
411
412 Ok(push_result)
413 }
414}
415
416fn on_disk_bytes_limit(
424 disk_usage_retriever: DiskUsageRetrieverWrapper, storage_max_disk_ratio: f64, max_on_disk_bytes: u64,
425) -> Result<u64, GenericError> {
426 let total_space = disk_usage_retriever.inner.total_space()? as f64;
427 let available_space = disk_usage_retriever.inner.available_space()? as f64;
428 let disk_reserved = total_space * (1.0 - storage_max_disk_ratio);
429 let available_disk_usage = (available_space - disk_reserved).ceil() as u64;
430 Ok(max_on_disk_bytes.min(available_disk_usage))
431}
432
433async fn try_deserialize_entry<T: DeserializeOwned>(entry: &PersistedEntry) -> Result<Option<T>, GenericError> {
434 let serialized = match tokio::fs::read(&entry.path).await {
435 Ok(serialized) => serialized,
436 Err(e) => match e.kind() {
437 io::ErrorKind::NotFound => {
438 return Ok(None);
444 }
445 _ => {
446 return Err(e)
447 .with_error_context(|| format!("Failed to read persisted entry '{}'.", entry.path.display()))
448 }
449 },
450 };
451
452 let deserialized = match serde_json::from_slice(&serialized) {
453 Ok(deserialized) => deserialized,
454 Err(e) => {
455 if let Err(remove_err) = tokio::fs::remove_file(&entry.path).await {
458 warn!(
459 entry.path = %entry.path.display(),
460 error = %remove_err,
461 "Failed to remove corrupt persisted entry from disk.",
462 );
463 }
464
465 return Err(e)
466 .with_error_context(|| format!("Failed to deserialize persisted entry '{}'.", entry.path.display()));
467 }
468 };
469
470 tokio::fs::remove_file(&entry.path)
472 .await
473 .with_error_context(|| format!("Failed to delete persisted entry '{}'.", entry.path.display()))?;
474
475 debug!(entry.path = %entry.path.display(), entry.len = entry.size_bytes, "Consumed persisted entry and removed from disk.");
476 Ok(Some(deserialized))
477}
478
479fn generate_timestamped_filename() -> (PathBuf, u128) {
480 let now = Utc::now();
481 let now_ts = datetime_to_timestamp(now);
482 let nonce = rand::rng().random_range(100000000..999999999);
483
484 let filename = format!("retry-{}-{}.json", now.format("%Y%m%d%H%M%S%f"), nonce).into();
485
486 (filename, now_ts)
487}
488
489fn decode_timestamped_filename(path: &Path) -> Option<u128> {
490 let filename = path.file_stem()?.to_str()?;
491 let mut filename_parts = filename.split('-');
492
493 let prefix = filename_parts.next()?;
494 let timestamp_str = filename_parts.next()?;
495 let nonce = filename_parts.next()?;
496
497 if prefix != "retry" || nonce.parse::<u64>().is_err() {
499 return None;
500 }
501
502 NaiveDateTime::parse_from_str(timestamp_str, "%Y%m%d%H%M%S%f")
504 .map(|dt| datetime_to_timestamp(dt.and_utc()))
505 .ok()
506}
507
508fn datetime_to_timestamp(dt: DateTime<Utc>) -> u128 {
509 let secs = (dt.timestamp() as u128) * 1_000_000_000;
510 let ns = dt.timestamp_subsec_nanos() as u128;
511
512 secs + ns
513}
514
515async fn create_directory_recursive(path: PathBuf) -> Result<(), GenericError> {
516 let mut dir_builder = std::fs::DirBuilder::new();
517 dir_builder.recursive(true);
518
519 #[cfg(unix)]
522 {
523 use std::os::unix::fs::DirBuilderExt;
524 dir_builder.mode(0o700);
525 }
526
527 tokio::task::spawn_blocking(move || {
528 dir_builder
529 .create(&path)
530 .with_error_context(|| format!("Failed to create directory '{}'.", path.display()))
531 })
532 .await
533 .error_context("Failed to spawn directory creation blocking task.")?
534}
535
536async fn remove_outdated_retry_files(queue_path: &Path, max_age_days: u32) -> Result<u32, GenericError> {
542 let mut dir = match tokio::fs::read_dir(queue_path).await {
543 Ok(d) => d,
544 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(0),
545 Err(e) => {
546 return Err(e).with_error_context(|| {
547 format!(
548 "Failed to open retry queue directory '{}' for age-based cleanup.",
549 queue_path.display()
550 )
551 });
552 }
553 };
554 let now_ns = std::time::SystemTime::now()
555 .duration_since(std::time::UNIX_EPOCH)
556 .unwrap_or_default() .as_nanos();
558 let cutoff_ns = now_ns.saturating_sub(max_age_days as u128 * 24 * 3600 * 1_000_000_000);
559 let mut removed = 0u32;
560 loop {
561 let entry = match dir.next_entry().await {
562 Ok(Some(e)) => e,
563 Ok(None) => break,
564 Err(e) => {
565 return Err(e).with_error_context(|| "Error reading retry queue directory during age-based cleanup.");
566 }
567 };
568 let file_ts = match decode_timestamped_filename(&entry.path()) {
569 Some(ts) => ts,
570 None => continue,
571 };
572 if file_ts < cutoff_ns {
573 let name_str = entry.file_name();
574 let name = name_str.to_string_lossy();
575 match tokio::fs::remove_file(entry.path()).await {
576 Ok(()) => {
577 debug!(file = %name, "Removed outdated retry file.");
578 removed += 1;
579 }
580 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
581 debug!(file = %name, "Retry file already removed by concurrent cleanup.");
582 }
583 Err(e) => {
584 warn!(file = %name, error = %e, "Failed to remove outdated retry file.");
585 }
586 }
587 }
588 }
589 Ok(removed)
590}
591
592#[cfg(test)]
593mod tests {
594 use rand::RngExt as _;
595 use rand_distr::Alphanumeric;
596 use serde::Deserialize;
597
598 use super::*;
599
600 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
601 struct FakeData {
602 name: String,
603 value: u32,
604 }
605
606 impl FakeData {
607 fn random() -> Self {
608 Self {
609 name: rand::rng().sample_iter(&Alphanumeric).take(8).map(char::from).collect(),
610 value: rand::rng().random_range(0..100),
611 }
612 }
613 }
614
615 impl EventContainer for FakeData {
616 fn event_count(&self) -> u64 {
617 1
618 }
619 }
620
621 struct MockDiskUsageRetriever {}
622
623 impl DiskUsageRetriever for MockDiskUsageRetriever {
624 fn total_space(&self) -> Result<u64, GenericError> {
625 Ok(100)
626 }
627 fn available_space(&self) -> Result<u64, GenericError> {
628 Ok(100)
629 }
630 }
631
632 async fn files_in_dir(path: &Path) -> usize {
633 let mut file_count = 0;
634 let mut dir_reader = tokio::fs::read_dir(path).await.unwrap();
635 while let Some(entry) = dir_reader.next_entry().await.unwrap() {
636 if entry.metadata().await.unwrap().is_file() {
637 file_count += 1;
638 }
639 }
640 file_count
641 }
642
643 #[tokio::test]
644 async fn basic_push_pop() {
645 let data = FakeData::random();
646
647 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
649 let root_path = temp_dir.path().to_path_buf();
650
651 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
652 root_path: root_path.clone(),
653 max_on_disk_bytes: 1024,
654 storage_max_disk_ratio: 0.8,
655 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
656 max_age_days: 10,
657 })
658 .await
659 .expect("should not fail to create persisted queue");
660
661 assert_eq!(0, files_in_dir(&root_path).await);
663
664 let push_result = persisted_queue
666 .push(data.clone())
667 .await
668 .expect("should not fail to push data");
669 assert_eq!(1, files_in_dir(&root_path).await);
670 assert_eq!(0, push_result.items_dropped);
671 assert_eq!(0, push_result.events_dropped);
672
673 let actual = persisted_queue
675 .pop()
676 .await
677 .expect("should not fail to pop data")
678 .expect("should not be empty");
679 assert_eq!(data, actual);
680 assert_eq!(0, files_in_dir(&root_path).await);
681 }
682
683 #[tokio::test]
684 async fn entry_too_large() {
685 let data = FakeData::random();
686
687 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
689 let root_path = temp_dir.path().to_path_buf();
690
691 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
692 root_path: root_path.clone(),
693 max_on_disk_bytes: 1,
694 storage_max_disk_ratio: 0.8,
695 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
696 max_age_days: 10,
697 })
698 .await
699 .expect("should not fail to create persisted queue");
700
701 assert_eq!(0, files_in_dir(&root_path).await);
703
704 assert!(persisted_queue.push(data).await.is_err());
706
707 assert_eq!(0, files_in_dir(&root_path).await);
709 }
710
711 #[tokio::test]
712 async fn remove_oldest_entry_on_push() {
713 let data1 = FakeData::random();
714 let data2 = FakeData::random();
715
716 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
720 let root_path = temp_dir.path().to_path_buf();
721
722 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
723 root_path: root_path.clone(),
724 max_on_disk_bytes: 32,
725 storage_max_disk_ratio: 0.8,
726 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
727 max_age_days: 10,
728 })
729 .await
730 .expect("should not fail to create persisted queue");
731
732 assert_eq!(0, files_in_dir(&root_path).await);
734
735 let push_result = persisted_queue.push(data1).await.expect("should not fail to push data");
737 assert_eq!(1, files_in_dir(&root_path).await);
738 assert_eq!(0, push_result.items_dropped);
739 assert_eq!(0, push_result.events_dropped);
740
741 let push_result = persisted_queue
743 .push(data2.clone())
744 .await
745 .expect("should not fail to push data");
746 assert_eq!(1, files_in_dir(&root_path).await);
747 assert_eq!(1, push_result.items_dropped);
748 assert_eq!(1, push_result.events_dropped);
749
750 let actual = persisted_queue
753 .pop()
754 .await
755 .expect("should not fail to pop data")
756 .expect("should not be empty");
757 assert_eq!(data2, actual);
758 assert_eq!(0, files_in_dir(&root_path).await);
759 }
760
761 #[tokio::test]
762 async fn storage_ratio_exceeded() {
763 let data1 = FakeData::random();
764 let data2 = FakeData::random();
765
766 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
770 let root_path = temp_dir.path().to_path_buf();
771
772 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
773 root_path: root_path.clone(),
774 max_on_disk_bytes: 80,
775 storage_max_disk_ratio: 0.35,
776 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
777 max_age_days: 10,
778 })
779 .await
780 .expect("should not fail to create persisted queue");
781
782 assert_eq!(0, files_in_dir(&root_path).await);
784
785 let push_result = persisted_queue.push(data1).await.expect("should not fail to push data");
790
791 assert_eq!(1, files_in_dir(&root_path).await);
792 assert_eq!(0, push_result.items_dropped);
793 assert_eq!(0, push_result.events_dropped);
794
795 let push_result = persisted_queue
797 .push(data2.clone())
798 .await
799 .expect("should not fail to push data");
800 assert_eq!(1, files_in_dir(&root_path).await);
801 assert_eq!(1, push_result.items_dropped);
802 assert_eq!(1, push_result.events_dropped);
803
804 let actual = persisted_queue
807 .pop()
808 .await
809 .expect("should not fail to pop data")
810 .expect("should not be empty");
811 assert_eq!(data2, actual);
812 assert_eq!(0, files_in_dir(&root_path).await);
813 }
814
815 async fn write_corrupt_entry(dir: &Path) -> PathBuf {
818 let filename = "retry-20000101000000000000-100000000.json";
819 let path = dir.join(filename);
820 tokio::fs::write(&path, b"this is not valid json").await.unwrap();
821 path
822 }
823
824 #[tokio::test]
825 async fn corrupt_entry_is_skipped_on_pop() {
826 let data = FakeData::random();
827
828 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
829 let root_path = temp_dir.path().to_path_buf();
830
831 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
832 root_path: root_path.clone(),
833 max_on_disk_bytes: 1024,
834 storage_max_disk_ratio: 0.8,
835 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
836 max_age_days: 10,
837 })
838 .await
839 .expect("should not fail to create persisted queue");
840
841 let corrupt_path = write_corrupt_entry(&root_path).await;
843
844 let _ = persisted_queue
846 .push(data.clone())
847 .await
848 .expect("should not fail to push data");
849
850 persisted_queue.refresh_entry_state().await.unwrap();
852
853 let actual = persisted_queue
855 .pop()
856 .await
857 .expect("should not fail to pop data")
858 .expect("should have a valid entry");
859 assert_eq!(data, actual);
860
861 assert!(!corrupt_path.exists());
863
864 assert_eq!(1, persisted_queue.take_entries_dropped());
866
867 assert_eq!(0, files_in_dir(&root_path).await);
869 }
870
871 #[tokio::test]
872 async fn corrupt_entry_does_not_block_queue() {
873 let data1 = FakeData::random();
874 let data2 = FakeData::random();
875
876 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
877 let root_path = temp_dir.path().to_path_buf();
878
879 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
881 root_path: root_path.clone(),
882 max_on_disk_bytes: 1024,
883 storage_max_disk_ratio: 0.8,
884 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
885 max_age_days: 10,
886 })
887 .await
888 .expect("should not fail to create persisted queue");
889
890 let _ = persisted_queue.push(data1).await.expect("should not fail to push data");
892 let _ = persisted_queue
893 .push(data2.clone())
894 .await
895 .expect("should not fail to push data");
896 assert_eq!(2, persisted_queue.entries.len());
897
898 let oldest_path = persisted_queue.entries[0].path.clone();
900 tokio::fs::write(&oldest_path, b"corrupted").await.unwrap();
901
902 let actual = persisted_queue
904 .pop()
905 .await
906 .expect("should not fail to pop data")
907 .expect("should have a valid entry");
908 assert_eq!(data2, actual);
909
910 assert_eq!(1, persisted_queue.take_entries_dropped());
911 assert_eq!(0, files_in_dir(&root_path).await);
912 }
913
914 #[tokio::test]
915 async fn pop_returns_none_when_all_entries_corrupt() {
916 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
917 let root_path = temp_dir.path().to_path_buf();
918
919 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
920 root_path: root_path.clone(),
921 max_on_disk_bytes: 1024,
922 storage_max_disk_ratio: 0.8,
923 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
924 max_age_days: 10,
925 })
926 .await
927 .expect("should not fail to create persisted queue");
928
929 write_corrupt_entry(&root_path).await;
931 persisted_queue.refresh_entry_state().await.unwrap();
932
933 let result = persisted_queue.pop().await.expect("should not fail to pop data");
935 assert!(result.is_none());
936
937 assert_eq!(1, persisted_queue.take_entries_dropped());
938 assert_eq!(0, files_in_dir(&root_path).await);
939 }
940
941 #[tokio::test]
942 async fn corrupt_entry_dropped_during_eviction() {
943 let data = FakeData::random();
944
945 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
946 let root_path = temp_dir.path().to_path_buf();
947
948 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
950 root_path: root_path.clone(),
951 max_on_disk_bytes: 32,
952 storage_max_disk_ratio: 0.8,
953 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
954 max_age_days: 10,
955 })
956 .await
957 .expect("should not fail to create persisted queue");
958
959 let _ = persisted_queue
961 .push(FakeData::random())
962 .await
963 .expect("should not fail to push data");
964 let first_path = persisted_queue.entries[0].path.clone();
965 tokio::fs::write(&first_path, b"corrupted").await.unwrap();
966
967 let _ = persisted_queue
970 .push(data.clone())
971 .await
972 .expect("should not fail to push data");
973
974 assert_eq!(1, persisted_queue.take_entries_dropped());
976
977 let actual = persisted_queue
979 .pop()
980 .await
981 .expect("should not fail to pop data")
982 .expect("should have a valid entry");
983 assert_eq!(data, actual);
984 assert_eq!(0, files_in_dir(&root_path).await);
985 }
986
987 #[tokio::test]
988 async fn persisted_queue_removes_outdated_files_on_initialization() {
989 let data = FakeData::random();
990
991 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
992 let root_path = temp_dir.path().to_path_buf();
993
994 let stale_content = serde_json::to_vec(&data).unwrap();
997 tokio::fs::write(
998 root_path.join("retry-20000101000000000000000-100000000.json"),
999 &stale_content,
1000 )
1001 .await
1002 .unwrap();
1003
1004 assert_eq!(1, files_in_dir(&root_path).await);
1005
1006 let mut queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
1008 root_path: root_path.clone(),
1009 max_on_disk_bytes: 1024 * 1024,
1010 storage_max_disk_ratio: 0.8,
1011 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
1012 max_age_days: 10,
1013 })
1014 .await
1015 .expect("should not fail to create persisted queue");
1016 queue
1017 .remove_stale_files()
1018 .await
1019 .expect("should not fail to remove stale files");
1020
1021 assert_eq!(0, files_in_dir(&root_path).await);
1022 assert!(queue.is_empty());
1023 }
1024
1025 #[tokio::test]
1026 async fn persisted_queue_zero_age_removes_all_retry_files_on_initialization() {
1027 let data = FakeData::random();
1030
1031 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
1032 let root_path = temp_dir.path().to_path_buf();
1033
1034 let mut seeding_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
1036 root_path: root_path.clone(),
1037 max_on_disk_bytes: 1024 * 1024,
1038 storage_max_disk_ratio: 0.8,
1039 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
1040 max_age_days: 10,
1041 })
1042 .await
1043 .expect("should not fail to create persisted queue");
1044 let _ = seeding_queue.push(data).await.expect("should not fail to push data");
1045 assert_eq!(1, files_in_dir(&root_path).await);
1046
1047 let mut queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
1049 root_path: root_path.clone(),
1050 max_on_disk_bytes: 1024 * 1024,
1051 storage_max_disk_ratio: 0.8,
1052 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
1053 max_age_days: 0,
1054 })
1055 .await
1056 .expect("should not fail to create persisted queue");
1057 queue
1058 .remove_stale_files()
1059 .await
1060 .expect("should not fail to remove stale files");
1061
1062 assert_eq!(0, files_in_dir(&root_path).await);
1063 assert!(queue.is_empty());
1064 }
1065}