use crate::types::Budget;
use std::future::Future;
use std::pin::Pin;
pub enum Finalizer {
Sync(Box<dyn FnOnce() + Send>),
Async(Pin<Box<dyn Future<Output = ()> + Send>>),
}
impl std::fmt::Debug for Finalizer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Sync(_) => f.debug_tuple("Sync").field(&"<closure>").finish(),
Self::Async(_) => f.debug_tuple("Async").field(&"<future>").finish(),
}
}
}
pub const FINALIZER_POLL_BUDGET: u32 = 100;
pub const FINALIZER_TIME_BUDGET_NANOS: u64 = 5_000_000_000;
#[must_use]
#[inline]
pub fn finalizer_budget() -> Budget {
Budget::new().with_poll_quota(FINALIZER_POLL_BUDGET)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FinalizerEscalation {
Soft,
#[default]
BoundedLog,
BoundedPanic,
}
impl FinalizerEscalation {
#[inline]
#[must_use]
pub const fn allows_continuation(self) -> bool {
matches!(self, Self::BoundedLog)
}
#[inline]
#[must_use]
pub const fn is_soft(self) -> bool {
matches!(self, Self::Soft)
}
}
#[derive(Debug, Default)]
pub struct FinalizerStack {
finalizers: Vec<Finalizer>,
escalation: FinalizerEscalation,
}
impl FinalizerStack {
#[must_use]
#[inline]
pub fn new() -> Self {
Self::default()
}
#[must_use]
#[inline]
pub fn with_escalation(escalation: FinalizerEscalation) -> Self {
Self {
finalizers: Vec::new(),
escalation,
}
}
#[must_use]
#[inline]
pub const fn escalation(&self) -> FinalizerEscalation {
self.escalation
}
#[inline]
pub fn push(&mut self, finalizer: Finalizer) {
self.finalizers.push(finalizer);
}
#[inline]
pub fn pop(&mut self) -> Option<Finalizer> {
self.finalizers.pop()
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.finalizers.len()
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.finalizers.is_empty()
}
pub fn push_sync<F>(&mut self, f: F)
where
F: FnOnce() + Send + 'static,
{
self.push(Finalizer::Sync(Box::new(f)));
}
pub fn push_async<F>(&mut self, future: F)
where
F: Future<Output = ()> + Send + 'static,
{
self.push(Finalizer::Async(Box::pin(future)));
}
}
#[cfg(test)]
mod tests {
use super::*;
use parking_lot::Mutex;
fn finalizer_policy_table() -> String {
[
FinalizerEscalation::Soft,
FinalizerEscalation::BoundedLog,
FinalizerEscalation::BoundedPanic,
]
.into_iter()
.map(|policy| {
format!(
"{policy:?}|soft={}|continue={}|polls={}|time_ns={}",
policy.is_soft(),
policy.allows_continuation(),
FINALIZER_POLL_BUDGET,
FINALIZER_TIME_BUDGET_NANOS
)
})
.collect::<Vec<_>>()
.join("\n")
}
fn init_test(name: &str) {
crate::test_utils::init_test_logging();
crate::test_phase!(name);
}
#[test]
fn finalizer_stack_lifo_order() {
init_test("finalizer_stack_lifo_order");
let mut stack = FinalizerStack::new();
let order = std::sync::Arc::new(Mutex::new(Vec::new()));
let o1 = order.clone();
let o2 = order.clone();
let o3 = order.clone();
stack.push_sync(move || o1.lock().push(1));
stack.push_sync(move || o2.lock().push(2));
stack.push_sync(move || o3.lock().push(3));
while let Some(finalizer) = stack.pop() {
if let Finalizer::Sync(f) = finalizer {
f();
}
}
let order = order.lock().clone();
crate::assert_with_log!(order == vec![3, 2, 1], "order", vec![3, 2, 1], order);
crate::test_complete!("finalizer_stack_lifo_order");
}
#[test]
fn finalizer_stack_empty() {
init_test("finalizer_stack_empty");
let mut stack = FinalizerStack::new();
let empty = stack.is_empty();
crate::assert_with_log!(empty, "empty", true, empty);
let len = stack.len();
crate::assert_with_log!(len == 0, "len", 0, len);
let pop = stack.pop();
crate::assert_with_log!(pop.is_none(), "pop none", true, pop.is_none());
crate::test_complete!("finalizer_stack_empty");
}
#[test]
fn finalizer_escalation_policies() {
init_test("finalizer_escalation_policies");
let soft = FinalizerEscalation::Soft.is_soft();
crate::assert_with_log!(soft, "soft is soft", true, soft);
let log_soft = FinalizerEscalation::BoundedLog.is_soft();
crate::assert_with_log!(!log_soft, "log not soft", false, log_soft);
let panic_soft = FinalizerEscalation::BoundedPanic.is_soft();
crate::assert_with_log!(!panic_soft, "panic not soft", false, panic_soft);
let log_cont = FinalizerEscalation::BoundedLog.allows_continuation();
crate::assert_with_log!(log_cont, "log allows", true, log_cont);
let soft_cont = FinalizerEscalation::Soft.allows_continuation();
crate::assert_with_log!(!soft_cont, "soft no continue", false, soft_cont);
let panic_cont = FinalizerEscalation::BoundedPanic.allows_continuation();
crate::assert_with_log!(!panic_cont, "panic no continue", false, panic_cont);
crate::test_complete!("finalizer_escalation_policies");
}
#[test]
fn finalizer_budget_has_expected_values() {
init_test("finalizer_budget_has_expected_values");
let budget = finalizer_budget();
crate::assert_with_log!(
budget.poll_quota == FINALIZER_POLL_BUDGET,
"poll_quota",
FINALIZER_POLL_BUDGET,
budget.poll_quota
);
crate::test_complete!("finalizer_budget_has_expected_values");
}
#[test]
fn finalizer_debug_impl() {
init_test("finalizer_debug_impl");
let sync_finalizer = Finalizer::Sync(Box::new(|| {}));
let debug_str = format!("{sync_finalizer:?}");
let sync_debug_present = debug_str.contains("Sync");
crate::assert_with_log!(sync_debug_present, "sync debug", true, sync_debug_present);
let async_finalizer = Finalizer::Async(Box::pin(async {}));
let debug_str = format!("{async_finalizer:?}");
let async_debug_present = debug_str.contains("Async");
crate::assert_with_log!(
async_debug_present,
"async debug",
true,
async_debug_present
);
crate::test_complete!("finalizer_debug_impl");
}
#[test]
fn finalizer_escalation_debug_clone_copy_eq_default() {
let e = FinalizerEscalation::BoundedLog;
let dbg = format!("{e:?}");
assert!(dbg.contains("BoundedLog"), "{dbg}");
let copied = e;
let cloned = e;
assert_eq!(copied, cloned);
let def = FinalizerEscalation::default();
assert_eq!(def, FinalizerEscalation::BoundedLog);
}
#[test]
fn finalizer_stack_debug_default() {
let stack = FinalizerStack::default();
let dbg = format!("{stack:?}");
assert!(dbg.contains("FinalizerStack"), "{dbg}");
assert!(stack.is_empty());
}
#[test]
fn finalizer_policy_table_snapshot() {
insta::assert_snapshot!("finalizer_policy_table", finalizer_policy_table());
}
}