use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use radixdb_catalog::ResourcePolicy;
use crate::{Diagnostic, DiagnosticKind, ProceduralResult};
#[derive(Debug, Clone)]
pub struct CancellationHandle(Arc<AtomicBool>);
impl CancellationHandle {
pub fn cancel(&self) {
self.0.store(true, Ordering::Release);
}
pub fn is_cancelled(&self) -> bool {
self.0.load(Ordering::Acquire)
}
}
pub trait CancellationProbe: Send + Sync {
fn is_cancelled(&self) -> bool;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct BudgetSnapshot {
pub instructions: u64,
pub heap_bytes: u64,
pub live_frames: u32,
pub sql_statements: u64,
pub rows: u64,
pub result_bytes: u64,
}
#[derive(Debug)]
struct BudgetState {
usage: BudgetSnapshot,
}
struct BudgetShared {
limits: ResourcePolicy,
state: Mutex<BudgetState>,
started: Instant,
cancelled: Arc<AtomicBool>,
parent_cancellation: Option<Arc<dyn CancellationProbe>>,
}
impl std::fmt::Debug for BudgetShared {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("BudgetShared")
.field("limits", &self.limits)
.field("state", &self.state)
.field("started", &self.started)
.field("cancelled", &self.cancelled)
.field(
"has_parent_cancellation",
&self.parent_cancellation.is_some(),
)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct BudgetOwner(Arc<BudgetShared>);
impl BudgetOwner {
pub fn new(limits: ResourcePolicy) -> ProceduralResult<Self> {
Self::new_inner(limits, None)
}
pub fn with_parent_cancellation(
limits: ResourcePolicy,
parent_cancellation: Arc<dyn CancellationProbe>,
) -> ProceduralResult<Self> {
Self::new_inner(limits, Some(parent_cancellation))
}
fn new_inner(
limits: ResourcePolicy,
parent_cancellation: Option<Arc<dyn CancellationProbe>>,
) -> ProceduralResult<Self> {
let limits = limits.validate().map_err(|error| {
Diagnostic::new(
DiagnosticKind::RuntimeInvalidIr,
format!("invalid catalog resource policy: {error}"),
)
})?;
Ok(Self(Arc::new(BudgetShared {
limits,
state: Mutex::new(BudgetState {
usage: BudgetSnapshot::default(),
}),
started: Instant::now(),
cancelled: Arc::new(AtomicBool::new(false)),
parent_cancellation,
})))
}
pub fn limits(&self) -> ResourcePolicy {
self.0.limits
}
pub fn cancellation_handle(&self) -> CancellationHandle {
CancellationHandle(Arc::clone(&self.0.cancelled))
}
pub fn snapshot(&self) -> BudgetSnapshot {
self.0.state.lock().unwrap().usage
}
pub fn check_boundary(&self) -> ProceduralResult<()> {
if self.0.cancelled.load(Ordering::Acquire)
|| self
.0
.parent_cancellation
.as_ref()
.is_some_and(|probe| probe.is_cancelled())
{
return Err(Diagnostic::new(
DiagnosticKind::ResourceCancelled,
"procedural call was cancelled",
));
}
if self.0.started.elapsed() >= Duration::from_millis(self.0.limits.deadline_ms) {
return Err(Diagnostic::new(
DiagnosticKind::ResourceDeadline,
"procedural call deadline expired",
));
}
Ok(())
}
pub fn remaining_deadline(&self) -> ProceduralResult<Duration> {
self.check_boundary()?;
Duration::from_millis(self.0.limits.deadline_ms)
.checked_sub(self.0.started.elapsed())
.ok_or_else(|| {
Diagnostic::new(
DiagnosticKind::ResourceDeadline,
"procedural call deadline expired",
)
})
}
pub fn charge_instructions(&self, amount: u64) -> ProceduralResult<()> {
self.charge_u64(
amount,
self.0.limits.instructions,
DiagnosticKind::ResourceInstructions,
|usage| &mut usage.instructions,
"executed instruction budget exceeded",
)
}
pub fn charge_sql_statement(&self) -> ProceduralResult<()> {
self.charge_u64(
1,
self.0.limits.sql_statements,
DiagnosticKind::ResourceSqlStatements,
|usage| &mut usage.sql_statements,
"SQL statement budget exceeded",
)
}
pub fn charge_rows(&self, rows: u64) -> ProceduralResult<()> {
self.charge_u64(
rows,
self.0.limits.rows,
DiagnosticKind::ResourceRows,
|usage| &mut usage.rows,
"row budget exceeded",
)
}
pub fn charge_result_bytes(&self, bytes: u64) -> ProceduralResult<()> {
self.charge_u64(
bytes,
self.0.limits.result_bytes,
DiagnosticKind::ResourceBytes,
|usage| &mut usage.result_bytes,
"result byte budget exceeded",
)
}
pub fn charge_heap(&self, bytes: u64) -> ProceduralResult<()> {
self.charge_u64(
bytes,
self.0.limits.heap_bytes,
DiagnosticKind::ResourceHeap,
|usage| &mut usage.heap_bytes,
"procedural heap budget exceeded",
)
}
pub fn release_heap(&self, bytes: u64) {
let mut state = self.0.state.lock().unwrap();
state.usage.heap_bytes = state.usage.heap_bytes.saturating_sub(bytes);
}
pub(crate) fn enter_frame(&self) -> ProceduralResult<FrameLease> {
self.check_boundary()?;
{
let mut state = self.0.state.lock().unwrap();
let next = state.usage.live_frames.checked_add(1).ok_or_else(|| {
Diagnostic::new(DiagnosticKind::ResourceFrames, "frame counter overflow")
})?;
if next > self.0.limits.frames {
return Err(limit_error(
DiagnosticKind::ResourceFrames,
"live frame budget exceeded",
u64::from(next),
u64::from(self.0.limits.frames),
));
}
state.usage.live_frames = next;
}
Ok(FrameLease {
owner: self.clone(),
})
}
fn charge_u64(
&self,
amount: u64,
limit: u64,
kind: DiagnosticKind,
select: impl FnOnce(&mut BudgetSnapshot) -> &mut u64,
message: &'static str,
) -> ProceduralResult<()> {
let mut state = self.0.state.lock().unwrap();
let counter = select(&mut state.usage);
let attempted = counter
.checked_add(amount)
.ok_or_else(|| limit_error(kind, message, u64::MAX, limit))?;
if attempted > limit {
return Err(limit_error(kind, message, attempted, limit));
}
*counter = attempted;
Ok(())
}
}
#[derive(Debug)]
pub(crate) struct FrameLease {
owner: BudgetOwner,
}
impl Drop for FrameLease {
fn drop(&mut self) {
let mut state = self.owner.0.state.lock().unwrap();
state.usage.live_frames = state.usage.live_frames.saturating_sub(1);
}
}
fn limit_error(
kind: DiagnosticKind,
message: &'static str,
attempted: u64,
limit: u64,
) -> Diagnostic {
Diagnostic::new(kind, message)
.with_detail("attempted", attempted.to_string())
.with_detail("limit", limit.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clones_spend_one_shared_budget() {
let mut policy = ResourcePolicy::default_call();
policy.instructions = 3;
let owner = BudgetOwner::new(policy).unwrap();
owner.charge_instructions(2).unwrap();
let child = owner.clone();
child.charge_instructions(1).unwrap();
assert_eq!(owner.snapshot().instructions, 3);
assert_eq!(
child.charge_instructions(1).unwrap_err().kind(),
DiagnosticKind::ResourceInstructions
);
}
#[test]
fn cancellation_is_shared_and_checked_at_boundaries() {
let owner = BudgetOwner::new(ResourcePolicy::default_call()).unwrap();
owner.cancellation_handle().cancel();
assert_eq!(
owner.check_boundary().unwrap_err().kind(),
DiagnosticKind::ResourceCancelled
);
}
#[test]
fn frame_limit_failure_does_not_leak_live_frame_usage() {
let mut policy = ResourcePolicy::default_call();
policy.frames = 1;
let owner = BudgetOwner::new(policy).unwrap();
let frame = owner.enter_frame().unwrap();
assert_eq!(owner.snapshot().live_frames, 1);
assert_eq!(
owner.enter_frame().unwrap_err().kind(),
DiagnosticKind::ResourceFrames
);
assert_eq!(owner.snapshot().live_frames, 1);
drop(frame);
assert_eq!(owner.snapshot().live_frames, 0);
}
#[test]
fn cancelled_frame_entry_does_not_charge_a_frame() {
let owner = BudgetOwner::new(ResourcePolicy::default_call()).unwrap();
owner.cancellation_handle().cancel();
assert_eq!(
owner.enter_frame().unwrap_err().kind(),
DiagnosticKind::ResourceCancelled
);
assert_eq!(owner.snapshot().live_frames, 0);
}
#[derive(Debug)]
struct ParentCancellation(AtomicBool);
impl CancellationProbe for ParentCancellation {
fn is_cancelled(&self) -> bool {
self.0.load(Ordering::Acquire)
}
}
#[test]
fn caller_cancellation_stops_the_shared_budget() {
let parent = Arc::new(ParentCancellation(AtomicBool::new(false)));
let owner =
BudgetOwner::with_parent_cancellation(ResourcePolicy::default_call(), parent.clone())
.unwrap();
owner.check_boundary().unwrap();
parent.0.store(true, Ordering::Release);
assert_eq!(
owner.check_boundary().unwrap_err().kind(),
DiagnosticKind::ResourceCancelled
);
}
}