use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
use crate::grid::durable_store::DurableValueStore;
use crate::grid::hardening::ValueStoreError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DurableScrubConfig {
pub records_per_cycle: usize,
pub interval: Duration,
}
impl DurableScrubConfig {
pub fn new(records_per_cycle: usize, interval: Duration) -> Self {
Self {
records_per_cycle: records_per_cycle.max(1),
interval: if interval.is_zero() {
Duration::from_secs(1)
} else {
interval
},
}
}
}
impl Default for DurableScrubConfig {
fn default() -> Self {
Self::new(128, Duration::from_secs(60))
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
pub struct DurableScrubMetrics {
pub durable_scrub_records_total: u64,
pub durable_scrub_corruption_total: u64,
pub durable_scrub_cycle_seconds: f64,
pub durable_scrub_cursor_gauge: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DurableScrubCorruption {
pub key: String,
pub error: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DurableScrubReport {
pub checked_keys: Vec<String>,
pub records_checked: usize,
pub corruption_count: usize,
pub corruptions: Vec<DurableScrubCorruption>,
pub cursor: Option<String>,
pub finished_pass: bool,
pub elapsed: Duration,
}
impl DurableScrubReport {
pub fn is_clean(&self) -> bool {
self.corruption_count == 0
}
}
#[derive(Debug, Clone)]
pub struct DurableScrubber {
config: DurableScrubConfig,
cursor: Option<String>,
metrics: DurableScrubMetrics,
}
impl DurableScrubber {
pub fn new(config: DurableScrubConfig) -> Self {
Self {
config,
cursor: None,
metrics: DurableScrubMetrics::default(),
}
}
pub fn interval(&self) -> Duration {
self.config.interval
}
pub fn cursor(&self) -> Option<&str> {
self.cursor.as_deref()
}
pub fn metrics(&self) -> DurableScrubMetrics {
self.metrics
}
pub fn scrub_cycle(
&mut self,
store: &DurableValueStore,
) -> Result<DurableScrubReport, ValueStoreError> {
let started = Instant::now();
let limit = self.config.records_per_cycle;
let mut batch = store.raw_record_batch_after(self.cursor.as_deref(), limit + 1)?;
let has_more = batch.len() > limit;
if has_more {
batch.truncate(limit);
}
let mut checked_keys = Vec::with_capacity(batch.len());
let mut corruptions = Vec::new();
let mut last_key = None;
for raw in batch {
let key = raw.key;
checked_keys.push(key.clone());
last_key = Some(key.clone());
if let Err(error) = DurableValueStore::decode_raw_record(&key, &raw.bytes) {
corruptions.push(DurableScrubCorruption {
key,
error: error.to_string(),
});
}
}
let finished_pass = !has_more;
self.cursor = if finished_pass { None } else { last_key };
let elapsed = started.elapsed();
self.metrics.durable_scrub_records_total = self
.metrics
.durable_scrub_records_total
.saturating_add(checked_keys.len() as u64);
self.metrics.durable_scrub_corruption_total = self
.metrics
.durable_scrub_corruption_total
.saturating_add(corruptions.len() as u64);
self.metrics.durable_scrub_cycle_seconds = elapsed.as_secs_f64();
self.metrics.durable_scrub_cursor_gauge = cursor_gauge(self.cursor.as_deref());
Ok(DurableScrubReport {
records_checked: checked_keys.len(),
corruption_count: corruptions.len(),
checked_keys,
corruptions,
cursor: self.cursor.clone(),
finished_pass,
elapsed,
})
}
}
impl Default for DurableScrubber {
fn default() -> Self {
Self::new(DurableScrubConfig::default())
}
}
fn cursor_gauge(cursor: Option<&str>) -> u64 {
let Some(cursor) = cursor else {
return 0;
};
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
cursor.as_bytes().iter().fold(OFFSET, |hash, byte| {
(hash ^ u64::from(*byte)).wrapping_mul(PRIME)
})
}