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