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}