use std::collections::VecDeque;
use dashmap::DashMap;
pub const DEFAULT_MAX_ITERATIONS: u32 = 20;
pub const HISTORY_DEPTH: usize = 6;
#[derive(Default)]
pub struct OscillationDetector {
history: DashMap<String, VecDeque<u64>>,
}
impl OscillationDetector {
pub fn new() -> Self {
Self::default()
}
pub fn check_and_record(&self, surface_id: &str, patch_hash: u64) -> bool {
let mut entry = self
.history
.entry(surface_id.to_string())
.or_insert_with(|| VecDeque::with_capacity(HISTORY_DEPTH));
if entry.iter().any(|h| *h == patch_hash) {
return false;
}
if entry.len() >= HISTORY_DEPTH {
entry.pop_front();
}
entry.push_back(patch_hash);
true
}
pub fn reset(&self, surface_id: &str) {
self.history.remove(surface_id);
}
#[cfg(test)]
pub fn depth(&self, surface_id: &str) -> usize {
self.history
.get(surface_id)
.map(|e| e.len())
.unwrap_or(0)
}
}
pub struct IterationBudget {
counts: DashMap<String, u32>,
max_iterations: u32,
}
impl IterationBudget {
pub fn new() -> Self {
Self::with_max(DEFAULT_MAX_ITERATIONS)
}
pub fn with_max(max: u32) -> Self {
Self {
counts: DashMap::new(),
max_iterations: max,
}
}
pub fn try_consume(&self, surface_id: &str) -> bool {
let mut entry = self.counts.entry(surface_id.to_string()).or_insert(0);
if *entry >= self.max_iterations {
return false;
}
*entry += 1;
true
}
pub fn refund(&self, surface_id: &str) {
let mut entry = self.counts.entry(surface_id.to_string()).or_insert(0);
*entry = entry.saturating_sub(1);
}
pub fn reset(&self, surface_id: &str) {
self.counts.remove(surface_id);
}
pub fn count(&self, surface_id: &str) -> u32 {
self.counts.get(surface_id).map(|c| *c).unwrap_or(0)
}
pub fn max(&self) -> u32 {
self.max_iterations
}
}
impl Default for IterationBudget {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_patch_records_and_applies() {
let det = OscillationDetector::new();
assert!(det.check_and_record("s", 42));
assert_eq!(det.depth("s"), 1);
}
#[test]
fn same_patch_repeated_blocks_apply() {
let det = OscillationDetector::new();
assert!(det.check_and_record("s", 42));
assert!(!det.check_and_record("s", 42));
assert_eq!(det.depth("s"), 1);
}
#[test]
fn distinct_patches_compose_until_window_fills() {
let det = OscillationDetector::new();
for i in 0..(HISTORY_DEPTH as u64) {
assert!(det.check_and_record("s", i));
}
assert_eq!(det.depth("s"), HISTORY_DEPTH);
let next = HISTORY_DEPTH as u64;
assert!(det.check_and_record("s", next));
assert_eq!(det.depth("s"), HISTORY_DEPTH);
assert!(det.check_and_record("s", 0));
}
#[test]
fn detects_a_b_a_pattern() {
let det = OscillationDetector::new();
let a: u64 = 0xAAAA;
let b: u64 = 0xBBBB;
assert!(det.check_and_record("s", a)); assert!(det.check_and_record("s", b)); assert!(det.check_and_record("s", 0xCCCC));
assert!(!det.check_and_record("s", a));
}
#[test]
fn reset_clears_history() {
let det = OscillationDetector::new();
det.check_and_record("s", 1);
det.check_and_record("s", 2);
det.reset("s");
assert_eq!(det.depth("s"), 0);
assert!(det.check_and_record("s", 1));
}
#[test]
fn iteration_budget_starts_full() {
let b = IterationBudget::with_max(3);
assert_eq!(b.count("s"), 0);
assert!(b.try_consume("s"));
assert_eq!(b.count("s"), 1);
}
#[test]
fn iteration_budget_caps_after_max() {
let b = IterationBudget::with_max(2);
assert!(b.try_consume("s"));
assert!(b.try_consume("s"));
assert!(!b.try_consume("s"));
assert!(!b.try_consume("s"));
assert_eq!(b.count("s"), 2);
}
#[test]
fn iteration_budget_reset_clears() {
let b = IterationBudget::with_max(2);
b.try_consume("s");
b.try_consume("s");
assert!(!b.try_consume("s"));
b.reset("s");
assert!(b.try_consume("s"));
assert_eq!(b.count("s"), 1);
}
#[test]
fn iteration_budget_per_surface() {
let b = IterationBudget::with_max(1);
assert!(b.try_consume("a"));
assert!(!b.try_consume("a"));
assert!(b.try_consume("b"), "budget is per-surface");
}
#[test]
fn iteration_budget_refund_releases_slot() {
let b = IterationBudget::with_max(2);
assert!(b.try_consume("s"));
assert!(b.try_consume("s"));
assert!(!b.try_consume("s"), "at cap");
b.refund("s");
assert!(b.try_consume("s"), "refund opens a slot back up");
}
#[test]
fn iteration_budget_refund_saturates_at_zero() {
let b = IterationBudget::with_max(2);
b.refund("s");
b.refund("s");
assert_eq!(b.count("s"), 0);
assert!(b.try_consume("s"));
}
#[test]
fn iteration_budget_concurrent_consume_respects_cap() {
use std::sync::Arc;
let b = Arc::new(IterationBudget::with_max(10));
let mut handles = vec![];
for _ in 0..32 {
let b = b.clone();
handles.push(std::thread::spawn(move || b.try_consume("s")));
}
let successes = handles
.into_iter()
.map(|h| h.join().unwrap())
.filter(|ok| *ok)
.count();
assert_eq!(successes, 10, "exactly cap-many consumes must succeed");
assert_eq!(b.count("s"), 10);
}
#[test]
fn per_surface_history_isolated() {
let det = OscillationDetector::new();
det.check_and_record("a", 42);
det.check_and_record("b", 42);
assert!(!det.check_and_record("a", 42));
assert!(!det.check_and_record("b", 42));
}
}