sqlite_graphrag/storage/
utils.rs1use crate::constants::{MAX_SQLITE_BUSY_RETRIES, SQLITE_BUSY_BASE_DELAY_MS};
4use crate::errors::AppError;
5use rusqlite::ErrorCode;
6use std::thread;
7use std::time::Duration;
8
9fn resolved_busy_retries() -> u32 {
13 crate::runtime_config::db_busy_retries(MAX_SQLITE_BUSY_RETRIES).max(1)
14}
15
16fn resolved_busy_base_delay_ms() -> u64 {
18 crate::runtime_config::db_busy_base_delay_ms(SQLITE_BUSY_BASE_DELAY_MS).max(1)
19}
20
21pub fn is_sqlite_busy(err: &AppError) -> bool {
29 match err {
30 AppError::Database(rusqlite::Error::SqliteFailure(e, _)) => {
31 e.code == ErrorCode::DatabaseBusy || e.code == ErrorCode::DatabaseLocked
32 }
33 _ => false,
34 }
35}
36
37pub fn with_busy_retry<T, F>(op: F) -> Result<T, AppError>
48where
49 F: Fn() -> Result<T, AppError>,
50{
51 let max_retries = resolved_busy_retries();
52 let base_delay_ms = resolved_busy_base_delay_ms();
53 for attempt in 0..max_retries {
54 match op() {
55 Ok(v) => return Ok(v),
56 Err(e) if is_sqlite_busy(&e) => {
57 if crate::retry::is_kill_switch_active() {
58 tracing::warn!(target: "storage", "retry.disable is set, propagating SQLITE_BUSY immediately");
59 return Err(e);
60 }
61 let shift = attempt.min(63);
63 let base_ms = base_delay_ms.saturating_mul(1u64 << shift);
64 let half = base_ms / 2;
65 let jitter = if half == 0 { 0 } else { fastrand::u64(0..half) };
66 let delay_ms = half + jitter;
67 tracing::debug!(
68 target: "storage",
69 attempt = attempt + 1,
70 attempt_max = max_retries,
71 delay_ms,
72 "SQLITE_BUSY retry with half-jitter"
73 );
74 thread::sleep(Duration::from_millis(delay_ms));
75 }
76 Err(other) => return Err(other),
77 }
78 }
79
80 tracing::error!(
81 target: "storage",
82 retries = max_retries,
83 "SQLITE_BUSY exhausted all retries"
84 );
85 Err(AppError::DbBusy(format!(
86 "SQLITE_BUSY after {max_retries} retries"
87 )))
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use std::sync::atomic::{AtomicU32, Ordering};
94 use std::sync::Arc;
95
96 fn make_busy_error() -> AppError {
100 let ffi_err = rusqlite::ffi::Error {
103 code: ErrorCode::DatabaseBusy,
104 extended_code: 5,
105 };
106 AppError::Database(rusqlite::Error::SqliteFailure(ffi_err, None))
107 }
108
109 fn make_locked_error() -> AppError {
110 let ffi_err = rusqlite::ffi::Error {
111 code: ErrorCode::DatabaseLocked,
112 extended_code: 6,
113 };
114 AppError::Database(rusqlite::Error::SqliteFailure(ffi_err, None))
115 }
116
117 #[test]
118 fn is_sqlite_busy_detects_database_busy() {
119 assert!(is_sqlite_busy(&make_busy_error()));
120 }
121
122 #[test]
123 fn is_sqlite_busy_detects_database_locked() {
124 assert!(is_sqlite_busy(&make_locked_error()));
125 }
126
127 #[test]
128 fn is_sqlite_busy_rejects_other_errors() {
129 let err = AppError::Validation("invalid field".into());
130 assert!(!is_sqlite_busy(&err));
131 }
132
133 #[test]
134 fn with_busy_retry_propagates_non_busy_error() {
135 let calls = Arc::new(AtomicU32::new(0));
136 let calls_clone = Arc::clone(&calls);
137
138 let result: Result<(), AppError> = with_busy_retry(|| {
139 calls_clone.fetch_add(1, Ordering::SeqCst);
140 Err(AppError::Validation("campo x".into()))
141 });
142
143 assert_eq!(calls.load(Ordering::SeqCst), 1);
145 assert!(matches!(result, Err(AppError::Validation(_))));
146 }
147
148 #[test]
149 fn with_busy_retry_succeeds_on_third_attempt() {
150 let calls = Arc::new(AtomicU32::new(0));
151 let calls_clone = Arc::clone(&calls);
152
153 let result = with_busy_retry(|| {
155 let n = calls_clone.fetch_add(1, Ordering::SeqCst);
156 if n < 2 {
157 Err(make_busy_error())
158 } else {
159 Ok(())
160 }
161 });
162
163 assert_eq!(calls.load(Ordering::SeqCst), 3);
164 assert!(result.is_ok(), "expected Ok after 3rd attempt");
165 }
166
167 #[test]
168 fn busy_retry_jitter_in_range() {
169 let base_ms = SQLITE_BUSY_BASE_DELAY_MS * (1u64 << 2); let half = base_ms / 2;
175 for _ in 0..100 {
176 let jitter = fastrand::u64(0..half);
177 let delay_ms = half + jitter;
178 assert!(
179 delay_ms >= half && delay_ms < base_ms,
180 "delay_ms {delay_ms} out of [{half}, {base_ms})"
181 );
182 }
183 }
184
185 #[test]
186 fn with_busy_retry_returns_db_busy_after_all_retries() {
187 let calls = Arc::new(AtomicU32::new(0));
188 let calls_clone = Arc::clone(&calls);
189
190 let result: Result<(), AppError> = with_busy_retry(|| {
191 calls_clone.fetch_add(1, Ordering::SeqCst);
192 Err(make_busy_error())
193 });
194
195 let expected = resolved_busy_retries();
196 assert_eq!(
197 calls.load(Ordering::SeqCst),
198 expected,
199 "must attempt exactly the resolved busy-retry budget"
200 );
201 assert!(
202 matches!(result, Err(AppError::DbBusy(_))),
203 "must convert to DbBusy after exhausting retries"
204 );
205 }
206
207 #[test]
215 fn with_busy_retry_is_generic_over_return_type() {
216 let ok: Result<i64, AppError> = with_busy_retry(|| Ok(42i64));
218 assert_eq!(ok.unwrap(), 42);
219
220 let calls = Arc::new(AtomicU32::new(0));
224 let calls_clone = Arc::clone(&calls);
225 let result: Result<i64, AppError> = with_busy_retry(|| {
226 calls_clone.fetch_add(1, Ordering::SeqCst);
227 Err(make_busy_error())
228 });
229 assert_eq!(calls.load(Ordering::SeqCst), resolved_busy_retries());
230 assert!(matches!(result, Err(AppError::DbBusy(_))));
231 }
232
233 #[test]
235 fn resolved_busy_policy_defaults_match_constants() {
236 assert_eq!(resolved_busy_retries(), MAX_SQLITE_BUSY_RETRIES);
237 assert_eq!(resolved_busy_base_delay_ms(), SQLITE_BUSY_BASE_DELAY_MS);
238 }
239}