absurder_sql/storage/
export_import_lock.rs

1//! Export/Import Operation Locking
2//! 
3//! Provides per-database locking to serialize export/import operations and prevent
4//! concurrent access that could lead to data corruption or `BorrowMutError` panics.
5
6use std::cell::RefCell;
7use std::collections::HashMap;
8use std::rc::Rc;
9use crate::types::DatabaseError;
10
11const LOCK_TIMEOUT_RETRIES: u32 = 3000; // 3000 retries * 10ms = 30 seconds max wait
12
13/// Represents a lock for a specific database's export/import operations
14struct ExportImportLock {
15    /// Whether the lock is currently held
16    locked: bool,
17}
18
19impl ExportImportLock {
20    fn new() -> Self {
21        Self {
22            locked: false,
23        }
24    }
25    
26    /// Try to acquire the lock. Returns true if successful.
27    fn try_acquire(&mut self) -> bool {
28        if !self.locked {
29            self.locked = true;
30            true
31        } else {
32            false
33        }
34    }
35    
36    /// Release the lock
37    fn release(&mut self) {
38        self.locked = false;
39    }
40}
41
42thread_local! {
43    /// Global registry of per-database export/import locks
44    static EXPORT_IMPORT_LOCKS: RefCell<HashMap<String, Rc<RefCell<ExportImportLock>>>> = 
45        RefCell::new(HashMap::new());
46}
47
48/// Acquire export/import lock for a database
49/// 
50/// This will block (via polling) until the lock is available or timeout is reached.
51/// 
52/// # Arguments
53/// * `db_name` - Name of the database to lock
54/// 
55/// # Returns
56/// * `Ok(LockGuard)` - Lock acquired successfully
57/// * `Err(DatabaseError)` - Failed to acquire lock (timeout)
58pub async fn acquire_export_import_lock(db_name: &str) -> Result<LockGuard, DatabaseError> {
59    let db_name_owned = db_name.to_string();
60    let mut retries = 0u32;
61    
62    loop {
63        // Try to acquire the lock
64        let acquired = EXPORT_IMPORT_LOCKS.with(|locks| {
65            let mut locks_map = locks.borrow_mut();
66            let lock_rc = locks_map
67                .entry(db_name_owned.clone())
68                .or_insert_with(|| Rc::new(RefCell::new(ExportImportLock::new())));
69            
70            let mut lock = lock_rc.borrow_mut();
71            lock.try_acquire()
72        });
73        
74        if acquired {
75            log::debug!("Acquired export/import lock for: {}", db_name);
76            return Ok(LockGuard {
77                db_name: db_name_owned,
78            });
79        }
80        
81        // Check timeout
82        retries += 1;
83        if retries >= LOCK_TIMEOUT_RETRIES {
84            return Err(DatabaseError::new(
85                "LOCK_TIMEOUT",
86                &format!("Failed to acquire export/import lock for {} after {} seconds", 
87                        db_name, LOCK_TIMEOUT_RETRIES * 10 / 1000)
88            ));
89        }
90        
91        // Wait a bit before retrying
92        #[cfg(target_arch = "wasm32")]
93        {
94            // Use setTimeout to yield to browser event loop
95            let promise = js_sys::Promise::new(&mut |resolve, _reject| {
96                web_sys::window()
97                    .unwrap()
98                    .set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, 10)
99                    .unwrap();
100            });
101            wasm_bindgen_futures::JsFuture::from(promise).await.ok();
102        }
103        
104        #[cfg(not(target_arch = "wasm32"))]
105        {
106            use std::time::Duration;
107            tokio::time::sleep(Duration::from_millis(10)).await;
108        }
109    }
110}
111
112/// RAII guard that automatically releases the lock when dropped
113pub struct LockGuard {
114    db_name: String,
115}
116
117impl Drop for LockGuard {
118    fn drop(&mut self) {
119        // Release the lock
120        EXPORT_IMPORT_LOCKS.with(|locks| {
121            let locks_map = locks.borrow();
122            if let Some(lock_rc) = locks_map.get(&self.db_name) {
123                let mut lock = lock_rc.borrow_mut();
124                lock.release();
125                log::debug!("Released export/import lock for: {}", self.db_name);
126            }
127        });
128    }
129}
130
131// Tests are in the export_import_lock_tests.rs integration test file
132// since they need to test WASM async behavior properly