Skip to main content

cqlite_core/storage/sstable/reader/
integrity.rs

1//! Integrity checking and health monitoring methods for SSTableReader
2//!
3//! This module contains methods for checking SSTable integrity, monitoring health,
4//! and handling tombstone filtering.
5
6use super::super::verify::{self, VerifyMode};
7use super::{IntegrityCheckResult, IntegrityStatus, SSTableReader, SSTableReaderHealthMetrics};
8use crate::types::{ScanRow, Value};
9use crate::Result;
10
11use log::{debug, info};
12
13#[cfg(feature = "tombstones")]
14use super::super::tombstone_merger::GenerationValue;
15
16#[cfg(feature = "tombstones")]
17use crate::{types::TableId, RowKey};
18
19#[cfg(feature = "tombstones")]
20use log::warn;
21
22impl SSTableReader {
23    /// Get comprehensive reader health and performance metrics
24    pub async fn get_health_metrics(&self) -> Result<SSTableReaderHealthMetrics> {
25        let stats = self.stats().await?;
26
27        // Calculate actual cache hit rate from atomic counters
28        let cache_hit_rate = self.calculate_cache_hit_rate();
29
30        let memory_usage = self.estimate_memory_usage();
31
32        Ok(SSTableReaderHealthMetrics {
33            file_path: self.file_path.clone(),
34            file_accessible: self.file_path.exists(),
35            header_version: self.header.cassandra_version,
36            total_file_size: stats.file_size,
37            estimated_memory_usage: memory_usage,
38            block_cache_entries: self.block_cache.len(),
39            block_cache_hit_rate: cache_hit_rate,
40            compression_enabled: self.compression_reader.is_some(),
41            compression_algorithm: self.header.compression.algorithm.clone(),
42            bloom_filter_enabled: self.bloom_filter.is_some(),
43            index_available: self.index.is_some(),
44            generation: self.generation,
45            last_error: None,
46        })
47    }
48
49    /// Perform an integrity check on the SSTable.
50    ///
51    /// Issue #1283: this is a THIN PROJECTION over `verify::verify_sstable` — the
52    /// single source of truth for SSTable integrity — not an independent check
53    /// pipeline. The legacy implementation walked only `Data.db` blocks, so a
54    /// corrupt `Index.db` / `Digest.crc32` / `Summary.db` / `Filter.db` or
55    /// out-of-order keys (all of which the verifier FAILs) read back `Healthy`
56    /// here — a divergent verdict. We now run the authoritative verifier in
57    /// `Full` mode over the reader's EXACT generation (`self.file_path`, not merely
58    /// its parent directory — roborev #1283) and map its `VerifyReport` onto the
59    /// legacy `IntegrityCheckResult` shape the (test-only) consumers expect.
60    pub async fn perform_integrity_check(&self) -> Result<IntegrityCheckResult> {
61        debug!("Starting integrity check for {:?}", self.file_path);
62
63        // Delegate to the authoritative engine, verifying the EXACT generation this
64        // reader is opened on (issue #1283, roborev). The directory may hold several
65        // generations; `verify_sstable` resolves the lexicographically-first
66        // `*-Data.db`, which would report the wrong SSTable's integrity here.
67        // `verify_sstable_generation` verifies precisely `self.file_path`'s
68        // generation. We use the SAME Config/Platform the reader was opened with.
69        // Data corruption is reported as findings inside an Ok(report); only
70        // environmental problems return Err.
71        let report = verify::verify_sstable_generation(
72            &self.file_path,
73            VerifyMode::Full,
74            &self.open_config,
75            self.platform.clone(),
76        )
77        .await?;
78
79        // Project VerifyReport -> IntegrityCheckResult.
80        //  - any finding  => Corrupted; none => Healthy (no Degraded — issue #1283).
81        //  - rows_scanned => total_entries.
82        //  - findings' rendered strings => parsing_errors.
83        //  - per-block indices (corrupted_blocks/unreadable_blocks/total_blocks_checked)
84        //    are not produced by the verifier and no production consumer reads them,
85        //    so they stay best-effort empty/zero.
86        let parsing_errors: Vec<String> = report.findings.iter().map(|f| f.to_string()).collect();
87        let overall_status = if report.findings.is_empty() {
88            IntegrityStatus::Healthy
89        } else {
90            IntegrityStatus::Corrupted
91        };
92
93        // `checksum_mismatches` is a deprecated, always-0 compatibility field
94        // (issue #1283): the projection never populates it. The narrow allow keeps
95        // clippy `-D warnings` clean while the dead computation stays removed.
96        #[allow(deprecated)]
97        let result = IntegrityCheckResult {
98            file_path: self.file_path.clone(),
99            total_blocks_checked: 0,
100            corrupted_blocks: Vec::new(),
101            checksum_mismatches: 0,
102            unreadable_blocks: 0,
103            total_entries: report.rows_scanned.unwrap_or(0),
104            parsing_errors,
105            overall_status,
106        };
107
108        info!(
109            "Integrity check completed for {:?}: {:?}, {} findings, {} rows scanned",
110            self.file_path,
111            result.overall_status,
112            result.parsing_errors.len(),
113            result.total_entries
114        );
115
116        Ok(result)
117    }
118
119    /// Enhanced tombstone filtering using TombstoneMerger
120    #[cfg(feature = "tombstones")]
121    pub(super) fn filter_tombstone(&self, row: &ScanRow) -> bool {
122        // Issue #1334: a live row (`ScanRow::Row`) is always kept here — row-level
123        // tombstone suppression only applies to markers. (Cell tombstones inside a
124        // live row are preserved for callers to inspect.) Only a marker carries a
125        // `Value` whose tombstone/TTL semantics this filter evaluates.
126        let value = match row {
127            // A live row (decoded or raw undecoded fallback) is always kept.
128            ScanRow::Row(_) | ScanRow::RawRow(_) => return true,
129            ScanRow::Marker(v) => v,
130        };
131        // Use the fast tombstone check for performance
132        let write_time = self.extract_write_time_from_value(value);
133
134        if self
135            .tombstone_merger
136            .fast_tombstone_check(value, write_time)
137        {
138            // Value is deleted by tombstone
139            return false;
140        }
141
142        // Check for TTL expiration on regular values
143        if let Some(ttl) = self.extract_ttl_from_value(value) {
144            let current_time = std::time::SystemTime::now()
145                .duration_since(std::time::UNIX_EPOCH)
146                .map(|d| d.as_micros() as i64)
147                .unwrap_or_else(|e| {
148                    warn!("Failed to get system time: {}; using fallback value 0", e);
149                    0
150                });
151
152            if current_time > write_time + ttl {
153                // Value has expired
154                return false;
155            }
156        }
157
158        true // Keep valid, non-deleted values
159    }
160
161    /// Simple tombstone filtering (fallback when tombstones feature is disabled).
162    ///
163    /// Row tombstones (`Value::Tombstone(RowTombstone)`) are always filtered out of
164    /// user-facing scan/get results, regardless of the `tombstones` feature flag.
165    /// This prevents deleted rows that are still present on disk (either from a live
166    /// SSTable that contains a tombstone entry, or from a post-compaction SSTable
167    /// that preserved tombstone rows for GC purposes) from appearing in query results.
168    ///
169    /// Cell tombstones (`Value::Tombstone(CellTombstone)`) within a Map are NOT
170    /// filtered here — they are preserved so callers can inspect them.  If a caller
171    /// needs to suppress null-cell entries, it should do so at the query layer.
172    ///
173    /// (Issue #505)
174    #[cfg(not(feature = "tombstones"))]
175    pub(super) fn filter_tombstone(&self, row: &ScanRow) -> bool {
176        use crate::types::TombstoneType;
177        // Issue #1334: a live row (`ScanRow::Row`) is always kept; filter out only a
178        // row-level tombstone marker.
179        !matches!(
180            row,
181            ScanRow::Marker(Value::Tombstone(info))
182                if info.tombstone_type == TombstoneType::RowTombstone
183        )
184    }
185
186    /// Enhanced multi-generation tombstone filtering for compaction
187    #[cfg(feature = "tombstones")]
188    pub async fn filter_with_multi_generation_merge(
189        &self,
190        table_id: &TableId,
191        entries: Vec<(RowKey, Vec<GenerationValue>)>,
192    ) -> Result<Vec<(RowKey, ScanRow)>> {
193        let mut results = Vec::new();
194
195        log::debug!(
196            "Processing {} key groups for multi-generation merge",
197            entries.len()
198        );
199
200        // Use batch processing for better performance
201        const BATCH_SIZE: usize = 1000;
202
203        let batches: Vec<_> = entries.chunks(BATCH_SIZE).collect();
204
205        for (batch_idx, batch) in batches.iter().enumerate() {
206            log::debug!(
207                "Processing batch {}/{} with {} entries",
208                batch_idx + 1,
209                batches.len(),
210                batch.len()
211            );
212
213            let batch_entries = batch.to_vec();
214            let merged_results = self
215                .tombstone_merger
216                .batch_merge_with_tombstones(batch_entries, BATCH_SIZE)?;
217
218            for (key, merged_value) in merged_results {
219                if let Some(value) = merged_value {
220                    if self.should_include_value_after_merge(&value, table_id, &key)? {
221                        results.push((key, value));
222                    }
223                } else {
224                    // Value was completely tombstoned
225                    log::debug!("Value for key {:?} was completely tombstoned", key);
226                }
227            }
228        }
229
230        log::debug!(
231            "Multi-generation merge completed: {} final results from {} input groups",
232            results.len(),
233            entries.len()
234        );
235
236        Ok(results)
237    }
238
239    /// Enhanced filtering logic for post-merge values including collection validation
240    #[cfg(feature = "tombstones")]
241    fn should_include_value_after_merge(
242        &self,
243        row: &ScanRow,
244        _table_id: &TableId,
245        _key: &RowKey,
246    ) -> Result<bool> {
247        // Issue #1334: the merge now yields whole rows. A live row with at least one
248        // cell is included; a marker (row tombstone / null row) or an empty row is
249        // suppressed.
250        match row {
251            ScanRow::Row(cells) => Ok(!cells.is_empty()),
252            // A raw undecoded fallback row carries live bytes → included.
253            ScanRow::RawRow(bytes) => Ok(!bytes.is_empty()),
254            ScanRow::Marker(_) => Ok(false),
255        }
256    }
257
258    /// Extract TTL from value metadata
259    #[cfg(feature = "tombstones")]
260    fn extract_ttl_from_value(&self, value: &Value) -> Option<i64> {
261        match value {
262            Value::Tombstone(info) => info.ttl,
263            _ => None, // Regular values would have TTL in SSTable metadata
264        }
265    }
266
267    /// Extract write time from value
268    #[cfg(feature = "tombstones")]
269    fn extract_write_time_from_value(&self, value: &Value) -> i64 {
270        match value {
271            Value::Tombstone(info) => info.deletion_time,
272            _ => std::time::SystemTime::now()
273                .duration_since(std::time::UNIX_EPOCH)
274                .map(|d| d.as_micros() as i64)
275                .unwrap_or_else(|e| {
276                    warn!("Failed to get system time: {}; using fallback value 0", e);
277                    0
278                }),
279        }
280    }
281}