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 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 warn!(entry.path = %entry.path.display(), entry.len = entry.size_bytes, "Dropped persisted entry.");
394 }
395
396 Ok(push_result)
397 }
398}
399
400fn on_disk_bytes_limit(
408 disk_usage_retriever: DiskUsageRetrieverWrapper, storage_max_disk_ratio: f64, max_on_disk_bytes: u64,
409) -> Result<u64, GenericError> {
410 let total_space = disk_usage_retriever.inner.total_space()? as f64;
411 let available_space = disk_usage_retriever.inner.available_space()? as f64;
412 let disk_reserved = total_space * (1.0 - storage_max_disk_ratio);
413 let available_disk_usage = (available_space - disk_reserved).ceil() as u64;
414 Ok(max_on_disk_bytes.min(available_disk_usage))
415}
416
417async fn try_deserialize_entry<T: DeserializeOwned>(entry: &PersistedEntry) -> Result<Option<T>, GenericError> {
418 let serialized = match tokio::fs::read(&entry.path).await {
419 Ok(serialized) => serialized,
420 Err(e) => match e.kind() {
421 io::ErrorKind::NotFound => {
422 return Ok(None);
428 }
429 _ => {
430 return Err(e)
431 .with_error_context(|| format!("Failed to read persisted entry '{}'.", entry.path.display()))
432 }
433 },
434 };
435
436 let deserialized = match serde_json::from_slice(&serialized) {
437 Ok(deserialized) => deserialized,
438 Err(e) => {
439 if let Err(remove_err) = tokio::fs::remove_file(&entry.path).await {
442 warn!(
443 entry.path = %entry.path.display(),
444 error = %remove_err,
445 "Failed to remove corrupt persisted entry from disk.",
446 );
447 }
448
449 return Err(e)
450 .with_error_context(|| format!("Failed to deserialize persisted entry '{}'.", entry.path.display()));
451 }
452 };
453
454 tokio::fs::remove_file(&entry.path)
456 .await
457 .with_error_context(|| format!("Failed to delete persisted entry '{}'.", entry.path.display()))?;
458
459 debug!(entry.path = %entry.path.display(), entry.len = entry.size_bytes, "Consumed persisted entry and removed from disk.");
460 Ok(Some(deserialized))
461}
462
463fn generate_timestamped_filename() -> (PathBuf, u128) {
464 let now = Utc::now();
465 let now_ts = datetime_to_timestamp(now);
466 let nonce = rand::rng().random_range(100000000..999999999);
467
468 let filename = format!("retry-{}-{}.json", now.format("%Y%m%d%H%M%S%f"), nonce).into();
469
470 (filename, now_ts)
471}
472
473fn decode_timestamped_filename(path: &Path) -> Option<u128> {
474 let filename = path.file_stem()?.to_str()?;
475 let mut filename_parts = filename.split('-');
476
477 let prefix = filename_parts.next()?;
478 let timestamp_str = filename_parts.next()?;
479 let nonce = filename_parts.next()?;
480
481 if prefix != "retry" || nonce.parse::<u64>().is_err() {
483 return None;
484 }
485
486 NaiveDateTime::parse_from_str(timestamp_str, "%Y%m%d%H%M%S%f")
488 .map(|dt| datetime_to_timestamp(dt.and_utc()))
489 .ok()
490}
491
492fn datetime_to_timestamp(dt: DateTime<Utc>) -> u128 {
493 let secs = (dt.timestamp() as u128) * 1_000_000_000;
494 let ns = dt.timestamp_subsec_nanos() as u128;
495
496 secs + ns
497}
498
499async fn create_directory_recursive(path: PathBuf) -> Result<(), GenericError> {
500 let mut dir_builder = std::fs::DirBuilder::new();
501 dir_builder.recursive(true);
502
503 #[cfg(unix)]
506 {
507 use std::os::unix::fs::DirBuilderExt;
508 dir_builder.mode(0o700);
509 }
510
511 tokio::task::spawn_blocking(move || {
512 dir_builder
513 .create(&path)
514 .with_error_context(|| format!("Failed to create directory '{}'.", path.display()))
515 })
516 .await
517 .error_context("Failed to spawn directory creation blocking task.")?
518}
519
520async fn remove_outdated_retry_files(queue_path: &Path, max_age_days: u32) -> Result<u32, GenericError> {
526 let mut dir = match tokio::fs::read_dir(queue_path).await {
527 Ok(d) => d,
528 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(0),
529 Err(e) => {
530 return Err(e).with_error_context(|| {
531 format!(
532 "Failed to open retry queue directory '{}' for age-based cleanup.",
533 queue_path.display()
534 )
535 });
536 }
537 };
538 let now_ns = std::time::SystemTime::now()
539 .duration_since(std::time::UNIX_EPOCH)
540 .unwrap_or_default() .as_nanos();
542 let cutoff_ns = now_ns.saturating_sub(max_age_days as u128 * 24 * 3600 * 1_000_000_000);
543 let mut removed = 0u32;
544 loop {
545 let entry = match dir.next_entry().await {
546 Ok(Some(e)) => e,
547 Ok(None) => break,
548 Err(e) => {
549 return Err(e).with_error_context(|| "Error reading retry queue directory during age-based cleanup.");
550 }
551 };
552 let file_ts = match decode_timestamped_filename(&entry.path()) {
553 Some(ts) => ts,
554 None => continue,
555 };
556 if file_ts < cutoff_ns {
557 let name_str = entry.file_name();
558 let name = name_str.to_string_lossy();
559 match tokio::fs::remove_file(entry.path()).await {
560 Ok(()) => {
561 debug!(file = %name, "Removed outdated retry file.");
562 removed += 1;
563 }
564 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
565 debug!(file = %name, "Retry file already removed by concurrent cleanup.");
566 }
567 Err(e) => {
568 warn!(file = %name, error = %e, "Failed to remove outdated retry file.");
569 }
570 }
571 }
572 }
573 Ok(removed)
574}
575
576#[cfg(test)]
577mod tests {
578 use rand::RngExt as _;
579 use rand_distr::Alphanumeric;
580 use serde::Deserialize;
581
582 use super::*;
583
584 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
585 struct FakeData {
586 name: String,
587 value: u32,
588 }
589
590 impl FakeData {
591 fn random() -> Self {
592 Self {
593 name: rand::rng().sample_iter(&Alphanumeric).take(8).map(char::from).collect(),
594 value: rand::rng().random_range(0..100),
595 }
596 }
597 }
598
599 impl EventContainer for FakeData {
600 fn event_count(&self) -> u64 {
601 1
602 }
603 }
604
605 struct MockDiskUsageRetriever {}
606
607 impl DiskUsageRetriever for MockDiskUsageRetriever {
608 fn total_space(&self) -> Result<u64, GenericError> {
609 Ok(100)
610 }
611 fn available_space(&self) -> Result<u64, GenericError> {
612 Ok(100)
613 }
614 }
615
616 async fn files_in_dir(path: &Path) -> usize {
617 let mut file_count = 0;
618 let mut dir_reader = tokio::fs::read_dir(path).await.unwrap();
619 while let Some(entry) = dir_reader.next_entry().await.unwrap() {
620 if entry.metadata().await.unwrap().is_file() {
621 file_count += 1;
622 }
623 }
624 file_count
625 }
626
627 #[tokio::test]
628 async fn basic_push_pop() {
629 let data = FakeData::random();
630
631 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
633 let root_path = temp_dir.path().to_path_buf();
634
635 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
636 root_path: root_path.clone(),
637 max_on_disk_bytes: 1024,
638 storage_max_disk_ratio: 0.8,
639 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
640 max_age_days: 10,
641 })
642 .await
643 .expect("should not fail to create persisted queue");
644
645 assert_eq!(0, files_in_dir(&root_path).await);
647
648 let push_result = persisted_queue
650 .push(data.clone())
651 .await
652 .expect("should not fail to push data");
653 assert_eq!(1, files_in_dir(&root_path).await);
654 assert_eq!(0, push_result.items_dropped);
655 assert_eq!(0, push_result.events_dropped);
656
657 let actual = persisted_queue
659 .pop()
660 .await
661 .expect("should not fail to pop data")
662 .expect("should not be empty");
663 assert_eq!(data, actual);
664 assert_eq!(0, files_in_dir(&root_path).await);
665 }
666
667 #[tokio::test]
668 async fn entry_too_large() {
669 let data = FakeData::random();
670
671 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
673 let root_path = temp_dir.path().to_path_buf();
674
675 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
676 root_path: root_path.clone(),
677 max_on_disk_bytes: 1,
678 storage_max_disk_ratio: 0.8,
679 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
680 max_age_days: 10,
681 })
682 .await
683 .expect("should not fail to create persisted queue");
684
685 assert_eq!(0, files_in_dir(&root_path).await);
687
688 assert!(persisted_queue.push(data).await.is_err());
690
691 assert_eq!(0, files_in_dir(&root_path).await);
693 }
694
695 #[tokio::test]
696 async fn remove_oldest_entry_on_push() {
697 let data1 = FakeData::random();
698 let data2 = FakeData::random();
699
700 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
704 let root_path = temp_dir.path().to_path_buf();
705
706 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
707 root_path: root_path.clone(),
708 max_on_disk_bytes: 32,
709 storage_max_disk_ratio: 0.8,
710 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
711 max_age_days: 10,
712 })
713 .await
714 .expect("should not fail to create persisted queue");
715
716 assert_eq!(0, files_in_dir(&root_path).await);
718
719 let push_result = persisted_queue.push(data1).await.expect("should not fail to push data");
721 assert_eq!(1, files_in_dir(&root_path).await);
722 assert_eq!(0, push_result.items_dropped);
723 assert_eq!(0, push_result.events_dropped);
724
725 let push_result = persisted_queue
727 .push(data2.clone())
728 .await
729 .expect("should not fail to push data");
730 assert_eq!(1, files_in_dir(&root_path).await);
731 assert_eq!(1, push_result.items_dropped);
732 assert_eq!(1, push_result.events_dropped);
733
734 let actual = persisted_queue
737 .pop()
738 .await
739 .expect("should not fail to pop data")
740 .expect("should not be empty");
741 assert_eq!(data2, actual);
742 assert_eq!(0, files_in_dir(&root_path).await);
743 }
744
745 #[tokio::test]
746 async fn storage_ratio_exceeded() {
747 let data1 = FakeData::random();
748 let data2 = FakeData::random();
749
750 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
754 let root_path = temp_dir.path().to_path_buf();
755
756 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
757 root_path: root_path.clone(),
758 max_on_disk_bytes: 80,
759 storage_max_disk_ratio: 0.35,
760 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
761 max_age_days: 10,
762 })
763 .await
764 .expect("should not fail to create persisted queue");
765
766 assert_eq!(0, files_in_dir(&root_path).await);
768
769 let push_result = persisted_queue.push(data1).await.expect("should not fail to push data");
774
775 assert_eq!(1, files_in_dir(&root_path).await);
776 assert_eq!(0, push_result.items_dropped);
777 assert_eq!(0, push_result.events_dropped);
778
779 let push_result = persisted_queue
781 .push(data2.clone())
782 .await
783 .expect("should not fail to push data");
784 assert_eq!(1, files_in_dir(&root_path).await);
785 assert_eq!(1, push_result.items_dropped);
786 assert_eq!(1, push_result.events_dropped);
787
788 let actual = persisted_queue
791 .pop()
792 .await
793 .expect("should not fail to pop data")
794 .expect("should not be empty");
795 assert_eq!(data2, actual);
796 assert_eq!(0, files_in_dir(&root_path).await);
797 }
798
799 async fn write_corrupt_entry(dir: &Path) -> PathBuf {
802 let filename = "retry-20000101000000000000-100000000.json";
803 let path = dir.join(filename);
804 tokio::fs::write(&path, b"this is not valid json").await.unwrap();
805 path
806 }
807
808 #[tokio::test]
809 async fn corrupt_entry_is_skipped_on_pop() {
810 let data = FakeData::random();
811
812 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
813 let root_path = temp_dir.path().to_path_buf();
814
815 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
816 root_path: root_path.clone(),
817 max_on_disk_bytes: 1024,
818 storage_max_disk_ratio: 0.8,
819 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
820 max_age_days: 10,
821 })
822 .await
823 .expect("should not fail to create persisted queue");
824
825 let corrupt_path = write_corrupt_entry(&root_path).await;
827
828 let _ = persisted_queue
830 .push(data.clone())
831 .await
832 .expect("should not fail to push data");
833
834 persisted_queue.refresh_entry_state().await.unwrap();
836
837 let actual = persisted_queue
839 .pop()
840 .await
841 .expect("should not fail to pop data")
842 .expect("should have a valid entry");
843 assert_eq!(data, actual);
844
845 assert!(!corrupt_path.exists());
847
848 assert_eq!(1, persisted_queue.take_entries_dropped());
850
851 assert_eq!(0, files_in_dir(&root_path).await);
853 }
854
855 #[tokio::test]
856 async fn corrupt_entry_does_not_block_queue() {
857 let data1 = FakeData::random();
858 let data2 = FakeData::random();
859
860 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
861 let root_path = temp_dir.path().to_path_buf();
862
863 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
865 root_path: root_path.clone(),
866 max_on_disk_bytes: 1024,
867 storage_max_disk_ratio: 0.8,
868 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
869 max_age_days: 10,
870 })
871 .await
872 .expect("should not fail to create persisted queue");
873
874 let _ = persisted_queue.push(data1).await.expect("should not fail to push data");
876 let _ = persisted_queue
877 .push(data2.clone())
878 .await
879 .expect("should not fail to push data");
880 assert_eq!(2, persisted_queue.entries.len());
881
882 let oldest_path = persisted_queue.entries[0].path.clone();
884 tokio::fs::write(&oldest_path, b"corrupted").await.unwrap();
885
886 let actual = persisted_queue
888 .pop()
889 .await
890 .expect("should not fail to pop data")
891 .expect("should have a valid entry");
892 assert_eq!(data2, actual);
893
894 assert_eq!(1, persisted_queue.take_entries_dropped());
895 assert_eq!(0, files_in_dir(&root_path).await);
896 }
897
898 #[tokio::test]
899 async fn pop_returns_none_when_all_entries_corrupt() {
900 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
901 let root_path = temp_dir.path().to_path_buf();
902
903 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
904 root_path: root_path.clone(),
905 max_on_disk_bytes: 1024,
906 storage_max_disk_ratio: 0.8,
907 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
908 max_age_days: 10,
909 })
910 .await
911 .expect("should not fail to create persisted queue");
912
913 write_corrupt_entry(&root_path).await;
915 persisted_queue.refresh_entry_state().await.unwrap();
916
917 let result = persisted_queue.pop().await.expect("should not fail to pop data");
919 assert!(result.is_none());
920
921 assert_eq!(1, persisted_queue.take_entries_dropped());
922 assert_eq!(0, files_in_dir(&root_path).await);
923 }
924
925 #[tokio::test]
926 async fn corrupt_entry_dropped_during_eviction() {
927 let data = FakeData::random();
928
929 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
930 let root_path = temp_dir.path().to_path_buf();
931
932 let mut persisted_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
934 root_path: root_path.clone(),
935 max_on_disk_bytes: 32,
936 storage_max_disk_ratio: 0.8,
937 disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}),
938 max_age_days: 10,
939 })
940 .await
941 .expect("should not fail to create persisted queue");
942
943 let _ = persisted_queue
945 .push(FakeData::random())
946 .await
947 .expect("should not fail to push data");
948 let first_path = persisted_queue.entries[0].path.clone();
949 tokio::fs::write(&first_path, b"corrupted").await.unwrap();
950
951 let _ = persisted_queue
954 .push(data.clone())
955 .await
956 .expect("should not fail to push data");
957
958 assert_eq!(1, persisted_queue.take_entries_dropped());
960
961 let actual = persisted_queue
963 .pop()
964 .await
965 .expect("should not fail to pop data")
966 .expect("should have a valid entry");
967 assert_eq!(data, actual);
968 assert_eq!(0, files_in_dir(&root_path).await);
969 }
970
971 #[tokio::test]
972 async fn persisted_queue_removes_outdated_files_on_initialization() {
973 let data = FakeData::random();
974
975 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
976 let root_path = temp_dir.path().to_path_buf();
977
978 let stale_content = serde_json::to_vec(&data).unwrap();
981 tokio::fs::write(
982 root_path.join("retry-20000101000000000000000-100000000.json"),
983 &stale_content,
984 )
985 .await
986 .unwrap();
987
988 assert_eq!(1, files_in_dir(&root_path).await);
989
990 let mut queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
992 root_path: root_path.clone(),
993 max_on_disk_bytes: 1024 * 1024,
994 storage_max_disk_ratio: 0.8,
995 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
996 max_age_days: 10,
997 })
998 .await
999 .expect("should not fail to create persisted queue");
1000 queue
1001 .remove_stale_files()
1002 .await
1003 .expect("should not fail to remove stale files");
1004
1005 assert_eq!(0, files_in_dir(&root_path).await);
1006 assert!(queue.is_empty());
1007 }
1008
1009 #[tokio::test]
1010 async fn persisted_queue_zero_age_removes_all_retry_files_on_initialization() {
1011 let data = FakeData::random();
1014
1015 let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory");
1016 let root_path = temp_dir.path().to_path_buf();
1017
1018 let mut seeding_queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
1020 root_path: root_path.clone(),
1021 max_on_disk_bytes: 1024 * 1024,
1022 storage_max_disk_ratio: 0.8,
1023 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
1024 max_age_days: 10,
1025 })
1026 .await
1027 .expect("should not fail to create persisted queue");
1028 let _ = seeding_queue.push(data).await.expect("should not fail to push data");
1029 assert_eq!(1, files_in_dir(&root_path).await);
1030
1031 let mut queue = PersistedQueue::<FakeData>::from_root_path(PersistedQueueArgs {
1033 root_path: root_path.clone(),
1034 max_on_disk_bytes: 1024 * 1024,
1035 storage_max_disk_ratio: 0.8,
1036 disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())),
1037 max_age_days: 0,
1038 })
1039 .await
1040 .expect("should not fail to create persisted queue");
1041 queue
1042 .remove_stale_files()
1043 .await
1044 .expect("should not fail to remove stale files");
1045
1046 assert_eq!(0, files_in_dir(&root_path).await);
1047 assert!(queue.is_empty());
1048 }
1049}