sqlite_graphrag/
memory_guard.rs1use sysinfo::{
13 get_current_pid, MemoryRefreshKind, ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System,
14 UpdateKind,
15};
16
17use crate::errors::AppError;
18
19pub fn available_memory_mb() -> u64 {
21 let sys =
22 System::new_with_specifics(RefreshKind::new().with_memory(MemoryRefreshKind::everything()));
23 let available_bytes = sys.available_memory();
24 available_bytes / (1024 * 1024)
25}
26
27pub fn current_process_memory_mb() -> Option<u64> {
29 let pid = get_current_pid().ok()?;
30 let mut sys =
31 System::new_with_specifics(RefreshKind::new().with_memory(MemoryRefreshKind::everything()));
32 sys.refresh_processes_specifics(
33 ProcessesToUpdate::Some(&[pid]),
34 true,
35 ProcessRefreshKind::new()
36 .with_memory()
37 .with_exe(UpdateKind::OnlyIfNotSet),
38 );
39 sys.process(pid).map(|p| p.memory() / (1024 * 1024))
40}
41
42pub fn calculate_safe_concurrency(
49 available_mb: u64,
50 cpu_count: usize,
51 ram_per_task_mb: u64,
52 max_concurrency: usize,
53) -> usize {
54 let cpu_count = cpu_count.max(1);
55 let max_concurrency = max_concurrency.max(1);
56 let ram_per_task_mb = ram_per_task_mb.max(1);
57
58 let memory_bound = (available_mb / ram_per_task_mb) as usize;
59 let resource_bound = cpu_count.min(memory_bound).max(1);
60 resource_bound.min(max_concurrency)
63}
64
65pub fn check_available_memory(min_mb: u64) -> Result<u64, AppError> {
77 let available_mb = available_memory_mb();
78
79 if available_mb < min_mb {
80 return Err(AppError::LowMemory {
81 available_mb,
82 required_mb: min_mb,
83 });
84 }
85
86 Ok(available_mb)
87}
88
89pub fn check_embedding_input_size(text: &str) -> Result<(), AppError> {
107 let bytes = text.len();
110 if bytes > crate::constants::MAX_MEMORY_BODY_LEN {
111 return Err(AppError::BodyTooLarge {
112 bytes: bytes as u64,
113 limit: crate::constants::MAX_MEMORY_BODY_LEN as u64,
114 });
115 }
116
117 let tokens = crate::tokenizer::count_tokens(text);
119 if tokens > crate::constants::EMBEDDING_REQUEST_MAX_TOKENS {
120 return Err(AppError::TooManyTokens {
121 tokens: tokens as u64,
122 limit: crate::constants::EMBEDDING_REQUEST_MAX_TOKENS as u64,
123 });
124 }
125
126 Ok(())
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn check_available_memory_with_zero_always_passes() {
135 let result = check_available_memory(0);
136 assert!(result.is_ok(), "min_mb=0 must always pass, got: {result:?}");
137 let mb = result.unwrap();
138 assert!(mb > 0, "system must report positive memory");
139 }
140
141 #[test]
142 fn check_available_memory_with_huge_value_fails() {
143 let result = check_available_memory(u64::MAX);
144 assert!(
145 matches!(result, Err(AppError::LowMemory { .. })),
146 "u64::MAX MiB must fail with LowMemory, got: {result:?}"
147 );
148 }
149
150 #[test]
151 fn low_memory_error_contains_correct_values() {
152 match check_available_memory(u64::MAX) {
153 Err(AppError::LowMemory {
154 available_mb,
155 required_mb,
156 }) => {
157 assert_eq!(required_mb, u64::MAX);
158 assert!(available_mb < u64::MAX);
159 }
160 other => unreachable!("expected LowMemory, got: {other:?}"),
161 }
162 }
163
164 #[test]
165 fn calculate_safe_concurrency_no_half_margin() {
166 let permits = calculate_safe_concurrency(8_000, 8, 1_000, 16);
168 assert_eq!(permits, 8);
169 }
170
171 #[test]
172 fn calculate_safe_concurrency_never_returns_zero() {
173 let permits = calculate_safe_concurrency(100, 1, 10_000, 16);
174 assert_eq!(permits, 1);
175 }
176
177 #[test]
178 fn calculate_safe_concurrency_respects_max_ceiling() {
179 let permits = calculate_safe_concurrency(128_000, 64, 500, 16);
181 assert_eq!(permits, 16);
182 }
183
184 #[test]
185 fn calculate_safe_concurrency_llm_worker_budget() {
186 let permits = calculate_safe_concurrency(64_000, 8, 350, 16);
189 assert_eq!(permits, 8);
190 }
191
192 #[test]
193 fn current_process_memory_mb_returns_some_value() {
194 let rss = current_process_memory_mb();
195 assert!(rss.is_some());
196 }
197
198 #[test]
199 fn check_embedding_input_size_accepts_small_text() {
200 assert!(check_embedding_input_size("a short passage").is_ok());
201 }
202
203 #[test]
204 fn check_embedding_input_size_rejects_above_token_ceiling() {
205 let big = "word ".repeat(crate::constants::EMBEDDING_REQUEST_MAX_TOKENS + 5_000);
208 assert!(
209 big.len() <= crate::constants::MAX_MEMORY_BODY_LEN,
210 "token guard, not byte guard, must be exercised"
211 );
212 match check_embedding_input_size(&big) {
213 Err(e @ AppError::TooManyTokens { tokens, limit }) => {
216 assert_eq!(e.exit_code(), 6);
217 assert!(tokens > limit, "tokens={tokens} limit={limit}");
218 assert_eq!(limit, crate::constants::EMBEDDING_REQUEST_MAX_TOKENS as u64);
219 }
220 other => unreachable!("expected TooManyTokens, got: {other:?}"),
221 }
222 }
223
224 #[test]
225 fn check_embedding_input_size_rejects_above_byte_cap() {
226 let huge = "x".repeat(crate::constants::MAX_MEMORY_BODY_LEN + 1);
227 match check_embedding_input_size(&huge) {
228 Err(e @ AppError::BodyTooLarge { bytes, limit }) => {
231 assert_eq!(e.exit_code(), 6);
232 assert!(bytes > limit, "bytes={bytes} limit={limit}");
233 assert_eq!(limit, crate::constants::MAX_MEMORY_BODY_LEN as u64);
234 }
235 other => unreachable!("expected BodyTooLarge, got: {other:?}"),
236 }
237 }
238}