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> {
104 let bytes = text.len();
107 if bytes > crate::constants::MAX_MEMORY_BODY_LEN {
108 return Err(AppError::Validation(format!(
109 "embedding input is {} bytes, above the {}-byte body cap; \
110 split it into smaller memories",
111 bytes,
112 crate::constants::MAX_MEMORY_BODY_LEN
113 )));
114 }
115
116 let tokens = crate::tokenizer::count_tokens(text);
118 if tokens > crate::constants::EMBEDDING_REQUEST_MAX_TOKENS {
119 return Err(AppError::Validation(format!(
120 "embedding input is {} tokens, above the {}-token model ceiling; \
121 split it into smaller memories",
122 tokens,
123 crate::constants::EMBEDDING_REQUEST_MAX_TOKENS
124 )));
125 }
126
127 Ok(())
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn check_available_memory_with_zero_always_passes() {
136 let result = check_available_memory(0);
137 assert!(result.is_ok(), "min_mb=0 must always pass, got: {result:?}");
138 let mb = result.unwrap();
139 assert!(mb > 0, "system must report positive memory");
140 }
141
142 #[test]
143 fn check_available_memory_with_huge_value_fails() {
144 let result = check_available_memory(u64::MAX);
145 assert!(
146 matches!(result, Err(AppError::LowMemory { .. })),
147 "u64::MAX MiB must fail with LowMemory, got: {result:?}"
148 );
149 }
150
151 #[test]
152 fn low_memory_error_contains_correct_values() {
153 match check_available_memory(u64::MAX) {
154 Err(AppError::LowMemory {
155 available_mb,
156 required_mb,
157 }) => {
158 assert_eq!(required_mb, u64::MAX);
159 assert!(available_mb < u64::MAX);
160 }
161 other => unreachable!("expected LowMemory, got: {other:?}"),
162 }
163 }
164
165 #[test]
166 fn calculate_safe_concurrency_no_half_margin() {
167 let permits = calculate_safe_concurrency(8_000, 8, 1_000, 16);
169 assert_eq!(permits, 8);
170 }
171
172 #[test]
173 fn calculate_safe_concurrency_never_returns_zero() {
174 let permits = calculate_safe_concurrency(100, 1, 10_000, 16);
175 assert_eq!(permits, 1);
176 }
177
178 #[test]
179 fn calculate_safe_concurrency_respects_max_ceiling() {
180 let permits = calculate_safe_concurrency(128_000, 64, 500, 16);
182 assert_eq!(permits, 16);
183 }
184
185 #[test]
186 fn calculate_safe_concurrency_llm_worker_budget() {
187 let permits = calculate_safe_concurrency(64_000, 8, 350, 16);
190 assert_eq!(permits, 8);
191 }
192
193 #[test]
194 fn current_process_memory_mb_returns_some_value() {
195 let rss = current_process_memory_mb();
196 assert!(rss.is_some());
197 }
198
199 #[test]
200 fn check_embedding_input_size_accepts_small_text() {
201 assert!(check_embedding_input_size("a short passage").is_ok());
202 }
203
204 #[test]
205 fn check_embedding_input_size_rejects_above_token_ceiling() {
206 let big = "word ".repeat(crate::constants::EMBEDDING_REQUEST_MAX_TOKENS + 5_000);
209 assert!(
210 big.len() <= crate::constants::MAX_MEMORY_BODY_LEN,
211 "token guard, not byte guard, must be exercised"
212 );
213 match check_embedding_input_size(&big) {
214 Err(AppError::Validation(msg)) => assert!(msg.contains("tokens")),
215 other => unreachable!("expected Validation(tokens), got: {other:?}"),
216 }
217 }
218
219 #[test]
220 fn check_embedding_input_size_rejects_above_byte_cap() {
221 let huge = "x".repeat(crate::constants::MAX_MEMORY_BODY_LEN + 1);
222 match check_embedding_input_size(&huge) {
223 Err(AppError::Validation(msg)) => assert!(msg.contains("bytes")),
224 other => unreachable!("expected Validation(bytes), got: {other:?}"),
225 }
226 }
227}