aquarium_control/watchmen/
memory_config_check.rs

1/* Copyright 2025 Uwe Martin
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
5The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
7THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8*/
9use crate::watchmen::memory_config::MemoryConfig;
10use thiserror::Error;
11
12/// Definition of errors which can be encountered during memory_config_check
13#[derive(Error, Debug)]
14pub enum MemoryConfigError {
15    /// environment variable is not configured in .toml configuration file
16    #[error("environment variable for memory configuration check is not configured in .toml configuration file")]
17    ConfigEnvVarNameEmpty,
18
19    /// result of environment variable value request could not be parsed to numeric value
20    #[error(
21        "result of environment variable value request ({0}) could not be parsed to numeric value"
22    )]
23    EnvVarParseError(String),
24
25    /// environment variable value is lower than configured minimum value
26    #[error("environment variable value {0} is lower than configured minimum value {1}")]
27    EnvVarValueTooLow(u64, u64),
28
29    /// environment variable value is higher than configured maximum value
30    #[error("environment variable value {0} is higher than configured maximum value {1}")]
31    EnvVarValueTooHigh(u64, u64),
32
33    /// environment variable is not set
34    #[error("environment variable {0} is not set")]
35    EnvVarNotSet(String),
36}
37
38/// Checks a specified environment variable against a configured minimum and maximum values.
39///
40/// This function is used at application startup to verify that a system memory
41/// setting (`MALLOC_ARENA_MAX`) is within a safe and expected range.
42///
43/// # Arguments
44/// * `memory_config` - A reference to the `MemoryConfig` struct containing the check
45///   parameters (active flag, variable name, min/max values).
46///
47/// # Returns
48/// A `Result` containing an `Option` on success:
49/// - `Ok(Some((String, u64)))`: If the check is active and the environment variable's
50///   value is within the configured bounds. The tuple contains the variable's name
51///   and its parsed value.
52/// - `Ok(None)`: If the check is configured to be inactive (`active: false`).
53///
54/// # Errors
55/// Returns a `MemoryConfigError` if the check is active and any validation step fails:
56/// - `MemoryConfigError::ConfigEnvVarNameEmpty`: If `active` is true, but the
57///   `env_variable_name` in the configuration is empty.
58/// - `MemoryConfigError::EnvVarNotSet`: If the specified environment variable is not
59///   set in the current environment.
60/// - `MemoryConfigError::EnvVarParseError`: If the value of the environment variable
61///   cannot be parsed into a `u64` integer.
62/// - `MemoryConfigError::EnvVarValueTooLow`: If the parsed value is less than the
63///   configured `env_variable_min_val`.
64/// - `MemoryConfigError::EnvVarValueTooHigh`: If the parsed value is greater than the
65///   configured `env_variable_max_val`.
66pub fn memory_config_check(
67    memory_config: &MemoryConfig,
68) -> Result<Option<(String, u64)>, MemoryConfigError> {
69    if memory_config.env_variable_name.is_empty() {
70        return Err(MemoryConfigError::ConfigEnvVarNameEmpty);
71    }
72
73    let var_name = &memory_config.env_variable_name;
74
75    // Get the environment variable's value as a string.
76    let val_string =
77        std::env::var(var_name).map_err(|_| MemoryConfigError::EnvVarNotSet(var_name.clone()))?;
78
79    // Parse the string into u64.
80    let val = val_string
81        .parse::<u64>()
82        .map_err(|_| MemoryConfigError::EnvVarParseError(val_string))?;
83
84    // Check if the value is within the configured bounds.
85    if val < memory_config.env_variable_min_val {
86        return Err(MemoryConfigError::EnvVarValueTooLow(
87            val,
88            memory_config.env_variable_min_val,
89        ));
90    }
91    if val > memory_config.env_variable_max_val {
92        return Err(MemoryConfigError::EnvVarValueTooHigh(
93            val,
94            memory_config.env_variable_max_val,
95        ));
96    }
97
98    // If all checks pass, return the name and value.
99    Ok(Some((var_name.clone(), val)))
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use std::env;
106
107    // Helper function to create a default config for tests
108    fn create_test_config() -> MemoryConfig {
109        MemoryConfig {
110            active: true,
111            execute: false,
112            env_variable_name: "TEST_VAR".to_string(),
113            env_variable_min_val: 1,
114            env_variable_max_val: 10,
115            #[cfg(feature = "jemalloc")]
116            max_resident: 0,
117            #[cfg(feature = "jemalloc")]
118            max_allocated: 0,
119            check_interval: 0,
120        }
121    }
122
123    // Test if memory_config_check returns an error if the check is active, but the variable name is empty.
124    #[test]
125    fn test_empty_env_variable_name_returns_error() {
126        let mut config = create_test_config();
127        config.env_variable_name = String::new();
128
129        let result = memory_config_check(&config);
130        assert!(matches!(
131            result,
132            Err(MemoryConfigError::ConfigEnvVarNameEmpty)
133        ));
134    }
135
136    // Test if memory_config_check returns an error if the environment variable is not set.
137    #[test]
138    fn test_unset_env_variable_returns_error() {
139        let mut config = create_test_config();
140        let exotic_name = "EXOTIC_VAR_NAME_THAT_DOES_NOT_EXIST_123";
141        config.env_variable_name = exotic_name.to_string();
142
143        // Ensure the variable is not set
144        env::remove_var(exotic_name);
145
146        let result = memory_config_check(&config);
147        match result {
148            Err(MemoryConfigError::EnvVarNotSet(name)) => {
149                assert_eq!(name, exotic_name);
150            }
151            _ => panic!("{}: Expected EnvVarNotSet error", module_path!()),
152        }
153    }
154
155    // Test if memory_config_check returns a parse error for a non-numeric environment variable.
156    #[test]
157    fn test_non_numeric_env_variable_returns_parse_error() {
158        let mut config = create_test_config();
159        let var_name = "TEST_VAR_NON_NUMERIC";
160        let var_value = "not-a-number";
161        config.env_variable_name = var_name.to_string();
162
163        env::set_var(var_name, var_value);
164        let result = memory_config_check(&config);
165        env::remove_var(var_name); // Cleanup
166
167        match result {
168            Err(MemoryConfigError::EnvVarParseError(val)) => {
169                assert_eq!(val, var_value);
170            }
171            _ => panic!("{}: Expected EnvVarParseError", module_path!()),
172        }
173    }
174
175    // Test if memory_config_check returns an error if the value is below the configured minimum.
176    #[test]
177    fn test_value_below_minimum_returns_error() {
178        let mut config = create_test_config();
179        let var_name = "TEST_VAR_TOO_LOW";
180        config.env_variable_name = var_name.to_string();
181        config.env_variable_min_val = 5;
182        config.env_variable_max_val = 10;
183
184        env::set_var(var_name, "2");
185        let result = memory_config_check(&config);
186        env::remove_var(var_name); // Cleanup
187
188        match result {
189            Err(MemoryConfigError::EnvVarValueTooLow(val, min)) => {
190                assert_eq!(val, 2);
191                assert_eq!(min, 5);
192            }
193            _ => panic!("{}: Expected EnvVarValueTooLow error", module_path!()),
194        }
195    }
196
197    // Test if memory_config_check returns an error if the value is above the configured maximum.
198    #[test]
199    fn test_value_above_maximum_returns_error() {
200        let mut config = create_test_config();
201        let var_name = "TEST_VAR_TOO_HIGH";
202        config.env_variable_name = var_name.to_string();
203        config.env_variable_min_val = 5;
204        config.env_variable_max_val = 10;
205
206        env::set_var(var_name, "15");
207        let result = memory_config_check(&config);
208        env::remove_var(var_name); // Cleanup
209
210        match result {
211            Err(MemoryConfigError::EnvVarValueTooHigh(val, max)) => {
212                assert_eq!(val, 15);
213                assert_eq!(max, 10);
214            }
215            _ => panic!("{}: Expected EnvVarValueTooHigh error", module_path!()),
216        }
217    }
218
219    // Test if memory_config_check returns Ok(value) when the value is within the configured bounds.
220    #[test]
221    fn test_value_within_bounds_is_ok() {
222        let mut config = create_test_config();
223        let var_name = "TEST_VAR_IN_BOUNDS";
224        config.env_variable_name = var_name.to_string();
225        config.env_variable_min_val = 5;
226        config.env_variable_max_val = 10;
227
228        env::set_var(var_name, "8");
229        let result = memory_config_check(&config);
230        env::remove_var(var_name); // Cleanup
231
232        match result {
233            Ok(Some((name, val))) => {
234                assert_eq!(name, var_name);
235                assert_eq!(val, 8);
236            }
237            _ => panic!("{}, Expected Ok(Some(...))", module_path!()),
238        }
239    }
240}