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 { catalog, cache, cache_db_path }
34    }
35
36    /// Safe cleanup: validates the plan cannot touch persistent data, then
37    /// runs catalog-side and cache-side operations atomically in intent
38    /// (RFC-011 §8 "lifecycle-aware cleanup").
39    pub fn run_safe(&self, plan: &CleanupPlan) -> OrbokResult<FullCleanupOutcome> {
40        // Catalog side.
41        let catalog_outcome = CleanupExecutor::new(self.catalog).run_safe(plan)?;
42        info!(
43            action = ?plan.action,
44            rows = catalog_outcome.deleted_rows,
45            "catalog cleanup completed"
46        );
47
48        // Cache side: map CleanupAction to cache namespace operations.
49        let cache_bytes_freed = self.run_cache_side(plan)?;
50        if cache_bytes_freed > 0 {
51            info!(bytes = cache_bytes_freed, "cache cleanup freed space");
52        }
53
54        Ok(FullCleanupOutcome {
55            catalog_rows_deleted: catalog_outcome.deleted_rows,
56            cache_bytes_freed,
57        })
58    }
59
60    /// Destructive catalog reset (requires confirmed ResetCatalog plan).
61    pub fn run_reset(
62        &self,
63        plan: &CleanupPlan,
64        keep_settings: bool,
65    ) -> OrbokResult<FullCleanupOutcome> {
66        // Catalog reset.
67        let catalog_outcome =
68            CleanupExecutor::new(self.catalog).run_reset_catalog(plan, keep_settings)?;
69
70        // Purge all cache namespaces (RFC-011 §13: full reset clears caches).
71        let cache_bytes_freed = self.purge_all_cache_namespaces()?;
72
73        info!(
74            rows = catalog_outcome.deleted_rows,
75            cache_freed = cache_bytes_freed,
76            "catalog reset completed"
77        );
78
79        Ok(FullCleanupOutcome {
80            catalog_rows_deleted: catalog_outcome.deleted_rows,
81            cache_bytes_freed,
82        })
83    }
84
85    fn run_cache_side(&self, plan: &CleanupPlan) -> OrbokResult<u64> {
86        use orbok_cache::{EngineOptions, OrbokCacheNamespace};
87
88        let size_before = self
89            .cache_db_path
90            .metadata()
91            .map(|m| m.len())
92            .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.cleanup_expired().map_err(|e| {
103                    orbok_core::OrbokError::Cache(e.to_string())
104                })?;
105                engine.shrink_database().map_err(|e| {
106                    orbok_core::OrbokError::Cache(e.to_string())
107                })?;
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.purge_stale_versions().map_err(|e| {
118                    orbok_core::OrbokError::Cache(e.to_string())
119                })?;
120                engine.cleanup_missing_files().map_err(|e| {
121                    orbok_core::OrbokError::Cache(e.to_string())
122                })?;
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.cleanup_missing_files().map_err(|e| {
136                        orbok_core::OrbokError::Cache(e.to_string())
137                    })?;
138                }
139            }
140            _ => {}
141        }
142
143        let size_after = self
144            .cache_db_path
145            .metadata()
146            .map(|m| m.len())
147            .unwrap_or(0);
148        Ok(size_before.saturating_sub(size_after))
149    }
150
151    fn purge_all_cache_namespaces(&self) -> OrbokResult<u64> {
152        use orbok_cache::{EngineOptions, OrbokCacheNamespace};
153        let size_before = self.cache_db_path.metadata().map(|m| m.len()).unwrap_or(0);
154        for ns in [
155            OrbokCacheNamespace::ExtractSegments,
156            OrbokCacheNamespace::ChunkBundle,
157            OrbokCacheNamespace::PreviewCache,
158        ] {
159            let engine = self.cache.engine::<Vec<u8>>(
160                self.catalog,
161                &ns,
162                EngineOptions::default(),
163            )?;
164            let _ = engine.purge_stale_versions();
165            let _ = engine.cleanup_expired();
166            let _ = engine.shrink_database();
167        }
168        let size_after = self.cache_db_path.metadata().map(|m| m.len()).unwrap_or(0);
169        Ok(size_before.saturating_sub(size_after))
170    }
171}