use crate::query::ast::FreshnessCondition;
use crate::query::ResultRow;
#[derive(Debug, Clone)]
pub struct FreshnessConfig {
pub threshold: f32,
pub decay_rates: Vec<(String, f32)>,
pub default_decay_rate: f32,
}
impl Default for FreshnessConfig {
fn default() -> Self {
Self {
threshold: 0.5,
decay_rates: Vec::new(),
default_decay_rate: 0.01,
}
}
}
#[derive(Debug, Clone)]
pub struct FreshnessAnnotatedRow {
pub row: ResultRow,
pub freshness: f32,
pub is_stale: bool,
}
pub fn annotate_freshness(
rows: &[ResultRow],
config: &FreshnessConfig,
now_micros: u64,
) -> Vec<FreshnessAnnotatedRow> {
rows.iter()
.map(|row| {
let decay_rate = find_decay_rate(&row.key, config);
let age_micros = now_micros.saturating_sub(row.timestamp);
let age_secs = age_micros as f32 / 1_000_000.0;
let freshness = (-decay_rate * age_secs).exp();
let is_stale = freshness < config.threshold;
FreshnessAnnotatedRow {
row: row.clone(),
freshness,
is_stale,
}
})
.collect()
}
pub fn filter_by_freshness(
rows: Vec<ResultRow>,
condition: &FreshnessCondition,
config: &FreshnessConfig,
now_micros: u64,
) -> Vec<ResultRow> {
let annotated = annotate_freshness(&rows, config, now_micros);
annotated
.into_iter()
.filter(|a| matches_freshness_condition(a, condition, config))
.map(|a| a.row)
.collect()
}
pub fn sort_by_freshness(
rows: &mut [ResultRow],
config: &FreshnessConfig,
now_micros: u64,
descending: bool,
) {
rows.sort_by(|a, b| {
let fa = compute_freshness(a, config, now_micros);
let fb = compute_freshness(b, config, now_micros);
if descending {
fb.partial_cmp(&fa).unwrap_or(std::cmp::Ordering::Equal)
} else {
fa.partial_cmp(&fb).unwrap_or(std::cmp::Ordering::Equal)
}
});
}
pub fn count_stale(rows: &[ResultRow], config: &FreshnessConfig, now_micros: u64) -> usize {
annotate_freshness(rows, config, now_micros)
.iter()
.filter(|a| a.is_stale)
.count()
}
fn compute_freshness(row: &ResultRow, config: &FreshnessConfig, now_micros: u64) -> f32 {
let decay_rate = find_decay_rate(&row.key, config);
let age_micros = now_micros.saturating_sub(row.timestamp);
let age_secs = age_micros as f32 / 1_000_000.0;
(-decay_rate * age_secs).exp()
}
fn find_decay_rate(key: &str, config: &FreshnessConfig) -> f32 {
for (prefix, rate) in &config.decay_rates {
if key.starts_with(prefix.as_str()) {
return *rate;
}
}
config.default_decay_rate
}
fn matches_freshness_condition(
annotated: &FreshnessAnnotatedRow,
condition: &FreshnessCondition,
_config: &FreshnessConfig,
) -> bool {
match condition {
FreshnessCondition::Fresh => !annotated.is_stale,
FreshnessCondition::Stale => annotated.is_stale,
FreshnessCondition::Compare { op, value } => {
use crate::query::ast::ComparisonOp;
let f = annotated.freshness as f64;
match op {
ComparisonOp::Gt => f > *value,
ComparisonOp::Gte => f >= *value,
ComparisonOp::Lt => f < *value,
ComparisonOp::Lte => f <= *value,
ComparisonOp::Eq => (f - *value).abs() < 0.001,
ComparisonOp::Ne => (f - *value).abs() >= 0.001,
_ => true,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::query::ast::ComparisonOp;
use crate::types::Atom;
fn make_row(key: &str, ts: u64) -> ResultRow {
ResultRow {
key: key.to_string(),
value: Atom::Float(1.0),
timestamp: ts,
}
}
#[test]
fn test_fresh_data_has_high_freshness() {
let now = 1_000_000_000u64; let row = make_row("sensor/temp", now - 1_000_000); let config = FreshnessConfig {
default_decay_rate: 0.01,
..Default::default()
};
let annotated = annotate_freshness(&[row], &config, now);
assert_eq!(annotated.len(), 1);
assert!(annotated[0].freshness > 0.99); assert!(!annotated[0].is_stale);
}
#[test]
fn test_old_data_is_stale() {
let now = 1_000_000_000u64;
let row = make_row("sensor/temp", now - 100_000_000); let config = FreshnessConfig {
default_decay_rate: 0.01,
threshold: 0.5,
..Default::default()
};
let annotated = annotate_freshness(&[row], &config, now);
assert!(annotated[0].freshness < 0.5);
assert!(annotated[0].is_stale);
}
#[test]
fn test_filter_fresh_only() {
let now = 1_000_000_000u64;
let rows = vec![
make_row("fresh", now - 1_000_000), make_row("stale", now - 200_000_000), ];
let config = FreshnessConfig {
default_decay_rate: 0.01,
threshold: 0.5,
..Default::default()
};
let filtered = filter_by_freshness(rows, &FreshnessCondition::Fresh, &config, now);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].key, "fresh");
}
#[test]
fn test_filter_stale_only() {
let now = 1_000_000_000u64;
let rows = vec![
make_row("fresh", now - 1_000_000),
make_row("stale", now - 200_000_000),
];
let config = FreshnessConfig::default();
let filtered = filter_by_freshness(rows, &FreshnessCondition::Stale, &config, now);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].key, "stale");
}
#[test]
fn test_filter_by_threshold() {
let now = 1_000_000_000u64;
let rows = vec![
make_row("high", now - 1_000_000), make_row("medium", now - 50_000_000), make_row("low", now - 200_000_000), ];
let config = FreshnessConfig::default();
let condition = FreshnessCondition::Compare {
op: ComparisonOp::Gt,
value: 0.5,
};
let filtered = filter_by_freshness(rows, &condition, &config, now);
assert_eq!(filtered.len(), 2); }
#[test]
fn test_sort_by_freshness() {
let now = 1_000_000_000u64;
let mut rows = vec![
make_row("old", now - 100_000_000),
make_row("new", now - 1_000_000),
make_row("mid", now - 50_000_000),
];
let config = FreshnessConfig::default();
sort_by_freshness(&mut rows, &config, now, true); assert_eq!(rows[0].key, "new"); assert_eq!(rows[2].key, "old"); }
#[test]
fn test_custom_decay_rate_per_prefix() {
let now = 1_000_000_000u64;
let rows = vec![
make_row("sensor/temp", now - 10_000_000), make_row("config/name", now - 10_000_000), ];
let config = FreshnessConfig {
threshold: 0.5,
decay_rates: vec![
("sensor/".to_string(), 0.1), ("config/".to_string(), 0.001), ],
default_decay_rate: 0.01,
};
let annotated = annotate_freshness(&rows, &config, now);
assert!(annotated[0].is_stale);
assert!(!annotated[1].is_stale);
}
#[test]
fn test_count_stale() {
let now = 1_000_000_000u64;
let rows = vec![
make_row("a", now - 1_000_000),
make_row("b", now - 200_000_000),
make_row("c", now - 300_000_000),
];
let config = FreshnessConfig::default();
let stale = count_stale(&rows, &config, now);
assert_eq!(stale, 2); }
}