Skip to main content

cd_da_reader/
retry.rs

1use std::time::Duration;
2
3/// Retry policy for failed drive reads.
4///
5/// Track and sector-range reads are split into chunks, and this policy is
6/// applied independently to each chunk. If a chunk read fails, the next
7/// attempt starts at the same LBA. Retry delays use capped exponential backoff.
8/// When adaptive chunk reduction is enabled, retries request fewer sectors from
9/// that LBA, down to the configured minimum. Chunks that were already read
10/// successfully are not repeated.
11///
12/// The default values are suitable for most drives. Unless you have specific
13/// requirements, using [`RetryConfig::default`] is recommended.
14///
15/// The default policy uses:
16///
17/// - 4 attempts, including the initial read;
18/// - a 20 ms initial backoff;
19/// - a 300 ms maximum backoff;
20/// - adaptive chunk reduction;
21/// - a minimum chunk size of 1 sector.
22#[derive(Debug, Clone)]
23pub struct RetryConfig {
24    pub(crate) max_attempts: u8,
25    pub(crate) initial_backoff: Duration,
26    pub(crate) max_backoff: Duration,
27    pub(crate) reduce_chunk_on_retry: bool,
28    pub(crate) min_sectors_per_read: u32,
29}
30
31impl RetryConfig {
32    /// Set the maximum attempts per chunk, including the initial read.
33    ///
34    /// A value of zero is normalized to one attempt.
35    pub fn with_max_attempts(mut self, attempts: u8) -> Self {
36        self.max_attempts = attempts.max(1);
37        self
38    }
39
40    /// Set the delay before the second attempt.
41    ///
42    /// The first attempt is always immediate.
43    pub fn with_initial_backoff(mut self, backoff: Duration) -> Self {
44        self.initial_backoff = backoff;
45        self
46    }
47
48    /// Set the upper bound for exponential backoff delays.
49    ///
50    /// A duration of zero disables retry delays.
51    pub fn with_max_backoff(mut self, backoff: Duration) -> Self {
52        self.max_backoff = backoff;
53        self
54    }
55
56    /// Enable or disable requesting fewer sectors after a failed chunk read.
57    pub fn with_chunk_reduction(mut self, enabled: bool) -> Self {
58        self.reduce_chunk_on_retry = enabled;
59        self
60    }
61
62    /// Set the minimum sectors per command when adaptive reduction is enabled.
63    ///
64    /// A value of zero is normalized to one sector.
65    pub fn with_min_sectors_per_read(mut self, sectors: u32) -> Self {
66        self.min_sectors_per_read = sectors.max(1);
67        self
68    }
69}
70
71impl Default for RetryConfig {
72    fn default() -> Self {
73        Self {
74            max_attempts: 4,
75            initial_backoff: Duration::from_millis(20),
76            max_backoff: Duration::from_millis(300),
77            reduce_chunk_on_retry: true,
78            min_sectors_per_read: 1,
79        }
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn default_policy_matches_documented_values() {
89        let config = RetryConfig::default();
90
91        assert_eq!(config.max_attempts, 4);
92        assert_eq!(config.initial_backoff, Duration::from_millis(20));
93        assert_eq!(config.max_backoff, Duration::from_millis(300));
94        assert!(config.reduce_chunk_on_retry);
95        assert_eq!(config.min_sectors_per_read, 1);
96    }
97
98    #[test]
99    fn builders_override_and_normalize_values() {
100        let config = RetryConfig::default()
101            .with_max_attempts(0)
102            .with_initial_backoff(Duration::from_millis(50))
103            .with_max_backoff(Duration::from_secs(1))
104            .with_chunk_reduction(false)
105            .with_min_sectors_per_read(0);
106
107        assert_eq!(config.max_attempts, 1);
108        assert_eq!(config.initial_backoff, Duration::from_millis(50));
109        assert_eq!(config.max_backoff, Duration::from_secs(1));
110        assert!(!config.reduce_chunk_on_retry);
111        assert_eq!(config.min_sectors_per_read, 1);
112    }
113}