Skip to main content

snerd_rust/
file_store.rs

1use std::collections::HashMap;
2use std::fs::{File, OpenOptions};
3use std::io::{BufRead, BufReader, Write};
4use std::path::{Path, PathBuf};
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex, RwLock};
7
8use crate::task::RetryableTask;
9
10#[derive(Clone)]
11pub struct FileStore {
12    file_path: Arc<PathBuf>,
13    total_tasks: Arc<Mutex<usize>>,
14    deleted_tasks: Arc<Mutex<usize>>,
15    append_count: Arc<Mutex<usize>>,
16    compacting: Arc<AtomicBool>,
17    file_lock: Arc<RwLock<()>>,
18    tasks_cache: Arc<RwLock<HashMap<String, RetryableTask>>>,
19}
20
21impl FileStore {
22    pub fn new<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
23        let fs = FileStore {
24            file_path: Arc::new(path.as_ref().to_path_buf()),
25            total_tasks: Arc::new(Mutex::new(0)),
26            deleted_tasks: Arc::new(Mutex::new(0)),
27            append_count: Arc::new(Mutex::new(0)),
28            compacting: Arc::new(AtomicBool::new(false)),
29            file_lock: Arc::new(RwLock::new(())),
30            tasks_cache: Arc::new(RwLock::new(HashMap::new())),
31        };
32        fs.rebuild_metadata()?;
33        Ok(fs)
34    }
35
36    fn rebuild_metadata(&self) -> std::io::Result<()> {
37        let _lock = self.file_lock.write().unwrap();
38        let mut cache = self.tasks_cache.write().unwrap();
39        cache.clear();
40
41        if !self.file_path.exists() {
42            return Ok(());
43        }
44
45        let file = File::open(self.file_path.as_ref())?;
46
47        let mut total = 0;
48        let mut deleted = 0;
49        let mut appended = 0;
50
51        let reader = BufReader::new(&file);
52        for line_str in reader.lines().map_while(Result::ok) {
53            if line_str.trim().is_empty() {
54                continue;
55            }
56            if let Ok(task) = serde_json::from_str::<RetryableTask>(&line_str) {
57                appended += 1;
58                if task.deleted_at.is_some() {
59                    deleted += 1;
60                    cache.remove(&task.task_id);
61                } else {
62                    total += 1;
63                    cache.insert(task.task_id.clone(), task);
64                }
65            }
66        }
67
68        *self.total_tasks.lock().unwrap() = total;
69        *self.deleted_tasks.lock().unwrap() = deleted;
70        *self.append_count.lock().unwrap() = appended;
71
72        Ok(())
73    }
74
75    pub fn file_path(&self) -> &Path {
76        self.file_path.as_ref()
77    }
78
79    pub fn save_task(&self, task: &RetryableTask) -> std::io::Result<()> {
80        self.save_task_inner(task, true)
81    }
82
83    /// Internal save with option to skip compaction check.
84    /// delete_task passes false to prevent compaction cascades when
85    /// many tasks complete concurrently.
86    fn save_task_inner(&self, task: &RetryableTask, check_compact: bool) -> std::io::Result<()> {
87        let _lock = self.file_lock.write().unwrap();
88
89        if let Some(parent) = self.file_path.parent() {
90            std::fs::create_dir_all(parent)?;
91        }
92
93        let mut file = OpenOptions::new()
94            .create(true)
95            .append(true)
96            .open(self.file_path.as_ref())?;
97
98        let json_str = serde_json::to_string(task)?;
99        writeln!(file, "{}", json_str)?;
100        file.sync_all()?;
101        drop(file);
102
103        let is_deleted = task.deleted_at.is_some();
104        
105        {
106            let mut cache = self.tasks_cache.write().unwrap();
107            if is_deleted {
108                cache.remove(&task.task_id);
109            } else {
110                cache.insert(task.task_id.clone(), task.clone());
111            }
112        }
113
114        {
115            *self.append_count.lock().unwrap() += 1;
116            if is_deleted {
117                *self.deleted_tasks.lock().unwrap() += 1;
118            } else {
119                *self.total_tasks.lock().unwrap() += 1;
120            }
121        }
122
123        if check_compact && self.should_compact() {
124            let fs_clone = self.clone();
125            tokio::spawn(async move {
126                let _ = fs_clone.compact_log();
127            });
128        }
129
130        Ok(())
131    }
132
133    pub fn read_tasks(&self) -> std::io::Result<Vec<RetryableTask>> {
134        let cache = self.tasks_cache.read().unwrap();
135        Ok(cache.values().cloned().collect())
136    }
137
138    pub fn get_latest_task(&self, task_id: &str) -> std::io::Result<Option<RetryableTask>> {
139        let cache = self.tasks_cache.read().unwrap();
140        Ok(cache.get(task_id).cloned())
141    }
142
143    pub fn delete_task(&self, task_id: &str) -> std::io::Result<()> {
144        let mut task_opt = None;
145        {
146            let cache = self.tasks_cache.read().unwrap();
147            if let Some(task) = cache.get(task_id) {
148                if task.deleted_at.is_none() {
149                    task_opt = Some(task.clone());
150                }
151            }
152        }
153        
154        if let Some(mut task) = task_opt {
155            task.mark_deleted();
156            // Skip compaction check on delete to prevent cascading compaction
157            // when many tasks complete concurrently (which would empty the log).
158            self.save_task_inner(&task, false)?;
159        }
160        Ok(())
161    }
162
163    fn should_compact(&self) -> bool {
164        if let Ok(metadata) = std::fs::metadata(self.file_path.as_ref()) {
165            if metadata.len() > 20 * 1024 * 1024 {
166                return true;
167            }
168        }
169
170        let total = *self.total_tasks.lock().unwrap();
171        let deleted = *self.deleted_tasks.lock().unwrap();
172        if total > 0 && (deleted as f64 / total as f64) > 0.5 {
173            return true;
174        }
175
176        if *self.append_count.lock().unwrap() >= 10000 {
177            return true;
178        }
179
180        false
181    }
182
183    pub fn compact_log(&self) -> std::io::Result<()> {
184        if self
185            .compacting
186            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
187            .is_err()
188        {
189            return Ok(()); // Already compacting. 
190        }
191
192        let temp_path = self.file_path.with_extension("tmp");
193
194        let result = (|| -> std::io::Result<()> {
195            let _lock = self.file_lock.write().unwrap();
196            
197            let input_file = File::open(self.file_path.as_ref())?;
198
199            let mut temp_file = OpenOptions::new()
200                .create(true)
201                .write(true)
202                .truncate(true)
203                .open(&temp_path)?;
204
205            let mut task_map = HashMap::new();
206            let mut _total_tasks = 0;
207            let mut _deleted_count = 0;
208
209            let reader = BufReader::new(&input_file);
210            for line_str in reader.lines().map_while(Result::ok) {
211                if line_str.trim().is_empty() {
212                    continue;
213                }
214                _total_tasks += 1;
215                if let Ok(task) = serde_json::from_str::<RetryableTask>(&line_str) {
216                    if task.deleted_at.is_some() {
217                        _deleted_count += 1;
218                        task_map.remove(&task.task_id);
219                    } else if let Some(existing) = task_map.get(&task.task_id) {
220                        let existing_task: &RetryableTask = existing;
221                        if task.updated_at > existing_task.updated_at {
222                            task_map.insert(task.task_id.clone(), task);
223                        }
224                    } else {
225                        task_map.insert(task.task_id.clone(), task);
226                    }
227                }
228            }
229
230            for task in task_map.values() {
231                let json_str = serde_json::to_string(task)?;
232                writeln!(temp_file, "{}", json_str)?;
233            }
234
235            temp_file.sync_all()?;
236            std::fs::rename(&temp_path, self.file_path.as_ref())?;
237
238            *self.total_tasks.lock().unwrap() = task_map.len();
239            *self.deleted_tasks.lock().unwrap() = 0;
240            *self.append_count.lock().unwrap() = 0;
241
242            // Update memory cache
243            let mut cache = self.tasks_cache.write().unwrap();
244            cache.clear();
245            for (id, t) in task_map.into_iter() {
246                cache.insert(id, t);
247            }
248
249            Ok(())
250        })();
251
252        self.compacting.store(false, Ordering::SeqCst);
253        result
254    }
255}