common_base/
memory_limit.rs1use std::fmt::{self, Display};
16use std::str::FromStr;
17
18use serde::{Deserialize, Deserializer, Serialize, Serializer};
19
20use crate::readable_size::ReadableSize;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum MemoryLimit {
30 Size(ReadableSize),
32 Percentage(u8),
34 #[default]
36 Unlimited,
37}
38
39impl MemoryLimit {
40 pub fn resolve(&self, total_memory_bytes: u64) -> u64 {
43 match self {
44 MemoryLimit::Size(size) => size.as_bytes(),
45 MemoryLimit::Percentage(pct) => total_memory_bytes * (*pct as u64) / 100,
46 MemoryLimit::Unlimited => 0,
47 }
48 }
49
50 pub fn is_unlimited(&self) -> bool {
52 match self {
53 MemoryLimit::Size(size) => size.as_bytes() == 0,
54 MemoryLimit::Percentage(pct) => *pct == 0,
55 MemoryLimit::Unlimited => true,
56 }
57 }
58}
59
60impl FromStr for MemoryLimit {
61 type Err = String;
62
63 fn from_str(s: &str) -> Result<Self, Self::Err> {
64 let s = s.trim();
65
66 if s.eq_ignore_ascii_case("unlimited") {
67 return Ok(MemoryLimit::Unlimited);
68 }
69
70 if let Some(pct_str) = s.strip_suffix('%') {
71 let pct = pct_str
72 .trim()
73 .parse::<u8>()
74 .map_err(|e| format!("invalid percentage value '{}': {}", pct_str, e))?;
75
76 if pct > 100 {
77 return Err(format!("percentage must be between 0 and 100, got {}", pct));
78 }
79
80 if pct == 0 {
81 Ok(MemoryLimit::Unlimited)
82 } else {
83 Ok(MemoryLimit::Percentage(pct))
84 }
85 } else {
86 let size = ReadableSize::from_str(s)?;
87 if size.as_bytes() == 0 {
88 Ok(MemoryLimit::Unlimited)
89 } else {
90 Ok(MemoryLimit::Size(size))
91 }
92 }
93 }
94}
95
96impl Display for MemoryLimit {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 match self {
99 MemoryLimit::Size(size) => write!(f, "{}", size),
100 MemoryLimit::Percentage(pct) => write!(f, "{}%", pct),
101 MemoryLimit::Unlimited => write!(f, "unlimited"),
102 }
103 }
104}
105
106impl Serialize for MemoryLimit {
107 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
108 where
109 S: Serializer,
110 {
111 serializer.serialize_str(&self.to_string())
112 }
113}
114
115impl<'de> Deserialize<'de> for MemoryLimit {
116 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
117 where
118 D: Deserializer<'de>,
119 {
120 let s = String::deserialize(deserializer)?;
121 MemoryLimit::from_str(&s).map_err(serde::de::Error::custom)
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn test_parse_absolute_size() {
131 assert_eq!(
132 "2GB".parse::<MemoryLimit>().unwrap(),
133 MemoryLimit::Size(ReadableSize(2 * 1024 * 1024 * 1024))
134 );
135 assert_eq!(
136 "512MB".parse::<MemoryLimit>().unwrap(),
137 MemoryLimit::Size(ReadableSize(512 * 1024 * 1024))
138 );
139 assert_eq!("0".parse::<MemoryLimit>().unwrap(), MemoryLimit::Unlimited);
140 }
141
142 #[test]
143 fn test_parse_percentage() {
144 assert_eq!(
145 "50%".parse::<MemoryLimit>().unwrap(),
146 MemoryLimit::Percentage(50)
147 );
148 assert_eq!(
149 "75%".parse::<MemoryLimit>().unwrap(),
150 MemoryLimit::Percentage(75)
151 );
152 assert_eq!("0%".parse::<MemoryLimit>().unwrap(), MemoryLimit::Unlimited);
153 }
154
155 #[test]
156 fn test_parse_invalid() {
157 assert!("150%".parse::<MemoryLimit>().is_err());
158 assert!("-10%".parse::<MemoryLimit>().is_err());
159 assert!("invalid".parse::<MemoryLimit>().is_err());
160 }
161
162 #[test]
163 fn test_resolve() {
164 let total = 8 * 1024 * 1024 * 1024; assert_eq!(
167 MemoryLimit::Size(ReadableSize(2 * 1024 * 1024 * 1024)).resolve(total),
168 2 * 1024 * 1024 * 1024
169 );
170 assert_eq!(
171 MemoryLimit::Percentage(50).resolve(total),
172 4 * 1024 * 1024 * 1024
173 );
174 assert_eq!(MemoryLimit::Unlimited.resolve(total), 0);
175 }
176
177 #[test]
178 fn test_is_unlimited() {
179 assert!(MemoryLimit::Unlimited.is_unlimited());
180 assert!(!MemoryLimit::Size(ReadableSize(1024)).is_unlimited());
181 assert!(!MemoryLimit::Percentage(50).is_unlimited());
182 assert!(!MemoryLimit::Percentage(1).is_unlimited());
183
184 assert!(MemoryLimit::Size(ReadableSize(0)).is_unlimited());
186 assert!(MemoryLimit::Percentage(0).is_unlimited());
187 }
188
189 #[test]
190 fn test_parse_100_percent() {
191 assert_eq!(
192 "100%".parse::<MemoryLimit>().unwrap(),
193 MemoryLimit::Percentage(100)
194 );
195 }
196
197 #[test]
198 fn test_display_percentage() {
199 assert_eq!(MemoryLimit::Percentage(20).to_string(), "20%");
200 assert_eq!(MemoryLimit::Percentage(50).to_string(), "50%");
201 assert_eq!(MemoryLimit::Percentage(100).to_string(), "100%");
202 }
203
204 #[test]
205 fn test_parse_unlimited() {
206 assert_eq!(
207 "unlimited".parse::<MemoryLimit>().unwrap(),
208 MemoryLimit::Unlimited
209 );
210 assert_eq!(
211 "UNLIMITED".parse::<MemoryLimit>().unwrap(),
212 MemoryLimit::Unlimited
213 );
214 assert_eq!(
215 "Unlimited".parse::<MemoryLimit>().unwrap(),
216 MemoryLimit::Unlimited
217 );
218 }
219
220 #[test]
221 fn test_display_unlimited() {
222 assert_eq!(MemoryLimit::Unlimited.to_string(), "unlimited");
223 }
224
225 #[test]
226 fn test_parse_display_roundtrip() {
227 let cases = vec![
228 "50%",
229 "100%",
230 "1%",
231 "2GB",
232 "512MB",
233 "unlimited",
234 "UNLIMITED",
235 "0", "0%", ];
238
239 for input in cases {
240 let parsed = input.parse::<MemoryLimit>().unwrap();
241 let displayed = parsed.to_string();
242 let reparsed = displayed.parse::<MemoryLimit>().unwrap();
243 assert_eq!(
244 parsed, reparsed,
245 "round-trip failed: '{}' -> '{}' -> '{:?}'",
246 input, displayed, reparsed
247 );
248 }
249 }
250
251 #[test]
252 fn test_zero_normalization() {
253 assert_eq!("0".parse::<MemoryLimit>().unwrap(), MemoryLimit::Unlimited);
255 assert_eq!("0%".parse::<MemoryLimit>().unwrap(), MemoryLimit::Unlimited);
256 assert_eq!("0B".parse::<MemoryLimit>().unwrap(), MemoryLimit::Unlimited);
257 assert_eq!(
258 "0KB".parse::<MemoryLimit>().unwrap(),
259 MemoryLimit::Unlimited
260 );
261
262 assert_eq!(MemoryLimit::Unlimited.to_string(), "unlimited");
264 }
265}