use std::sync::Arc;
use std::time::Instant;
use parking_lot::Mutex;
use super::config::{FreezeConfig, FreezeDecisionEngine, FreezeDecisionInput};
pub struct FreezeGuard {
manager: Arc<BackgroundFreezeManager>,
start_time: Instant,
edges_frozen: u64,
completed: bool,
}
impl FreezeGuard {
pub fn new(manager: Arc<BackgroundFreezeManager>) -> Self {
Self {
manager,
start_time: Instant::now(),
edges_frozen: 0,
completed: false,
}
}
pub fn record_edges(&mut self, count: u64) {
self.edges_frozen = count;
self.completed = true;
}
pub fn finish(&mut self) {
if self.completed {
let duration_ms = self.start_time.elapsed().as_millis() as u64;
self.manager.record_freeze(self.edges_frozen, duration_ms);
log::info!(
"Freeze completed: {} edges in {}ms",
self.edges_frozen,
duration_ms
);
}
}
}
impl Drop for FreezeGuard {
fn drop(&mut self) {
self.finish();
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FreezeStats {
pub freeze_count: u64,
pub total_frozen_edges: u64,
pub last_freeze_duration_ms: u64,
pub current_delta_edges: u64,
}
#[derive(Debug, Clone)]
pub struct FreezeDecision {
pub should_freeze: bool,
pub current_delta_edges: u64,
pub edge_threshold: u64,
pub current_delta_memory_bytes: u64,
pub memory_threshold_bytes: u64,
pub freeze_reason: FreezeReason,
}
impl FreezeDecision {
pub fn summary(&self) -> String {
let edge_pct = if self.edge_threshold > 0 {
(self.current_delta_edges as f64 / self.edge_threshold as f64 * 100.0) as u64
} else {
0
};
let mem_pct = if self.memory_threshold_bytes > 0 {
(self.current_delta_memory_bytes as f64 / self.memory_threshold_bytes as f64 * 100.0)
as u64
} else {
0
};
format!(
"FreezeDecision {{ should_freeze: {}, edges: {} ({}/{} = {}%), memory: {}MB ({}/{}MB = {}%), reason: {:?} }}",
self.should_freeze,
self.current_delta_edges,
self.current_delta_edges,
self.edge_threshold,
edge_pct,
self.current_delta_memory_bytes / (1024 * 1024),
self.current_delta_memory_bytes / (1024 * 1024),
self.memory_threshold_bytes / (1024 * 1024),
mem_pct,
self.freeze_reason,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FreezeReason {
None,
EdgeCountExceeded,
MemoryExceeded,
DeletionExceeded,
Both,
}
pub struct BackgroundFreezeManager {
decision_engine: FreezeDecisionEngine,
stats: Arc<Mutex<FreezeStats>>,
}
impl BackgroundFreezeManager {
pub fn from_config(config: FreezeConfig) -> Self {
let decision_engine = FreezeDecisionEngine::new(config);
Self {
decision_engine,
stats: Arc::new(Mutex::new(FreezeStats::default())),
}
}
pub fn should_freeze_with_stats(&self, input: &FreezeDecisionInput) -> bool {
self.decision_engine.should_freeze(input)
}
pub fn get_freeze_decision_with_stats(&self, input: &FreezeDecisionInput) -> FreezeDecision {
let edge_exceeded =
input.delta_edge_count >= self.decision_engine.config.delta_edge_threshold;
let memory_exceeded =
input.delta_memory_bytes >= self.decision_engine.config.delta_memory_threshold_bytes;
let deletion_exceeded =
input.deletion_ratio >= self.decision_engine.config.deletion_threshold;
let freeze_reason = if deletion_exceeded {
FreezeReason::DeletionExceeded
} else {
match (edge_exceeded, memory_exceeded) {
(true, true) => FreezeReason::Both,
(true, false) => FreezeReason::EdgeCountExceeded,
(false, true) => FreezeReason::MemoryExceeded,
(false, false) => FreezeReason::None,
}
};
FreezeDecision {
should_freeze: self.should_freeze_with_stats(input),
current_delta_edges: input.delta_edge_count,
edge_threshold: self.decision_engine.config.delta_edge_threshold,
current_delta_memory_bytes: input.delta_memory_bytes,
memory_threshold_bytes: self.decision_engine.config.delta_memory_threshold_bytes,
freeze_reason,
}
}
pub fn get_stats(&self) -> FreezeStats {
*self.stats.lock()
}
pub(crate) fn record_freeze(&self, edges_frozen: u64, duration_ms: u64) {
let mut stats = self.stats.lock();
stats.freeze_count += 1;
stats.total_frozen_edges += edges_frozen;
stats.last_freeze_duration_ms = duration_ms;
}
pub(crate) fn record_delta_size(&self, delta_edges: u64) {
let mut stats = self.stats.lock();
stats.current_delta_edges = delta_edges;
}
pub fn get_reason(&self, input: &FreezeDecisionInput) -> String {
self.decision_engine.get_reason(input)
}
pub fn get_config(&self) -> &FreezeConfig {
&self.decision_engine.config
}
}
impl Default for BackgroundFreezeManager {
fn default() -> Self {
Self::from_config(FreezeConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_config() -> FreezeConfig {
FreezeConfig {
delta_edge_threshold: 100_000,
delta_memory_threshold_bytes: 256 * 1024 * 1024,
deletion_threshold: 0.5,
}
}
fn input(edges: u64, mem_mb: u64, deletion_ratio: f64) -> FreezeDecisionInput {
FreezeDecisionInput {
delta_edge_count: edges,
delta_memory_bytes: mem_mb * 1024 * 1024,
deletion_ratio,
}
}
#[test]
fn test_freeze_decision_below_both_thresholds() {
let manager = BackgroundFreezeManager::from_config(test_config());
let decision = manager.get_freeze_decision_with_stats(&input(50_000, 100, 0.1));
assert!(!decision.should_freeze);
assert_eq!(decision.freeze_reason, FreezeReason::None);
}
#[test]
fn test_freeze_decision_edge_count_exceeded() {
let manager = BackgroundFreezeManager::from_config(test_config());
let decision = manager.get_freeze_decision_with_stats(&input(150_000, 100, 0.1));
assert!(decision.should_freeze);
assert_eq!(decision.freeze_reason, FreezeReason::EdgeCountExceeded);
}
#[test]
fn test_freeze_decision_memory_exceeded() {
let manager = BackgroundFreezeManager::from_config(test_config());
let decision = manager.get_freeze_decision_with_stats(&input(50_000, 300, 0.1));
assert!(decision.should_freeze);
assert_eq!(decision.freeze_reason, FreezeReason::MemoryExceeded);
}
#[test]
fn test_freeze_decision_both_exceeded() {
let manager = BackgroundFreezeManager::from_config(test_config());
let decision = manager.get_freeze_decision_with_stats(&input(150_000, 300, 0.1));
assert!(decision.should_freeze);
assert_eq!(decision.freeze_reason, FreezeReason::Both);
}
#[test]
fn test_freeze_decision_deletion_exceeded() {
let manager = BackgroundFreezeManager::from_config(test_config());
let decision = manager.get_freeze_decision_with_stats(&input(50_000, 100, 0.6));
assert!(decision.should_freeze);
assert_eq!(decision.freeze_reason, FreezeReason::DeletionExceeded);
}
#[test]
fn test_should_freeze_with_stats_method() {
let manager = BackgroundFreezeManager::default();
assert!(!manager.should_freeze_with_stats(&input(50_000, 100, 0.1)));
assert!(manager.should_freeze_with_stats(&input(150_000, 100, 0.1)));
assert!(manager.should_freeze_with_stats(&input(50_000, 100, 0.3)));
}
#[test]
fn test_record_freeze() {
let manager = BackgroundFreezeManager::default();
manager.record_freeze(50_000, 100);
let stats = manager.get_stats();
assert_eq!(stats.freeze_count, 1);
assert_eq!(stats.total_frozen_edges, 50_000);
assert_eq!(stats.last_freeze_duration_ms, 100);
manager.record_freeze(30_000, 50);
let stats = manager.get_stats();
assert_eq!(stats.freeze_count, 2);
assert_eq!(stats.total_frozen_edges, 80_000);
assert_eq!(stats.last_freeze_duration_ms, 50);
}
#[test]
fn test_record_delta_size() {
let manager = BackgroundFreezeManager::default();
manager.record_delta_size(12_000);
let stats = manager.get_stats();
assert_eq!(stats.current_delta_edges, 12_000);
manager.record_delta_size(25_000);
let stats = manager.get_stats();
assert_eq!(stats.current_delta_edges, 25_000);
}
#[test]
fn test_manager_creation_and_stats() {
let manager = BackgroundFreezeManager::from_config(test_config());
let stats = manager.get_stats();
assert_eq!(stats.freeze_count, 0);
assert_eq!(stats.total_frozen_edges, 0);
assert_eq!(stats.current_delta_edges, 0);
}
#[test]
fn test_config_access() {
let config = FreezeConfig::development();
let manager = BackgroundFreezeManager::from_config(config.clone());
let retrieved_config = manager.get_config();
assert_eq!(retrieved_config.delta_edge_threshold, 50_000);
assert_eq!(
retrieved_config.delta_memory_threshold_bytes,
128 * 1024 * 1024
);
}
#[test]
fn test_default_config() {
let manager = BackgroundFreezeManager::default();
let config = manager.get_config();
assert_eq!(
config.delta_edge_threshold, 100_000,
"Default edge threshold should be 100K edges"
);
assert_eq!(
config.delta_memory_threshold_bytes,
256 * 1024 * 1024,
"Default memory threshold should be 256MB"
);
}
#[test]
fn test_freeze_guard_records_automatically() {
let manager = Arc::new(BackgroundFreezeManager::default());
let stats_before = manager.get_stats();
assert_eq!(stats_before.freeze_count, 0);
{
let mut guard = FreezeGuard::new(manager.clone());
guard.record_edges(50_000);
}
let stats_after = manager.get_stats();
assert_eq!(stats_after.freeze_count, 1);
assert_eq!(stats_after.total_frozen_edges, 50_000);
assert!(stats_after.last_freeze_duration_ms < 1000); }
#[test]
fn test_freeze_guard_without_edges_does_nothing() {
let manager = Arc::new(BackgroundFreezeManager::default());
{
let _guard = FreezeGuard::new(manager.clone());
}
let stats = manager.get_stats();
assert_eq!(stats.freeze_count, 0);
}
#[test]
fn test_freeze_guard_multiple() {
let manager = Arc::new(BackgroundFreezeManager::default());
{
let mut guard = FreezeGuard::new(manager.clone());
guard.record_edges(30_000);
}
{
let mut guard = FreezeGuard::new(manager.clone());
guard.record_edges(40_000);
}
let stats = manager.get_stats();
assert_eq!(stats.freeze_count, 2);
assert_eq!(stats.total_frozen_edges, 70_000);
}
#[test]
fn test_freeze_guard_manual_finish() {
let manager = Arc::new(BackgroundFreezeManager::default());
let mut guard = FreezeGuard::new(manager.clone());
guard.record_edges(50_000);
guard.finish();
let stats = manager.get_stats();
assert_eq!(stats.freeze_count, 1);
assert_eq!(stats.total_frozen_edges, 50_000);
}
}