Skip to main content

rusty_cat/binary/
binary_download_config.rs

1use std::time::Duration;
2
3use crate::error::{InnerErrorCode, MeowError};
4
5/// Default maximum response body retained by an in-memory binary task.
6pub const DEFAULT_BINARY_MAX_BODY_BYTES: u64 = 5 * 1024 * 1024;
7/// Hard safety ceiling for one in-memory response body.
8pub const BINARY_ABSOLUTE_MAX_BODY_BYTES: u64 = 64 * 1024 * 1024;
9pub(crate) const BINARY_MAX_RETRY_DELAYS: usize = 8;
10pub(crate) const BINARY_MAX_REDIRECTS: usize = 10;
11
12/// HTTP and memory limits for [`crate::api::BinaryTask`].
13///
14/// The configuration belongs to a [`crate::api::MeowConfig`] and is only used
15/// when the first binary task initializes its isolated executor.
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct BinaryDownloadConfig {
18    max_body_bytes: u64,
19    request_timeout: Option<Duration>,
20    tcp_keepalive: Option<Duration>,
21    redirect_limit: usize,
22    retry_delays: Vec<Duration>,
23}
24
25/// Builder for validated [`BinaryDownloadConfig`] values.
26#[derive(Clone, Debug)]
27pub struct BinaryDownloadConfigBuilder {
28    config: BinaryDownloadConfig,
29}
30
31impl Default for BinaryDownloadConfig {
32    fn default() -> Self {
33        Self {
34            max_body_bytes: DEFAULT_BINARY_MAX_BODY_BYTES,
35            request_timeout: None,
36            tcp_keepalive: None,
37            redirect_limit: 5,
38            retry_delays: vec![Duration::from_millis(300), Duration::from_millis(800)],
39        }
40    }
41}
42
43impl BinaryDownloadConfig {
44    /// Starts a builder from the safe defaults.
45    pub fn builder() -> BinaryDownloadConfigBuilder {
46        BinaryDownloadConfigBuilder {
47            config: Self::default(),
48        }
49    }
50
51    /// Maximum response body size in bytes.
52    pub fn max_body_bytes(&self) -> u64 {
53        self.max_body_bytes
54    }
55
56    /// Optional per-attempt timeout. `None` inherits `MeowConfig::http_timeout`.
57    pub fn request_timeout(&self) -> Option<Duration> {
58        self.request_timeout
59    }
60
61    /// Optional TCP keepalive. `None` inherits `MeowConfig::tcp_keepalive`.
62    pub fn tcp_keepalive(&self) -> Option<Duration> {
63        self.tcp_keepalive
64    }
65
66    /// Maximum number of redirects followed by one request.
67    pub fn redirect_limit(&self) -> usize {
68        self.redirect_limit
69    }
70
71    /// Delay before each retry. An empty slice disables retries.
72    pub fn retry_delays(&self) -> &[Duration] {
73        &self.retry_delays
74    }
75
76    pub(crate) fn validate(&self) -> Result<(), MeowError> {
77        if !(1..=BINARY_ABSOLUTE_MAX_BODY_BYTES).contains(&self.max_body_bytes) {
78            return Err(parameter_error(format!(
79                "binary max_body_bytes must be in 1..={BINARY_ABSOLUTE_MAX_BODY_BYTES}"
80            )));
81        }
82        if self.request_timeout.is_some_and(|value| value.is_zero()) {
83            return Err(parameter_error(
84                "binary request_timeout must be greater than zero",
85            ));
86        }
87        if self.tcp_keepalive.is_some_and(|value| value.is_zero()) {
88            return Err(parameter_error(
89                "binary tcp_keepalive must be greater than zero",
90            ));
91        }
92        if self.redirect_limit > BINARY_MAX_REDIRECTS {
93            return Err(parameter_error(format!(
94                "binary redirect_limit must be <= {BINARY_MAX_REDIRECTS}"
95            )));
96        }
97        if self.retry_delays.len() > BINARY_MAX_RETRY_DELAYS {
98            return Err(parameter_error(format!(
99                "binary retry_delays must contain at most {BINARY_MAX_RETRY_DELAYS} entries"
100            )));
101        }
102        if self.retry_delays.iter().any(Duration::is_zero) {
103            return Err(parameter_error(
104                "binary retry delays must be greater than zero",
105            ));
106        }
107        Ok(())
108    }
109}
110
111impl BinaryDownloadConfigBuilder {
112    /// Sets the global response body limit.
113    pub fn max_body_bytes(mut self, value: u64) -> Self {
114        self.config.max_body_bytes = value;
115        self
116    }
117
118    /// Overrides the inherited request timeout.
119    pub fn request_timeout(mut self, value: Duration) -> Self {
120        self.config.request_timeout = Some(value);
121        self
122    }
123
124    /// Overrides the inherited TCP keepalive.
125    pub fn tcp_keepalive(mut self, value: Duration) -> Self {
126        self.config.tcp_keepalive = Some(value);
127        self
128    }
129
130    /// Sets the redirect limit. Zero disables redirect following.
131    pub fn redirect_limit(mut self, value: usize) -> Self {
132        self.config.redirect_limit = value;
133        self
134    }
135
136    /// Replaces the retry schedule. An empty vector disables retries.
137    pub fn retry_delays(mut self, value: Vec<Duration>) -> Self {
138        self.config.retry_delays = value;
139        self
140    }
141
142    /// Validates and builds the configuration.
143    pub fn build(self) -> Result<BinaryDownloadConfig, MeowError> {
144        self.config.validate()?;
145        Ok(self.config)
146    }
147}
148
149fn parameter_error(message: impl Into<String>) -> MeowError {
150    MeowError::from_code(InnerErrorCode::ParameterEmpty, message.into())
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn defaults_are_bounded() {
159        let config = BinaryDownloadConfig::default();
160        assert_eq!(config.max_body_bytes(), 5 * 1024 * 1024);
161        assert_eq!(config.redirect_limit(), 5);
162        assert_eq!(config.retry_delays().len(), 2);
163        config.validate().expect("defaults must be valid");
164    }
165
166    #[test]
167    fn invalid_memory_and_timing_values_are_rejected() {
168        for max in [0, BINARY_ABSOLUTE_MAX_BODY_BYTES + 1] {
169            let error = BinaryDownloadConfig::builder()
170                .max_body_bytes(max)
171                .build()
172                .expect_err("invalid maximum must fail");
173            assert_eq!(error.code(), InnerErrorCode::ParameterEmpty as i32);
174        }
175        assert!(BinaryDownloadConfig::builder()
176            .request_timeout(Duration::ZERO)
177            .build()
178            .is_err());
179        assert!(BinaryDownloadConfig::builder()
180            .tcp_keepalive(Duration::ZERO)
181            .build()
182            .is_err());
183        assert!(BinaryDownloadConfig::builder()
184            .retry_delays(vec![Duration::ZERO])
185            .build()
186            .is_err());
187    }
188}