use crate::types::{RegionId, Time};
use std::collections::{HashMap, HashSet};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FinalizerId(pub u64);
impl fmt::Display for FinalizerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Finalizer({})", self.0)
}
}
#[derive(Debug, Clone)]
pub struct FinalizerViolation {
pub region: RegionId,
pub unrun_finalizers: Vec<FinalizerId>,
pub region_close_time: Time,
}
impl fmt::Display for FinalizerViolation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Region {} closed at {} with {} unrun finalizer(s): {:?}",
self.region,
self.region_close_time,
self.unrun_finalizers.len(),
self.unrun_finalizers
)
}
}
impl std::error::Error for FinalizerViolation {}
#[derive(Debug, Clone)]
struct FinalizerRecord {
#[allow(dead_code)] id: FinalizerId,
region: RegionId,
registered_at: Time,
}
#[derive(Debug, Default)]
pub struct FinalizerOracle {
finalizers: HashMap<FinalizerId, FinalizerRecord>,
finalizers_by_region: HashMap<RegionId, HashSet<FinalizerId>>,
ran_finalizers: HashSet<FinalizerId>,
region_closes: HashMap<RegionId, Time>,
violations: Vec<FinalizerViolation>,
next_id: u64,
}
impl FinalizerOracle {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn generate_id(&mut self) -> FinalizerId {
let id = FinalizerId(self.next_id);
self.next_id = self
.next_id
.checked_add(1)
.expect("finalizer oracle id counter exhausted");
id
}
pub fn on_register(&mut self, id: FinalizerId, region: RegionId, time: Time) {
if self
.finalizers
.get(&id)
.is_some_and(|existing| existing.region == region && existing.registered_at == time)
{
self.finalizers_by_region
.entry(region)
.or_default()
.insert(id);
return;
}
if let Some(previous) = self.finalizers.insert(
id,
FinalizerRecord {
id,
region,
registered_at: time,
},
) {
let remove_previous_region = self
.finalizers_by_region
.get_mut(&previous.region)
.is_some_and(|finalizers| {
finalizers.remove(&id);
finalizers.is_empty()
});
if remove_previous_region {
self.finalizers_by_region.remove(&previous.region);
}
}
self.ran_finalizers.remove(&id);
self.finalizers_by_region
.entry(region)
.or_default()
.insert(id);
}
pub fn on_run(&mut self, id: FinalizerId, _time: Time) {
self.ran_finalizers.insert(id);
}
pub fn on_region_close(&mut self, region: RegionId, time: Time) {
self.region_closes.insert(region, time);
let Some(finalizers) = self.finalizers_by_region.get(®ion) else {
return;
};
let mut unrun = Vec::new();
for &finalizer_id in finalizers {
if !self.ran_finalizers.contains(&finalizer_id) {
unrun.push(finalizer_id);
}
}
unrun.sort_by_key(|id| id.0);
if !unrun.is_empty() {
self.violations.push(FinalizerViolation {
region,
unrun_finalizers: unrun,
region_close_time: time,
});
}
}
pub fn check(&self) -> Result<(), FinalizerViolation> {
if let Some(violation) = self
.violations
.iter()
.min_by_key(|violation| (violation.region, violation.region_close_time))
{
return Err(violation.clone());
}
Ok(())
}
pub fn reset(&mut self) {
self.finalizers.clear();
self.finalizers_by_region.clear();
self.ran_finalizers.clear();
self.region_closes.clear();
self.violations.clear();
}
#[must_use]
pub fn registered_count(&self) -> usize {
self.finalizers.len()
}
#[must_use]
pub fn ran_count(&self) -> usize {
self.ran_finalizers.len()
}
#[must_use]
pub fn closed_region_count(&self) -> usize {
self.region_closes.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::ArenaIndex;
fn region(n: u32) -> RegionId {
RegionId::from_arena(ArenaIndex::new(n, 0))
}
fn t(nanos: u64) -> Time {
Time::from_nanos(nanos)
}
fn init_test(name: &str) {
crate::test_utils::init_test_logging();
crate::test_phase!(name);
}
#[test]
fn no_finalizers_passes() {
init_test("no_finalizers_passes");
let oracle = FinalizerOracle::new();
let ok = oracle.check().is_ok();
crate::assert_with_log!(ok, "oracle ok", true, ok);
crate::test_complete!("no_finalizers_passes");
}
#[test]
fn all_finalizers_run_passes() {
init_test("all_finalizers_run_passes");
let mut oracle = FinalizerOracle::new();
let f1 = oracle.generate_id();
let f2 = oracle.generate_id();
oracle.on_register(f1, region(0), t(10));
oracle.on_register(f2, region(0), t(20));
oracle.on_run(f1, t(50));
oracle.on_run(f2, t(60));
oracle.on_region_close(region(0), t(100));
let ok = oracle.check().is_ok();
crate::assert_with_log!(ok, "oracle ok", true, ok);
crate::test_complete!("all_finalizers_run_passes");
}
#[test]
fn unrun_finalizer_fails() {
init_test("unrun_finalizer_fails");
let mut oracle = FinalizerOracle::new();
let f1 = oracle.generate_id();
let f2 = oracle.generate_id();
oracle.on_register(f1, region(0), t(10));
oracle.on_register(f2, region(0), t(20));
oracle.on_run(f1, t(50));
oracle.on_region_close(region(0), t(100));
let result = oracle.check();
let err = result.is_err();
crate::assert_with_log!(err, "result err", true, err);
let violation = result.unwrap_err();
crate::assert_with_log!(
violation.region == region(0),
"violation region",
region(0),
violation.region
);
crate::assert_with_log!(
violation.unrun_finalizers == vec![f2],
"unrun finalizers",
vec![f2],
violation.unrun_finalizers
);
crate::assert_with_log!(
violation.region_close_time == t(100),
"region close time",
t(100),
violation.region_close_time
);
crate::test_complete!("unrun_finalizer_fails");
}
#[test]
fn no_finalizers_run_all_fail() {
init_test("no_finalizers_run_all_fail");
let mut oracle = FinalizerOracle::new();
let f1 = oracle.generate_id();
let f2 = oracle.generate_id();
oracle.on_register(f1, region(0), t(10));
oracle.on_register(f2, region(0), t(20));
oracle.on_region_close(region(0), t(100));
let result = oracle.check();
let err = result.is_err();
crate::assert_with_log!(err, "result err", true, err);
let violation = result.unwrap_err();
let count = violation.unrun_finalizers.len();
crate::assert_with_log!(count == 2, "unrun finalizers", 2, count);
crate::test_complete!("no_finalizers_run_all_fail");
}
#[test]
fn multiple_regions_independent() {
init_test("multiple_regions_independent");
let mut oracle = FinalizerOracle::new();
let f1 = oracle.generate_id();
oracle.on_register(f1, region(0), t(10));
oracle.on_run(f1, t(50));
oracle.on_region_close(region(0), t(100));
let f2 = oracle.generate_id();
oracle.on_register(f2, region(1), t(20));
oracle.on_region_close(region(1), t(100));
let result = oracle.check();
let err = result.is_err();
crate::assert_with_log!(err, "result err", true, err);
let violation = result.unwrap_err();
crate::assert_with_log!(
violation.region == region(1),
"violation region",
region(1),
violation.region
);
crate::assert_with_log!(
violation.unrun_finalizers == vec![f2],
"unrun finalizers",
vec![f2],
violation.unrun_finalizers
);
crate::test_complete!("multiple_regions_independent");
}
#[test]
fn check_reports_regions_and_finalizers_in_stable_order() {
init_test("check_reports_regions_and_finalizers_in_stable_order");
let mut oracle = FinalizerOracle::new();
let region0_f1 = oracle.generate_id();
let region0_f0 = oracle.generate_id();
let region1_f0 = oracle.generate_id();
oracle.on_register(region1_f0, region(1), t(10));
oracle.on_region_close(region(1), t(200));
oracle.on_register(region0_f1, region(0), t(20));
oracle.on_register(region0_f0, region(0), t(30));
oracle.on_region_close(region(0), t(100));
let violation = oracle
.check()
.expect_err("lower region id should be reported first");
crate::assert_with_log!(
violation.region == region(0),
"violation region",
region(0),
violation.region
);
crate::assert_with_log!(
violation.unrun_finalizers == vec![region0_f1, region0_f0],
"sorted unrun finalizers",
vec![region0_f1, region0_f0],
violation.unrun_finalizers
);
crate::assert_with_log!(
violation.region_close_time == t(100),
"region close time",
t(100),
violation.region_close_time
);
crate::test_complete!("check_reports_regions_and_finalizers_in_stable_order");
}
#[test]
fn finalizer_run_after_close_still_violates() {
init_test("finalizer_run_after_close_still_violates");
let mut oracle = FinalizerOracle::new();
let finalizer = oracle.generate_id();
oracle.on_register(finalizer, region(0), t(10));
oracle.on_region_close(region(0), t(100));
oracle.on_run(finalizer, t(110));
let violation = oracle
.check()
.expect_err("running a finalizer after close must not erase the violation");
crate::assert_with_log!(
violation.region == region(0),
"violation region",
region(0),
violation.region
);
crate::assert_with_log!(
violation.unrun_finalizers == vec![finalizer],
"unrun finalizers",
vec![finalizer],
violation.unrun_finalizers
);
crate::assert_with_log!(
violation.region_close_time == t(100),
"region close time",
t(100),
violation.region_close_time
);
crate::test_complete!("finalizer_run_after_close_still_violates");
}
#[test]
fn reregistered_finalizer_moves_out_of_previous_region() {
init_test("reregistered_finalizer_moves_out_of_previous_region");
let mut oracle = FinalizerOracle::new();
let finalizer = oracle.generate_id();
oracle.on_register(finalizer, region(0), t(10));
oracle.on_register(finalizer, region(1), t(20));
oracle.on_region_close(region(0), t(30));
let ok = oracle.check().is_ok();
crate::assert_with_log!(ok, "oracle ok", true, ok);
crate::test_complete!("reregistered_finalizer_moves_out_of_previous_region");
}
#[test]
fn reregistered_finalizer_requires_a_fresh_run() {
init_test("reregistered_finalizer_requires_a_fresh_run");
let mut oracle = FinalizerOracle::new();
let finalizer = oracle.generate_id();
oracle.on_register(finalizer, region(0), t(10));
oracle.on_run(finalizer, t(20));
oracle.on_register(finalizer, region(1), t(30));
oracle.on_region_close(region(1), t(40));
let violation = oracle
.check()
.expect_err("re-registering a finalizer must require a new run");
crate::assert_with_log!(
violation.region == region(1),
"violation region",
region(1),
violation.region
);
crate::assert_with_log!(
violation.unrun_finalizers == vec![finalizer],
"unrun finalizers",
vec![finalizer],
violation.unrun_finalizers
);
crate::assert_with_log!(
violation.region_close_time == t(40),
"region close time",
t(40),
violation.region_close_time
);
crate::test_complete!("reregistered_finalizer_requires_a_fresh_run");
}
#[test]
fn exact_duplicate_registration_preserves_completed_state() {
init_test("exact_duplicate_registration_preserves_completed_state");
let mut oracle = FinalizerOracle::new();
let finalizer = oracle.generate_id();
oracle.on_register(finalizer, region(0), t(10));
oracle.on_run(finalizer, t(20));
oracle.on_register(finalizer, region(0), t(10));
oracle.on_region_close(region(0), t(30));
let ok = oracle.check().is_ok();
crate::assert_with_log!(ok, "oracle ok", true, ok);
let ran = oracle.ran_count();
crate::assert_with_log!(ran == 1, "ran count", 1, ran);
crate::test_complete!("exact_duplicate_registration_preserves_completed_state");
}
#[test]
fn region_without_close_not_checked() {
init_test("region_without_close_not_checked");
let mut oracle = FinalizerOracle::new();
let f1 = oracle.generate_id();
oracle.on_register(f1, region(0), t(10));
let ok = oracle.check().is_ok();
crate::assert_with_log!(ok, "oracle ok", true, ok);
crate::test_complete!("region_without_close_not_checked");
}
#[test]
fn reset_clears_state() {
init_test("reset_clears_state");
let mut oracle = FinalizerOracle::new();
let f1 = oracle.generate_id();
oracle.on_register(f1, region(0), t(10));
oracle.on_region_close(region(0), t(100));
let err = oracle.check().is_err();
crate::assert_with_log!(err, "oracle err", true, err);
oracle.reset();
let ok = oracle.check().is_ok();
crate::assert_with_log!(ok, "oracle ok", true, ok);
let registered = oracle.registered_count();
crate::assert_with_log!(registered == 0, "registered count", 0, registered);
let ran = oracle.ran_count();
crate::assert_with_log!(ran == 0, "ran count", 0, ran);
crate::test_complete!("reset_clears_state");
}
#[test]
fn violation_display() {
init_test("violation_display");
let violation = FinalizerViolation {
region: region(0),
unrun_finalizers: vec![FinalizerId(1), FinalizerId(2)],
region_close_time: t(100),
};
let s = violation.to_string();
let has_region = s.contains("Region");
crate::assert_with_log!(has_region, "contains Region", true, has_region);
let has_unrun = s.contains("unrun finalizer");
crate::assert_with_log!(has_unrun, "contains unrun", true, has_unrun);
let has_two = s.contains('2');
crate::assert_with_log!(has_two, "contains 2", true, has_two);
crate::test_complete!("violation_display");
}
#[test]
fn finalizer_id_debug_clone_copy_eq_hash() {
use std::collections::HashSet;
let id = FinalizerId(42);
let id2 = id; let id3 = id;
assert_eq!(id, id2);
assert_eq!(id, id3);
assert_ne!(id, FinalizerId(99));
let dbg = format!("{id:?}");
assert!(dbg.contains("42"));
let mut set = HashSet::new();
set.insert(id);
assert!(set.contains(&id2));
}
#[test]
fn finalizer_violation_debug_clone() {
let v = FinalizerViolation {
region: region(0),
unrun_finalizers: vec![FinalizerId(1), FinalizerId(2)],
region_close_time: t(100),
};
let v2 = v.clone();
assert_eq!(v.region, v2.region);
assert_eq!(v.unrun_finalizers, v2.unrun_finalizers);
let dbg = format!("{v:?}");
assert!(dbg.contains("FinalizerViolation"));
}
}