Skip to main content

orbok_workers/
cleanup_service.rs

1//! End-to-end cleanup service (M10, RFC-011 §11): combines catalog-side
2//! cleanup (via [`CleanupExecutor`]) with cache-side cleanup (via
3//! [`CacheService`]), driven by a validated [`CleanupPlan`].
4//!
5//! Call `CleanupService::run_safe` for ordinary cleanup; it will never
6//! touch persistent source settings. For destructive operations use
7//! `run_reset` with an explicit confirmation token.
8
9use orbok_cache::CacheService;
10use orbok_core::{CleanupAction, CleanupPlan, OrbokResult};
11use orbok_db::Catalog;
12use orbok_db::repo::CleanupExecutor;
13use std::path::Path;
14use tracing::info;
15
16/// Combined cleanup outcome (catalog + cache sides).
17#[derive(Debug, Default)]
18pub struct FullCleanupOutcome {
19    pub catalog_rows_deleted: u64,
20    /// Approximate cache bytes freed (0 if cache cleanup is not applicable).
21    pub cache_bytes_freed: u64,
22}
23
24/// Orchestrates catalog and cache cleanup (RFC-011 §8).
25pub struct CleanupService<'a> {
26    catalog: &'a Catalog,
27    cache: &'a CacheService,
28    cache_db_path: &'a Path,
29}
30
31impl<'a> CleanupService<'a> {
32    pub fn new(catalog: &'a Catalog, cache: &'a CacheService, cache_db_path: &'a Path) -> Self {
33        Self {
34            catalog,
35            cache,
36            cache_db_path,
37        }
38    }
39
40    /// Safe cleanup: validates the plan cannot touch persistent data, then
41    /// runs catalog-side and cache-side operations atomically in intent
42    /// (RFC-011 §8 "lifecycle-aware cleanup").
43    pub fn run_safe(&self, plan: &CleanupPlan) -> OrbokResult<FullCleanupOutcome> {
44        // Catalog side.
45        let catalog_outcome = CleanupExecutor::new(self.catalog).run_safe(plan)?;
46        info!(
47            action = ?plan.action,
48            rows = catalog_outcome.deleted_rows,
49            "catalog cleanup completed"
50        );
51
52        // Cache side: map CleanupAction to cache namespace operations.
53        let cache_bytes_freed = self.run_cache_side(plan)?;
54        if cache_bytes_freed > 0 {
55            info!(bytes = cache_bytes_freed, "cache cleanup freed space");
56        }
57
58        Ok(FullCleanupOutcome {
59            catalog_rows_deleted: catalog_outcome.deleted_rows,
60            cache_bytes_freed,
61        })
62    }
63
64    /// Destructive catalog reset (requires confirmed ResetCatalog plan).
65    pub fn run_reset(
66        &self,
67        plan: &CleanupPlan,
68        keep_settings: bool,
69    ) -> OrbokResult<FullCleanupOutcome> {
70        // Catalog reset.
71        let catalog_outcome =
72            CleanupExecutor::new(self.catalog).run_reset_catalog(plan, keep_settings)?;
73
74        // Purge all cache namespaces (RFC-011 §13: full reset clears caches).
75        let cache_bytes_freed = self.purge_all_cache_namespaces()?;
76
77        info!(
78            rows = catalog_outcome.deleted_rows,
79            cache_freed = cache_bytes_freed,
80            "catalog reset completed"
81        );
82
83        Ok(FullCleanupOutcome {
84            catalog_rows_deleted: catalog_outcome.deleted_rows,
85            cache_bytes_freed,
86        })
87    }
88
89    fn run_cache_side(&self, plan: &CleanupPlan) -> OrbokResult<u64> {
90        use orbok_cache::{EngineOptions, OrbokCacheNamespace};
91
92        let size_before = self.cache_db_path.metadata().map(|m| m.len()).unwrap_or(0);
93
94        match plan.action {
95            CleanupAction::ClearSnippetCache | CleanupAction::ClearExpiredSearchCache => {
96                // Purge the preview-cache namespace.
97                let engine = self.cache.engine::<Vec<u8>>(
98                    self.catalog,
99                    &OrbokCacheNamespace::PreviewCache,
100                    EngineOptions::default(),
101                )?;
102                engine
103                    .cleanup_expired()
104                    .map_err(|e| orbok_core::OrbokError::Cache(e.to_string()))?;
105                engine
106                    .shrink_database()
107                    .map_err(|e| orbok_core::OrbokError::Cache(e.to_string()))?;
108            }
109            CleanupAction::ClearTemporaryExtraction
110            | CleanupAction::RemoveTemporarySourceIndexes => {
111                // Purge extract-segments namespace.
112                let engine = self.cache.engine::<Vec<u8>>(
113                    self.catalog,
114                    &OrbokCacheNamespace::ExtractSegments,
115                    EngineOptions::default(),
116                )?;
117                engine
118                    .purge_stale_versions()
119                    .map_err(|e| orbok_core::OrbokError::Cache(e.to_string()))?;
120                engine
121                    .cleanup_missing_files()
122                    .map_err(|e| orbok_core::OrbokError::Cache(e.to_string()))?;
123            }
124            CleanupAction::RemoveReplacedStaleIndexes => {
125                // Clean up chunk and embedding bundle caches.
126                for ns in [
127                    OrbokCacheNamespace::ChunkBundle,
128                    OrbokCacheNamespace::ExtractSegments,
129                ] {
130                    let engine = self.cache.engine::<Vec<u8>>(
131                        self.catalog,
132                        &ns,
133                        EngineOptions::default(),
134                    )?;
135                    engine
136                        .cleanup_missing_files()
137                        .map_err(|e| orbok_core::OrbokError::Cache(e.to_string()))?;
138                }
139            }
140            _ => {}
141        }
142
143        let size_after = self.cache_db_path.metadata().map(|m| m.len()).unwrap_or(0);
144        Ok(size_before.saturating_sub(size_after))
145    }
146
147    fn purge_all_cache_namespaces(&self) -> OrbokResult<u64> {
148        use orbok_cache::{EngineOptions, OrbokCacheNamespace};
149        let size_before = self.cache_db_path.metadata().map(|m| m.len()).unwrap_or(0);
150        for ns in [
151            OrbokCacheNamespace::ExtractSegments,
152            OrbokCacheNamespace::ChunkBundle,
153            OrbokCacheNamespace::PreviewCache,
154        ] {
155            let engine =
156                self.cache
157                    .engine::<Vec<u8>>(self.catalog, &ns, EngineOptions::default())?;
158            let _ = engine.purge_stale_versions();
159            let _ = engine.cleanup_expired();
160            let _ = engine.shrink_database();
161        }
162        let size_after = self.cache_db_path.metadata().map(|m| m.len()).unwrap_or(0);
163        Ok(size_before.saturating_sub(size_after))
164    }
165}