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, error, 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 debug!(entry.len = serialized.len(), "Enqueued persisted entry.");
258
259 Ok(push_result)
260 }
261
262 pub async fn pop(&mut self) -> Result<Option<T>, GenericError> {
268 loop {
269 if self.entries.is_empty() {
270 return Ok(None);
271 }
272
273 let entry = self.entries.remove(0);
274 match try_deserialize_entry(&entry).await {
275 Ok(Some(deserialized)) => {
276 self.total_on_disk_bytes -= entry.size_bytes;
278 debug!(entry.len = entry.size_bytes, "Dequeued persisted entry.");
279
280 return Ok(Some(deserialized));
281 }
282 Ok(None) => {
283 self.refresh_entry_state().await?;
286 continue;
287 }
288 Err(e) => {
289 warn!(
292 entry.path = %entry.path.display(),
293 entry.len = entry.size_bytes,
294 error = %e,
295 "Permanently dropping persisted entry that could not be consumed.",
296 );
297
298 self.total_on_disk_bytes -= entry.size_bytes;
299 self.entries_dropped += 1;
300
301 continue;
302 }
303 }
304 }
305 }
306
307 async fn refresh_entry_state(&mut self) -> io::Result<()> {
308 let mut entries = Vec::new();
310
311 let mut dir_reader = tokio::fs::read_dir(&self.root_path).await?;
312 while let Some(entry) = dir_reader.next_entry().await? {
313 let metadata = entry.metadata().await?;
314 if metadata.is_file() {
315 match PersistedEntry::try_from_path(entry.path(), metadata.len()) {
316 Some(entry) => entries.push(entry),
317 None => {
318 warn!(
319 file_size = metadata.len(),
320 "Ignoring unrecognized file '{}' in retry directory.",
321 entry.path().display()
322 );
323 continue;
324 }
325 }
326 }
327 }
328
329 entries.sort_by_key(|entry| entry.timestamp);
331 self.total_on_disk_bytes = entries.iter().map(|entry| entry.size_bytes).sum();
332 self.entries = entries;
333
334 Ok(())
335 }
336
337 async fn remove_until_available_space(&mut self, required_bytes: u64) -> Result<PushResult, GenericError> {
344 let mut push_result = PushResult::default();
345
346 let disk_usage_retriever = self.disk_usage_retriever.clone();
347 let storage_max_disk_ratio = self.storage_max_disk_ratio;
348 let max_on_disk_bytes = self.max_on_disk_bytes;
349
350 let limit = tokio::task::spawn_blocking(move || {
357 on_disk_bytes_limit(disk_usage_retriever, storage_max_disk_ratio, max_on_disk_bytes)
358 })
359 .await
360 .error_context("Failed to run disk size limit check to completion.")??;
361
362 while !self.entries.is_empty() && self.total_on_disk_bytes + required_bytes > limit {
363 let entry = self.entries.remove(0);
364
365 let deserialized = match try_deserialize_entry::<T>(&entry).await {
367 Ok(Some(deserialized)) => deserialized,
368 Ok(None) => {
369 warn!(entry.path = %entry.path.display(), "Failed to find entry on disk. Persisted entry state may be inconsistent.");
370 continue;
371 }
372 Err(e) => {
373 warn!(
376 entry.path = %entry.path.display(),
377 entry.len = entry.size_bytes,
378 error = %e,
379 "Permanently dropping persisted entry that could not be consumed during eviction.",
380 );
381
382 self.total_on_disk_bytes -= entry.size_bytes;
383 self.entries_dropped += 1;
384
385 continue;
386 }
387 };
388
389 self.total_on_disk_bytes -= entry.size_bytes;
391 push_result.track_dropped_item(&deserialized);
392
393 error!(
397 entry.path = %entry.path.display(),
398 entry.len = entry.size_bytes,
399 "Maximum disk space for retry transactions is reached. Removing {}.",
400 entry.path.display()
401 );
402 }
403
404 Ok(push_result)
405 }
406}
407
408fn on_disk_bytes_limit(
416 disk_usage_retriever: DiskUsageRetrieverWrapper, storage_max_disk_ratio: f64, max_on_disk_bytes: u64,
417) -> Result<u64, GenericError> {
418 let total_space = disk_usage_retriever.inner.total_space()? as f64;
419 let available_space = disk_usage_retriever.inner.available_space()? as f64;
420 let disk_reserved = total_space * (1.0 - storage_max_disk_ratio);
421 let available_disk_usage = (available_space - disk_reserved).ceil() as u64;
422 Ok(max_on_disk_bytes.min(available_disk_usage))
423}
424
425async fn try_deserialize_entry<T: DeserializeOwned>(entry: &PersistedEntry) -> Result<Option<T>, GenericError> {
426 let serialized = match tokio::fs::read(&entry.path).await {
427 Ok(serialized) => serialized,
428 Err(e) => match e.kind() {
429 io::ErrorKind::NotFound => {
430 return Ok(None);
436 }
437 _ => {
438 return Err(e)
439 .with_error_context(|| format!("Failed to read persisted entry '{}'.", entry.path.display()))
440 }
441 },
442 };
443
444 let deserialized = match serde_json::from_slice(&serialized) {
445 Ok(deserialized) => deserialized,
446 Err(e) => {
447 if let Err(remove_err) = tokio::fs::remove_file(&entry.path).await {
450 warn!(
451 entry.path = %entry.path.display(),
452 error = %remove_err,
453 "Failed to remove corrupt persisted entry from disk.",
454 );
455 }
456
457 return Err(e)
458 .with_error_context(|| format!("Failed to deserialize persisted entry '{}'.", entry.path.display()));
459 }
460 };
461
462 tokio::fs::remove_file(&entry.path)
464 .await
465 .with_error_context(|| format!("Failed to delete persisted entry '{}'.", entry.path.display()))?;
466
467 debug!(entry.path = %entry.path.display(), entry.len = entry.size_bytes, "Consumed persisted entry and removed from disk.");
468 Ok(Some(deserialized))
469}
470
471fn generate_timestamped_filename() -> (PathBuf, u128) {
472 let now = Utc::now();
473 let now_ts = datetime_to_timestamp(now);
474 let nonce = rand::rng().random_range(100000000..999999999);
475
476 let filename = format!("retry-{}-{}.json", now.format("%Y%m%d%H%M%S%f"), nonce).into();
477
478 (filename, now_ts)
479}
480
481fn decode_timestamped_filename(path: &Path) -> Option<u128> {
482 let filename = path.file_stem()?.to_str()?;
483 let mut filename_parts = filename.split('-');
484
485 let prefix = filename_parts.next()?;
486 let timestamp_str = filename_parts.next()?;
487 let nonce = filename_parts.next()?;
488
489 if prefix != "retry" || nonce.parse::<u64>().is_err() {
491 return None;
492 }
493
494 NaiveDateTime::parse_from_str(timestamp_str, "%Y%m%d%H%M%S%f")
496 .map(|dt| datetime_to_timestamp(dt.and_utc()))
497 .ok()
498}
499
500fn datetime_to_timestamp(dt: DateTime<Utc>) -> u128 {
501 let secs = (dt.timestamp() as u128) * 1_000_000_000;
502 let ns = dt.timestamp_subsec_nanos() as u128;
503
504 secs + ns
505}
506
507async fn create_directory_recursive(path: PathBuf) -> Result<(), GenericError> {
508 let mut dir_builder = std::fs::DirBuilder::new();
509 dir_builder.recursive(true);
510
511 #[cfg(unix)]
514 {
515 use std::os::unix::fs::DirBuilderExt;
516 dir_builder.mode(0o700);
517 }
518
519 tokio::task::spawn_blocking(move || {
520 dir_builder
521 .create(&path)
522 .with_error_context(|| format!("Failed to create directory '{}'.", path.display()))
523 })
524 .await
525 .error_context("Failed to spawn directory creation blocking task.")?
526}
527
528async fn remove_outdated_retry_files(queue_path: &Path, max_age_days: u32) -> Result<u32, GenericError> {
534 let mut dir = match tokio::fs::read_dir(queue_path).await {
535 Ok(d) => d,
536 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(0),
537 Err(e) => {
538 return Err(e).with_error_context(|| {
539 format!(
540 "Failed to open retry queue directory '{}' for age-based cleanup.",
541 queue_path.display()
542 )
543 });
544 }
545 };
546 let now_ns = std::time::SystemTime::now()
547 .duration_since(std::time::UNIX_EPOCH)
548 .unwrap_or_default() .as_nanos();
550 let cutoff_ns = now_ns.saturating_sub(max_age_days as u128 * 24 * 3600 * 1_000_000_000);
551 let mut removed = 0u32;
552 loop {
553 let entry = match dir.next_entry().await {
554 Ok(Some(e)) => e,
555 Ok(None) => break,
556 Err(e) => {
557 return Err(e).with_error_context(|| "Error reading retry queue directory during age-based cleanup.");
558 }
559 };
560 let file_ts = match decode_timestamped_filename(&entry.path()) {
561 Some(ts) => ts,
562 None => continue,
563 };
564 if file_ts < cutoff_ns {
565 let name_str = entry.file_name();
566 let name = name_str.to_string_lossy();
567 match tokio::fs::remove_file(entry.path()).await {
568 Ok(()) => {
569 debug!(file = %name, "Removed outdated retry file.");
570 removed += 1;
571 }
572 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
573 debug!(file = %name, "Retry file already removed by concurrent cleanup.");
574 }
575 Err(e) => {
576 warn!(file = %name, error = %e, "Failed to remove outdated retry file.");
577 }
578 }
579 }
580 }
581 Ok(removed)
582}
583
584#[cfg(test)]
585mod tests {
586 use rand::RngExt as _;
587 use rand_distr::Alphanumeric;
588 use serde::Deserialize;
589
590 use super::*;
591
592 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
593 struct FakeData {
594 name: String,
595 value: u32,
596 }
597
598 impl FakeData {
599 fn random() -> Self {
600 Self {
601 name: rand::rng().sample_iter(&Alphanumeric).take(8).map(char::from).collect(),
602 value: rand::rng().random_range(0..100),
603 }
604 }
605 }
606
607 impl EventContainer for FakeData {
608 fn event_count(&self) -> u64 {
609 1
610 }
611 }
612
613 struct MockDiskUsageRetriever {}
614
615 impl DiskUsageRetriever for MockDiskUsageRetriever {
616 fn total_space(&self) -> Result<u64, GenericError> {
617 Ok(100)
618 }
619 fn available_space(&self) -> Result<u64, GenericError> {
620 Ok(100)
621 }
622 }
623
624 async fn files_in_dir(path: &Path) -> usize {
625 let mut file_count = 0;
626 let mut dir_reader = tokio::fs::read_dir(path).await.unwrap();
627 while let Some(entry) = dir_reader.next_entry().await.unwrap() {
628 if entry.metadata().await.unwrap().is_file() {
629 file_count += 1;
630 }
631 }
632 file_count
633 }
634
635 #[tokio::test]
636 async fn basic_push_pop() {
637 let data = FakeData::random();
638
639 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
641 let root_path = temp_dir.path().to_path_buf();
642
643 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
644 root_path: root_path.clone(),
645 max_on_disk_bytes: 1024,
646 storage_max_disk_ratio: 0.8,
647 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
648 max_age_days: 10,
649 })
650 .await
651 .expect("should not fail to create persisted queue");
652
653 assert_eq!(0, files_in_dir(&root_path).await);
655
656 let push_result = persisted_queue
658 .push(data.clone())
659 .await
660 .expect("should not fail to push data");
661 assert_eq!(1, files_in_dir(&root_path).await);
662 assert_eq!(0, push_result.items_dropped);
663 assert_eq!(0, push_result.events_dropped);
664
665 let actual = persisted_queue
667 .pop()
668 .await
669 .expect("should not fail to pop data")
670 .expect("should not be empty");
671 assert_eq!(data, actual);
672 assert_eq!(0, files_in_dir(&root_path).await);
673 }
674
675 #[tokio::test]
676 async fn entry_too_large() {
677 let data = FakeData::random();
678
679 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
681 let root_path = temp_dir.path().to_path_buf();
682
683 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
684 root_path: root_path.clone(),
685 max_on_disk_bytes: 1,
686 storage_max_disk_ratio: 0.8,
687 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
688 max_age_days: 10,
689 })
690 .await
691 .expect("should not fail to create persisted queue");
692
693 assert_eq!(0, files_in_dir(&root_path).await);
695
696 assert!(persisted_queue.push(data).await.is_err());
698
699 assert_eq!(0, files_in_dir(&root_path).await);
701 }
702
703 #[tokio::test]
704 async fn remove_oldest_entry_on_push() {
705 let data1 = FakeData::random();
706 let data2 = FakeData::random();
707
708 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
712 let root_path = temp_dir.path().to_path_buf();
713
714 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
715 root_path: root_path.clone(),
716 max_on_disk_bytes: 32,
717 storage_max_disk_ratio: 0.8,
718 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
719 max_age_days: 10,
720 })
721 .await
722 .expect("should not fail to create persisted queue");
723
724 assert_eq!(0, files_in_dir(&root_path).await);
726
727 let push_result = persisted_queue.push(data1).await.expect("should not fail to push data");
729 assert_eq!(1, files_in_dir(&root_path).await);
730 assert_eq!(0, push_result.items_dropped);
731 assert_eq!(0, push_result.events_dropped);
732
733 let push_result = persisted_queue
735 .push(data2.clone())
736 .await
737 .expect("should not fail to push data");
738 assert_eq!(1, files_in_dir(&root_path).await);
739 assert_eq!(1, push_result.items_dropped);
740 assert_eq!(1, push_result.events_dropped);
741
742 let actual = persisted_queue
745 .pop()
746 .await
747 .expect("should not fail to pop data")
748 .expect("should not be empty");
749 assert_eq!(data2, actual);
750 assert_eq!(0, files_in_dir(&root_path).await);
751 }
752
753 #[tokio::test]
754 async fn storage_ratio_exceeded() {
755 let data1 = FakeData::random();
756 let data2 = FakeData::random();
757
758 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
762 let root_path = temp_dir.path().to_path_buf();
763
764 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
765 root_path: root_path.clone(),
766 max_on_disk_bytes: 80,
767 storage_max_disk_ratio: 0.35,
768 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
769 max_age_days: 10,
770 })
771 .await
772 .expect("should not fail to create persisted queue");
773
774 assert_eq!(0, files_in_dir(&root_path).await);
776
777 let push_result = persisted_queue.push(data1).await.expect("should not fail to push data");
782
783 assert_eq!(1, files_in_dir(&root_path).await);
784 assert_eq!(0, push_result.items_dropped);
785 assert_eq!(0, push_result.events_dropped);
786
787 let push_result = persisted_queue
789 .push(data2.clone())
790 .await
791 .expect("should not fail to push data");
792 assert_eq!(1, files_in_dir(&root_path).await);
793 assert_eq!(1, push_result.items_dropped);
794 assert_eq!(1, push_result.events_dropped);
795
796 let actual = persisted_queue
799 .pop()
800 .await
801 .expect("should not fail to pop data")
802 .expect("should not be empty");
803 assert_eq!(data2, actual);
804 assert_eq!(0, files_in_dir(&root_path).await);
805 }
806
807 async fn write_corrupt_entry(dir: &Path) -> PathBuf {
810 let filename = "retry-20000101000000000000-100000000.json";
811 let path = dir.join(filename);
812 tokio::fs::write(&path, b"this is not valid json").await.unwrap();
813 path
814 }
815
816 #[tokio::test]
817 async fn corrupt_entry_is_skipped_on_pop() {
818 let data = FakeData::random();
819
820 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
821 let root_path = temp_dir.path().to_path_buf();
822
823 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
824 root_path: root_path.clone(),
825 max_on_disk_bytes: 1024,
826 storage_max_disk_ratio: 0.8,
827 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
828 max_age_days: 10,
829 })
830 .await
831 .expect("should not fail to create persisted queue");
832
833 let corrupt_path = write_corrupt_entry(&root_path).await;
835
836 let _ = persisted_queue
838 .push(data.clone())
839 .await
840 .expect("should not fail to push data");
841
842 persisted_queue.refresh_entry_state().await.unwrap();
844
845 let actual = persisted_queue
847 .pop()
848 .await
849 .expect("should not fail to pop data")
850 .expect("should have a valid entry");
851 assert_eq!(data, actual);
852
853 assert!(!corrupt_path.exists());
855
856 assert_eq!(1, persisted_queue.take_entries_dropped());
858
859 assert_eq!(0, files_in_dir(&root_path).await);
861 }
862
863 #[tokio::test]
864 async fn corrupt_entry_does_not_block_queue() {
865 let data1 = FakeData::random();
866 let data2 = FakeData::random();
867
868 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
869 let root_path = temp_dir.path().to_path_buf();
870
871 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
873 root_path: root_path.clone(),
874 max_on_disk_bytes: 1024,
875 storage_max_disk_ratio: 0.8,
876 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
877 max_age_days: 10,
878 })
879 .await
880 .expect("should not fail to create persisted queue");
881
882 let _ = persisted_queue.push(data1).await.expect("should not fail to push data");
884 let _ = persisted_queue
885 .push(data2.clone())
886 .await
887 .expect("should not fail to push data");
888 assert_eq!(2, persisted_queue.entries.len());
889
890 let oldest_path = persisted_queue.entries[0].path.clone();
892 tokio::fs::write(&oldest_path, b"corrupted").await.unwrap();
893
894 let actual = persisted_queue
896 .pop()
897 .await
898 .expect("should not fail to pop data")
899 .expect("should have a valid entry");
900 assert_eq!(data2, actual);
901
902 assert_eq!(1, persisted_queue.take_entries_dropped());
903 assert_eq!(0, files_in_dir(&root_path).await);
904 }
905
906 #[tokio::test]
907 async fn pop_returns_none_when_all_entries_corrupt() {
908 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
909 let root_path = temp_dir.path().to_path_buf();
910
911 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
912 root_path: root_path.clone(),
913 max_on_disk_bytes: 1024,
914 storage_max_disk_ratio: 0.8,
915 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
916 max_age_days: 10,
917 })
918 .await
919 .expect("should not fail to create persisted queue");
920
921 write_corrupt_entry(&root_path).await;
923 persisted_queue.refresh_entry_state().await.unwrap();
924
925 let result = persisted_queue.pop().await.expect("should not fail to pop data");
927 assert!(result.is_none());
928
929 assert_eq!(1, persisted_queue.take_entries_dropped());
930 assert_eq!(0, files_in_dir(&root_path).await);
931 }
932
933 #[tokio::test]
934 async fn corrupt_entry_dropped_during_eviction() {
935 let data = FakeData::random();
936
937 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
938 let root_path = temp_dir.path().to_path_buf();
939
940 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
942 root_path: root_path.clone(),
943 max_on_disk_bytes: 32,
944 storage_max_disk_ratio: 0.8,
945 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
946 max_age_days: 10,
947 })
948 .await
949 .expect("should not fail to create persisted queue");
950
951 let _ = persisted_queue
953 .push(FakeData::random())
954 .await
955 .expect("should not fail to push data");
956 let first_path = persisted_queue.entries[0].path.clone();
957 tokio::fs::write(&first_path, b"corrupted").await.unwrap();
958
959 let _ = persisted_queue
962 .push(data.clone())
963 .await
964 .expect("should not fail to push data");
965
966 assert_eq!(1, persisted_queue.take_entries_dropped());
968
969 let actual = persisted_queue
971 .pop()
972 .await
973 .expect("should not fail to pop data")
974 .expect("should have a valid entry");
975 assert_eq!(data, actual);
976 assert_eq!(0, files_in_dir(&root_path).await);
977 }
978
979 #[tokio::test]
980 async fn persisted_queue_removes_outdated_files_on_initialization() {
981 let data = FakeData::random();
982
983 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
984 let root_path = temp_dir.path().to_path_buf();
985
986 let stale_content = serde_json::to_vec(&data).unwrap();
989 tokio::fs::write(
990 root_path.join("retry-20000101000000000000000-100000000.json"),
991 &stale_content,
992 )
993 .await
994 .unwrap();
995
996 assert_eq!(1, files_in_dir(&root_path).await);
997
998 let mut queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
1000 root_path: root_path.clone(),
1001 max_on_disk_bytes: 1024 * 1024,
1002 storage_max_disk_ratio: 0.8,
1003 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
1004 max_age_days: 10,
1005 })
1006 .await
1007 .expect("should not fail to create persisted queue");
1008 queue
1009 .remove_stale_files()
1010 .await
1011 .expect("should not fail to remove stale files");
1012
1013 assert_eq!(0, files_in_dir(&root_path).await);
1014 assert!(queue.is_empty());
1015 }
1016
1017 #[tokio::test]
1018 async fn persisted_queue_zero_age_removes_all_retry_files_on_initialization() {
1019 let data = FakeData::random();
1022
1023 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
1024 let root_path = temp_dir.path().to_path_buf();
1025
1026 let mut seeding_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
1028 root_path: root_path.clone(),
1029 max_on_disk_bytes: 1024 * 1024,
1030 storage_max_disk_ratio: 0.8,
1031 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
1032 max_age_days: 10,
1033 })
1034 .await
1035 .expect("should not fail to create persisted queue");
1036 let _ = seeding_queue.push(data).await.expect("should not fail to push data");
1037 assert_eq!(1, files_in_dir(&root_path).await);
1038
1039 let mut queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
1041 root_path: root_path.clone(),
1042 max_on_disk_bytes: 1024 * 1024,
1043 storage_max_disk_ratio: 0.8,
1044 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
1045 max_age_days: 0,
1046 })
1047 .await
1048 .expect("should not fail to create persisted queue");
1049 queue
1050 .remove_stale_files()
1051 .await
1052 .expect("should not fail to remove stale files");
1053
1054 assert_eq!(0, files_in_dir(&root_path).await);
1055 assert!(queue.is_empty());
1056 }
1057}