1use std::{
5 collections::{BTreeSet, HashMap, VecDeque},
6 hash::Hash,
7 path::{Path, PathBuf},
8 sync::{Arc, Mutex, RwLock},
9};
10
11use heddle_format::compression::CompressionConfig;
12
13use super::{
14 fs_io::{AtomicWriteMode, write_atomic},
15 fs_paths::{actions_dir, blobs_dir, packs_dir, states_dir, trees_dir},
16};
17use crate::{
18 fs_atomic::sync_directory,
19 object::{Blob, ContentHash, State, StateId, Tree},
20 store::{
21 Result,
22 pack::{PackManager, PackObjectId},
23 },
24};
25
26const RECENT_BLOB_CACHE_CAPACITY: usize = 2_048;
27const RECENT_TREE_CACHE_CAPACITY: usize = 1_024;
28const VERIFIED_LOOSE_BLOB_CACHE_CAPACITY: usize = 65_536;
35pub(super) const RECENT_BLOB_CACHE_MAX_BYTES: usize = 4 * 1024 * 1024;
39pub(super) const RECENT_BLOB_CACHE_MAX_TOTAL_BYTES: usize = 256 * 1024 * 1024;
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub enum LooseObjectWriteMode {
49 Durable,
50 BatchDirectorySync,
51}
52
53#[derive(Debug)]
73pub(super) struct RecentObjectCache<K, V> {
74 entries: HashMap<K, V>,
75 order: VecDeque<K>,
76 capacity: usize,
77 byte_budget: Option<usize>,
79 sizer: fn(&V) -> usize,
82 cached_bytes: usize,
84}
85
86impl<K, V> RecentObjectCache<K, V>
87where
88 K: Copy + Eq + Hash,
89{
90 pub(super) fn with_capacity(capacity: usize) -> Self {
94 Self {
95 entries: HashMap::new(),
96 order: VecDeque::new(),
97 capacity,
98 byte_budget: None,
99 sizer: |_| 0,
100 cached_bytes: 0,
101 }
102 }
103
104 pub(super) fn with_byte_budget(
108 capacity: usize,
109 byte_budget: usize,
110 sizer: fn(&V) -> usize,
111 ) -> Self {
112 Self {
113 entries: HashMap::new(),
114 order: VecDeque::new(),
115 capacity,
116 byte_budget: Some(byte_budget),
117 sizer,
118 cached_bytes: 0,
119 }
120 }
121
122 pub(super) fn get(&mut self, key: &K) -> Option<&V> {
126 if !self.entries.contains_key(key) {
127 return None;
128 }
129 self.promote(key);
130 self.entries.get(key)
131 }
132
133 pub(super) fn contains(&self, key: &K) -> bool {
138 self.entries.contains_key(key)
139 }
140
141 #[cfg(test)]
150 pub(super) fn remove(&mut self, key: &K) -> Option<V> {
151 let removed = self.entries.remove(key)?;
152 if let Some(position) = self.order.iter().position(|existing| existing == key) {
153 self.order.remove(position);
154 }
155 self.cached_bytes = self.cached_bytes.saturating_sub((self.sizer)(&removed));
156 Some(removed)
157 }
158
159 pub(super) fn insert(&mut self, key: K, value: V) {
160 if self.capacity == 0 {
161 return;
162 }
163 let new_bytes = self.byte_budget.map(|_| (self.sizer)(&value)).unwrap_or(0);
164 if let Some(old) = self.entries.insert(key, value) {
165 self.cached_bytes = self
166 .cached_bytes
167 .saturating_sub(self.byte_budget.map(|_| (self.sizer)(&old)).unwrap_or(0));
168 self.promote(&key);
169 } else {
170 self.order.push_back(key);
171 }
172 self.cached_bytes += new_bytes;
173 self.evict_to_fit();
174 }
175
176 fn evict_to_fit(&mut self) {
183 loop {
184 let over_count = self.entries.len() > self.capacity;
185 let over_bytes = self
186 .byte_budget
187 .is_some_and(|budget| self.cached_bytes > budget && self.entries.len() > 1);
188 if !over_count && !over_bytes {
189 break;
190 }
191 let Some(oldest) = self.order.pop_front() else {
192 break;
193 };
194 if let Some(evicted) = self.entries.remove(&oldest) {
197 self.cached_bytes = self.cached_bytes.saturating_sub(
198 self.byte_budget
199 .map(|_| (self.sizer)(&evicted))
200 .unwrap_or(0),
201 );
202 }
203 }
204 }
205
206 fn promote(&mut self, key: &K) {
207 if let Some(position) = self.order.iter().position(|existing| existing == key) {
208 let key = self.order.remove(position).expect("position in range");
209 self.order.push_back(key);
210 }
211 }
212}
213
214pub struct FsStore {
235 pub(super) root: PathBuf,
236 pub(super) compression: CompressionConfig,
237 pack_manager: RwLock<PackManager>,
238 pub(super) recent_blobs: RwLock<RecentObjectCache<ContentHash, Blob>>,
239 pub(super) recent_trees: RwLock<RecentObjectCache<ContentHash, Tree>>,
240 pub(super) recent_states: RwLock<RecentObjectCache<StateId, State>>,
241 pub(super) external_source: Option<Arc<dyn super::super::ExternalObjectSource>>,
242 loose_object_write_mode: LooseObjectWriteMode,
243 snapshot_write_batch_depth: Mutex<usize>,
244 pending_directory_syncs: Mutex<BTreeSet<PathBuf>>,
245 pub(super) verified_loose_blobs: RwLock<RecentObjectCache<ContentHash, ()>>,
266}
267
268impl Clone for FsStore {
269 fn clone(&self) -> Self {
270 let mut cloned = Self::with_compression(&self.root, self.compression);
271 cloned.loose_object_write_mode = self.loose_object_write_mode;
272 cloned.external_source = self.external_source.clone();
273 cloned
274 }
275}
276
277impl FsStore {
278 pub fn new(root: impl AsRef<Path>) -> Self {
282 let root = root.as_ref().to_path_buf();
283 let pack_manager = PackManager::new(packs_dir(&root));
284 Self {
285 root,
286 compression: CompressionConfig::default(),
287 pack_manager: RwLock::new(pack_manager),
288 recent_blobs: RwLock::new(RecentObjectCache::with_byte_budget(
289 RECENT_BLOB_CACHE_CAPACITY,
290 RECENT_BLOB_CACHE_MAX_TOTAL_BYTES,
291 |blob: &Blob| blob.content().len(),
292 )),
293 recent_trees: RwLock::new(RecentObjectCache::with_capacity(RECENT_TREE_CACHE_CAPACITY)),
294 recent_states: RwLock::new(RecentObjectCache::with_capacity(
295 RECENT_TREE_CACHE_CAPACITY,
296 )),
297 external_source: None,
298 loose_object_write_mode: LooseObjectWriteMode::Durable,
299 snapshot_write_batch_depth: Mutex::new(0),
300 pending_directory_syncs: Mutex::new(BTreeSet::new()),
301 verified_loose_blobs: RwLock::new(RecentObjectCache::with_capacity(
302 VERIFIED_LOOSE_BLOB_CACHE_CAPACITY,
303 )),
304 }
305 }
306
307 pub fn with_compression(root: impl AsRef<Path>, compression: CompressionConfig) -> Self {
309 let root = root.as_ref().to_path_buf();
310 let pack_manager = PackManager::new(packs_dir(&root));
311 Self {
312 root,
313 compression,
314 pack_manager: RwLock::new(pack_manager),
315 recent_blobs: RwLock::new(RecentObjectCache::with_byte_budget(
316 RECENT_BLOB_CACHE_CAPACITY,
317 RECENT_BLOB_CACHE_MAX_TOTAL_BYTES,
318 |blob: &Blob| blob.content().len(),
319 )),
320 recent_trees: RwLock::new(RecentObjectCache::with_capacity(RECENT_TREE_CACHE_CAPACITY)),
321 recent_states: RwLock::new(RecentObjectCache::with_capacity(
322 RECENT_TREE_CACHE_CAPACITY,
323 )),
324 external_source: None,
325 loose_object_write_mode: LooseObjectWriteMode::Durable,
326 snapshot_write_batch_depth: Mutex::new(0),
327 pending_directory_syncs: Mutex::new(BTreeSet::new()),
328 verified_loose_blobs: RwLock::new(RecentObjectCache::with_capacity(
329 VERIFIED_LOOSE_BLOB_CACHE_CAPACITY,
330 )),
331 }
332 }
333
334 pub fn init(&self) -> Result<()> {
336 crate::fs_atomic::create_dir_all_durable(&blobs_dir(&self.root))?;
339 crate::fs_atomic::create_dir_all_durable(&trees_dir(&self.root))?;
340 crate::fs_atomic::create_dir_all_durable(&states_dir(&self.root))?;
341 crate::fs_atomic::create_dir_all_durable(&actions_dir(&self.root))?;
342 crate::fs_atomic::create_dir_all_durable(&packs_dir(&self.root))?;
343 Ok(())
344 }
345
346 pub fn root(&self) -> &Path {
348 &self.root
349 }
350
351 pub fn compression(&self) -> CompressionConfig {
353 self.compression
354 }
355
356 pub fn set_compression(&mut self, compression: CompressionConfig) {
358 self.compression = compression;
359 }
360
361 pub fn loose_object_write_mode(&self) -> LooseObjectWriteMode {
362 self.loose_object_write_mode
363 }
364
365 pub fn set_loose_object_write_mode(&mut self, mode: LooseObjectWriteMode) {
366 self.loose_object_write_mode = mode;
367 }
368
369 pub fn set_external_source(&mut self, source: Arc<dyn super::super::ExternalObjectSource>) {
372 self.external_source = Some(source);
373 }
374
375 fn flush_pending_directory_syncs(&self) -> Result<usize> {
376 let pending_dirs = {
377 let mut guard = self.pending_directory_syncs.lock().map_err(|_| {
378 crate::store::HeddleError::Config(
379 "Failed to acquire pending directory sync lock".to_string(),
380 )
381 })?;
382 if guard.is_empty() {
383 return Ok(0);
384 }
385 let dirs = guard.iter().cloned().collect::<Vec<_>>();
386 guard.clear();
387 dirs
388 };
389
390 for (index, dir) in pending_dirs.iter().enumerate() {
391 if let Err(error) = sync_directory(dir) {
392 if let Ok(mut guard) = self.pending_directory_syncs.lock() {
393 guard.extend(pending_dirs[index..].iter().cloned());
394 }
395 return Err(error.into());
396 }
397 }
398
399 Ok(pending_dirs.len())
400 }
401
402 pub fn reload_packs(&self) -> Result<()> {
408 let packs = packs_dir(&self.root);
409 let _ = super::pack_install_journal::recover_pack_install_intents_with_ttl(
410 &packs,
411 Some(super::pack_install_journal::DEFAULT_PACK_INSTALL_INTENT_TTL_SECS),
412 )?;
413 let _ = super::fs_pack::prune_unpaired_pack_files(&packs)?;
415 let mut manager = self.pack_manager.write().map_err(|_| {
416 crate::store::HeddleError::Config("Failed to acquire pack manager lock".to_string())
417 })?;
418 manager.reload()
419 }
420
421 pub(super) fn reload_packs_if_stale(&self) -> Result<bool> {
436 {
438 let manager = self.pack_manager.read().map_err(|_| {
439 crate::store::HeddleError::Config("Failed to acquire pack manager lock".to_string())
440 })?;
441 if !manager.needs_reload()? {
442 return Ok(false);
443 }
444 }
445 let mut manager = self.pack_manager.write().map_err(|_| {
449 crate::store::HeddleError::Config("Failed to acquire pack manager lock".to_string())
450 })?;
451 manager.reload_if_disk_grew()
452 }
453
454 pub fn pack_manager(&self) -> &RwLock<PackManager> {
456 &self.pack_manager
457 }
458
459 pub fn clear_recent_object_caches(&self) {
460 if let Ok(mut blobs) = self.recent_blobs.write() {
461 *blobs = RecentObjectCache::with_byte_budget(
462 RECENT_BLOB_CACHE_CAPACITY,
463 RECENT_BLOB_CACHE_MAX_TOTAL_BYTES,
464 |blob: &Blob| blob.content().len(),
465 );
466 }
467 if let Ok(mut trees) = self.recent_trees.write() {
468 *trees = RecentObjectCache::with_capacity(RECENT_TREE_CACHE_CAPACITY);
469 }
470 if let Ok(mut states) = self.recent_states.write() {
471 *states = RecentObjectCache::with_capacity(RECENT_TREE_CACHE_CAPACITY);
472 }
473 }
474
475 #[cfg(test)]
483 pub(super) fn evict_recent_blob(&self, hash: &ContentHash) {
484 if let Ok(mut cache) = self.recent_blobs.write() {
485 cache.remove(hash);
486 }
487 }
488
489 pub fn pack_ids(&self) -> Result<Vec<PackObjectId>> {
490 let manager = self.pack_manager.read().map_err(|_| {
491 crate::store::HeddleError::Config("Failed to acquire pack manager lock".to_string())
492 })?;
493 manager.list_all_ids()
494 }
495
496 pub(super) fn write_loose_object_atomic(&self, path: &Path, data: &[u8]) -> Result<()> {
497 let batch_active = self.snapshot_write_batch_depth.lock().map_err(|_| {
498 crate::store::HeddleError::Config("Failed to acquire snapshot batch lock".to_string())
499 })?;
500 let configured_mode = if *batch_active > 0 {
501 LooseObjectWriteMode::BatchDirectorySync
502 } else {
503 self.loose_object_write_mode
504 };
505 drop(batch_active);
506
507 let mode = match configured_mode {
508 LooseObjectWriteMode::Durable => AtomicWriteMode::Durable,
509 LooseObjectWriteMode::BatchDirectorySync => AtomicWriteMode::BatchDirectorySync,
510 };
511 write_atomic(path, data, mode, Some(&self.pending_directory_syncs))
512 }
513
514 #[allow(dead_code)]
518 pub(super) fn write_pack_atomic(&self, path: &Path, data: &[u8]) -> Result<()> {
519 write_atomic(path, data, AtomicWriteMode::Durable, None)
520 }
521
522 pub(super) fn write_loose_object_cache(&self, path: &Path, data: &[u8]) -> Result<()> {
543 write_atomic(path, data, AtomicWriteMode::NoSync, None)
544 }
545
546 pub(super) fn begin_snapshot_write_batch_impl(&self) -> Result<()> {
547 let mut depth = self.snapshot_write_batch_depth.lock().map_err(|_| {
548 crate::store::HeddleError::Config("Failed to acquire snapshot batch lock".to_string())
549 })?;
550 *depth += 1;
551 Ok(())
552 }
553
554 pub(super) fn flush_snapshot_write_batch_impl(&self) -> Result<()> {
555 let should_flush = {
556 let mut depth = self.snapshot_write_batch_depth.lock().map_err(|_| {
557 crate::store::HeddleError::Config(
558 "Failed to acquire snapshot batch lock".to_string(),
559 )
560 })?;
561 if *depth == 0 {
562 return Ok(());
563 }
564 *depth -= 1;
565 *depth == 0
566 };
567
568 if should_flush {
569 let _ = self.flush_pending_directory_syncs()?;
570 }
571
572 Ok(())
573 }
574
575 pub(super) fn abort_snapshot_write_batch_impl(&self) {
576 if let Ok(mut depth) = self.snapshot_write_batch_depth.lock() {
577 *depth = 0;
578 }
579 if let Ok(mut pending) = self.pending_directory_syncs.lock() {
580 pending.clear();
581 }
582 }
583
584 #[cfg(test)]
585 pub(super) fn pending_directory_sync_count(&self) -> usize {
586 self.pending_directory_syncs
587 .lock()
588 .map(|pending| pending.len())
589 .unwrap_or(0)
590 }
591}