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}