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
9pub fn is_sqlite_busy(err: &AppError) -> bool {
17 match err {
18 AppError::Database(rusqlite::Error::SqliteFailure(e, _)) => {
19 e.code == ErrorCode::DatabaseBusy || e.code == ErrorCode::DatabaseLocked
20 }
21 _ => false,
22 }
23}
24
25pub fn with_busy_retry<F>(op: F) -> Result<(), AppError>
38where
39 F: Fn() -> Result<(), AppError>,
40{
41 for attempt in 0..MAX_SQLITE_BUSY_RETRIES {
42 match op() {
43 Ok(()) => return Ok(()),
44 Err(e) if is_sqlite_busy(&e) => {
45 let delay_ms = SQLITE_BUSY_BASE_DELAY_MS * (1u64 << attempt);
47 thread::sleep(Duration::from_millis(delay_ms));
48 }
49 Err(other) => return Err(other),
50 }
51 }
52
53 Err(AppError::DbBusy(format!(
55 "SQLITE_BUSY after {MAX_SQLITE_BUSY_RETRIES} retries"
56 )))
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use std::sync::atomic::{AtomicU32, Ordering};
63 use std::sync::Arc;
64
65 fn make_busy_error() -> AppError {
69 let ffi_err = rusqlite::ffi::Error {
72 code: ErrorCode::DatabaseBusy,
73 extended_code: 5,
74 };
75 AppError::Database(rusqlite::Error::SqliteFailure(ffi_err, None))
76 }
77
78 fn make_locked_error() -> AppError {
79 let ffi_err = rusqlite::ffi::Error {
80 code: ErrorCode::DatabaseLocked,
81 extended_code: 6,
82 };
83 AppError::Database(rusqlite::Error::SqliteFailure(ffi_err, None))
84 }
85
86 #[test]
87 fn is_sqlite_busy_detecta_database_busy() {
88 assert!(is_sqlite_busy(&make_busy_error()));
89 }
90
91 #[test]
92 fn is_sqlite_busy_detecta_database_locked() {
93 assert!(is_sqlite_busy(&make_locked_error()));
94 }
95
96 #[test]
97 fn is_sqlite_busy_rejeita_outros_erros() {
98 let err = AppError::Validation("campo inválido".into());
99 assert!(!is_sqlite_busy(&err));
100 }
101
102 #[test]
103 fn with_busy_retry_propagates_non_busy_error() {
104 let calls = Arc::new(AtomicU32::new(0));
105 let calls_clone = Arc::clone(&calls);
106
107 let result = with_busy_retry(|| {
108 calls_clone.fetch_add(1, Ordering::SeqCst);
109 Err(AppError::Validation("campo x".into()))
110 });
111
112 assert_eq!(calls.load(Ordering::SeqCst), 1);
114 assert!(matches!(result, Err(AppError::Validation(_))));
115 }
116
117 #[test]
118 fn with_busy_retry_succeeds_on_third_attempt() {
119 let calls = Arc::new(AtomicU32::new(0));
120 let calls_clone = Arc::clone(&calls);
121
122 let result = with_busy_retry(|| {
124 let n = calls_clone.fetch_add(1, Ordering::SeqCst);
125 if n < 2 {
126 Err(make_busy_error())
127 } else {
128 Ok(())
129 }
130 });
131
132 assert_eq!(calls.load(Ordering::SeqCst), 3);
133 assert!(result.is_ok(), "expected Ok after 3rd attempt");
134 }
135
136 #[test]
137 fn with_busy_retry_returns_db_busy_after_all_retries() {
138 let calls = Arc::new(AtomicU32::new(0));
139 let calls_clone = Arc::clone(&calls);
140
141 let result = with_busy_retry(|| {
142 calls_clone.fetch_add(1, Ordering::SeqCst);
143 Err(make_busy_error())
144 });
145
146 assert_eq!(
147 calls.load(Ordering::SeqCst),
148 MAX_SQLITE_BUSY_RETRIES,
149 "must attempt exactly MAX_SQLITE_BUSY_RETRIES times"
150 );
151 assert!(
152 matches!(result, Err(AppError::DbBusy(_))),
153 "must convert to DbBusy after exhausting retries"
154 );
155 }
156}