common_base/
memory_limit.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt::{self, Display};
16use std::str::FromStr;
17
18use serde::{Deserialize, Deserializer, Serialize, Serializer};
19
20use crate::readable_size::ReadableSize;
21
22/// Memory limit configuration that supports both absolute size and percentage.
23///
24/// Examples:
25/// - Absolute size: "2GB", "4GiB", "512MB"
26/// - Percentage: "50%", "75%"
27/// - Unlimited: "unlimited", "0"
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum MemoryLimit {
30    /// Absolute memory size.
31    Size(ReadableSize),
32    /// Percentage of total system memory (0-100).
33    Percentage(u8),
34    /// No memory limit.
35    #[default]
36    Unlimited,
37}
38
39impl MemoryLimit {
40    /// Resolve the memory limit to bytes based on total system memory.
41    /// Returns 0 if the limit is unlimited.
42    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    /// Returns true if this limit is unlimited.
51    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; // 8GB
165
166        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        // Defensive: these states shouldn't exist via public API, but check anyway
185        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",  // normalized to unlimited
236            "0%", // normalized to unlimited
237        ];
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        // All forms of zero should normalize to Unlimited
254        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        // Unlimited always displays as "unlimited"
263        assert_eq!(MemoryLimit::Unlimited.to_string(), "unlimited");
264    }
265}