1use std::collections::{BTreeMap, BTreeSet};
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::{Arc, Mutex, MutexGuard};
4use std::time::{Duration, Instant};
5
6#[cfg(feature = "tokio")]
7use tokio_util::sync::CancellationToken;
8
9use crate::{
10 BudgetBuildError, BudgetError, BudgetId, BudgetSnapshot, Remaining, Reservation,
11 ReservationSet, Resource, ResourceSnapshot,
12};
13
14static NEXT_BUDGET_ID: AtomicU64 = AtomicU64::new(1);
15
16#[derive(Clone, Copy, Debug, Default)]
17pub(crate) struct Usage {
18 pub(crate) consumed: u64,
19 pub(crate) reserved: u64,
20}
21
22#[derive(Debug, Default)]
23pub(crate) struct NodeState {
24 pub(crate) usage: BTreeMap<Resource, Usage>,
25}
26
27#[derive(Debug)]
28pub(crate) struct Node {
29 pub(crate) id: BudgetId,
30 pub(crate) name: Option<Arc<str>>,
31 pub(crate) parent: Option<Arc<Node>>,
32 pub(crate) limits: BTreeMap<Resource, u64>,
33 pub(crate) state: Mutex<NodeState>,
34 pub(crate) deadline: Option<Instant>,
35 #[cfg(feature = "tokio")]
36 pub(crate) cancellation: CancellationToken,
37}
38
39#[derive(Clone, Debug)]
41pub struct Budget {
42 pub(crate) node: Arc<Node>,
43}
44
45#[derive(Debug)]
47pub struct BudgetBuilder {
48 parent: Option<Budget>,
49 name: Option<Arc<str>>,
50 limits: BTreeMap<Resource, u64>,
51 duplicate_limit: Option<Resource>,
52 deadline: Option<DeadlineRequest>,
53 duplicate_deadline: bool,
54}
55
56#[derive(Clone, Copy, Debug)]
57enum DeadlineRequest {
58 At(Instant),
59 After(Duration),
60}
61
62impl Budget {
63 #[must_use]
65 pub fn builder() -> BudgetBuilder {
66 BudgetBuilder::new(None)
67 }
68
69 #[must_use]
71 pub fn child(&self) -> BudgetBuilder {
72 BudgetBuilder::new(Some(self.clone()))
73 }
74
75 #[must_use]
77 pub fn id(&self) -> BudgetId {
78 self.node.id
79 }
80
81 pub fn consume(&self, resource: &Resource, amount: u64) -> Result<(), BudgetError> {
88 if amount == 0 {
89 return Ok(());
90 }
91 self.ensure_active()?;
92 let requests = BTreeMap::from([(resource.clone(), amount)]);
93 self.apply(&requests, AccountingKind::Consumed)?;
94 self.trace_event("consume", resource, amount);
95 Ok(())
96 }
97
98 pub fn reserve(&self, resource: &Resource, amount: u64) -> Result<Reservation, BudgetError> {
105 if amount != 0 {
106 self.ensure_active()?;
107 let requests = BTreeMap::from([(resource.clone(), amount)]);
108 self.apply(&requests, AccountingKind::Reserved)?;
109 self.trace_event("reserve", resource, amount);
110 }
111 Ok(Reservation::new(self.clone(), resource.clone(), amount))
112 }
113
114 pub fn reserve_many<'a>(
125 &self,
126 resources: impl IntoIterator<Item = (&'a Resource, u64)>,
127 ) -> Result<ReservationSet, BudgetError> {
128 let mut requests = BTreeMap::<Resource, u64>::new();
129 let mut overflows = BTreeSet::new();
130 for (resource, amount) in resources {
131 if amount == 0 {
132 continue;
133 }
134 let current = requests.entry(resource.clone()).or_default();
135 if let Some(sum) = current.checked_add(amount) {
136 *current = sum;
137 } else {
138 overflows.insert(resource.clone());
139 }
140 }
141 if let Some(resource) = overflows.into_iter().next() {
142 return Err(BudgetError::Overflow { resource });
143 }
144 if !requests.is_empty() {
145 self.ensure_active()?;
146 self.apply(&requests, AccountingKind::Reserved)?;
147 #[cfg(feature = "tracing")]
148 tracing::event!(
149 tracing::Level::TRACE,
150 budget.id = self.id().get(),
151 budget.resources = requests.len(),
152 "budget.reserve_many"
153 );
154 }
155 Ok(ReservationSet::new(self.clone(), requests))
156 }
157
158 #[must_use]
163 pub fn remaining(&self, resource: &Resource) -> Remaining {
164 let lineage = self.lineage();
165 let guards = lock_lineage(&lineage);
166 effective_remaining(&lineage, &guards, resource)
167 }
168
169 #[must_use]
171 pub fn snapshot(&self) -> BudgetSnapshot {
172 let lineage = self.lineage();
173 let guards = lock_lineage(&lineage);
174 let leaf_index = lineage.len() - 1;
175 let leaf = &lineage[leaf_index];
176 let leaf_state = &guards[leaf_index];
177
178 let mut resources = BTreeSet::new();
179 for node in &lineage {
180 resources.extend(node.limits.keys().cloned());
181 }
182 resources.extend(leaf_state.usage.keys().cloned());
183
184 let resources = resources
185 .into_iter()
186 .map(|resource| {
187 let usage = leaf_state.usage.get(&resource).copied().unwrap_or_default();
188 ResourceSnapshot {
189 local_limit: leaf.limits.get(&resource).copied(),
190 effective_remaining: effective_remaining(&lineage, &guards, &resource),
191 resource,
192 consumed: usage.consumed,
193 reserved: usage.reserved,
194 }
195 })
196 .collect();
197
198 BudgetSnapshot {
199 id: leaf.id,
200 name: leaf.name.clone(),
201 resources,
202 deadline_remaining: leaf
203 .deadline
204 .map(|deadline| deadline.saturating_duration_since(Instant::now())),
205 #[cfg(feature = "tokio")]
206 cancelled: leaf.cancellation.is_cancelled(),
207 }
208 }
209
210 pub(crate) fn release_reserved(&self, resources: &BTreeMap<Resource, u64>) {
211 if resources.is_empty() {
212 return;
213 }
214 let lineage = self.lineage();
215 let mut guards = lock_lineage(&lineage);
216 for guard in &mut guards {
217 for (resource, amount) in resources {
218 if let Some(usage) = guard.usage.get_mut(resource) {
219 usage.reserved = usage.reserved.saturating_sub(*amount);
220 }
221 }
222 }
223 }
224
225 pub(crate) fn commit_reserved(
226 &self,
227 reserved: &BTreeMap<Resource, u64>,
228 actual: &BTreeMap<Resource, u64>,
229 ) {
230 if reserved.is_empty() {
231 return;
232 }
233 let lineage = self.lineage();
234 let mut guards = lock_lineage(&lineage);
235 for guard in &mut guards {
236 for (resource, reserved_amount) in reserved {
237 if let Some(usage) = guard.usage.get_mut(resource) {
238 let actual_amount = actual.get(resource).copied().unwrap_or(0);
239 usage.reserved = usage.reserved.saturating_sub(*reserved_amount);
240 usage.consumed += actual_amount.min(*reserved_amount);
241 }
242 }
243 }
244 }
245
246 fn apply(
247 &self,
248 requests: &BTreeMap<Resource, u64>,
249 kind: AccountingKind,
250 ) -> Result<(), BudgetError> {
251 let lineage = self.lineage();
252 let mut guards = lock_lineage(&lineage);
253
254 for (index, node) in lineage.iter().enumerate() {
255 for (resource, amount) in requests {
256 let usage = guards[index]
257 .usage
258 .get(resource)
259 .copied()
260 .unwrap_or_default();
261 let next = usage
262 .consumed
263 .checked_add(usage.reserved)
264 .and_then(|used| used.checked_add(*amount))
265 .ok_or_else(|| BudgetError::Overflow {
266 resource: resource.clone(),
267 })?;
268 if let Some(limit) = node.limits.get(resource) {
269 if next > *limit {
270 return Err(BudgetError::Exhausted {
271 resource: resource.clone(),
272 requested: *amount,
273 remaining: limit - usage.consumed - usage.reserved,
274 scope: node.id,
275 scope_name: node.name.clone(),
276 });
277 }
278 }
279 }
280 }
281
282 for guard in &mut guards {
283 for (resource, amount) in requests {
284 let usage = guard.usage.entry(resource.clone()).or_default();
285 match kind {
286 AccountingKind::Consumed => usage.consumed += amount,
287 AccountingKind::Reserved => usage.reserved += amount,
288 }
289 }
290 }
291 Ok(())
292 }
293
294 pub(crate) fn ensure_active(&self) -> Result<(), BudgetError> {
295 #[cfg(feature = "tokio")]
296 if self.node.cancellation.is_cancelled() {
297 return Err(BudgetError::Cancelled);
298 }
299 if self
300 .node
301 .deadline
302 .is_some_and(|deadline| Instant::now() >= deadline)
303 {
304 return Err(BudgetError::DeadlineExceeded);
305 }
306 Ok(())
307 }
308
309 pub(crate) fn lineage(&self) -> Vec<Arc<Node>> {
310 let mut lineage = Vec::new();
311 let mut current = Some(self.node.clone());
312 while let Some(node) = current.take() {
313 current.clone_from(&node.parent);
314 lineage.push(node);
315 }
316 lineage.reverse();
317 lineage
318 }
319
320 pub(crate) fn trace_event(&self, operation: &'static str, resource: &Resource, amount: u64) {
321 #[cfg(feature = "tracing")]
322 tracing::event!(
323 tracing::Level::TRACE,
324 budget.id = self.id().get(),
325 budget.operation = operation,
326 budget.resource = resource.as_str(),
327 budget.amount = amount,
328 "budget accounting operation"
329 );
330 #[cfg(not(feature = "tracing"))]
331 let _ = (self, operation, resource, amount);
332 }
333}
334
335impl BudgetBuilder {
336 fn new(parent: Option<Budget>) -> Self {
337 Self {
338 parent,
339 name: None,
340 limits: BTreeMap::new(),
341 duplicate_limit: None,
342 deadline: None,
343 duplicate_deadline: false,
344 }
345 }
346
347 #[must_use]
349 pub fn name(mut self, name: impl Into<Arc<str>>) -> Self {
350 self.name = Some(name.into());
351 self
352 }
353
354 #[must_use]
358 pub fn limit(mut self, resource: Resource, amount: u64) -> Self {
359 if self.limits.insert(resource.clone(), amount).is_some() && self.duplicate_limit.is_none()
360 {
361 self.duplicate_limit = Some(resource);
362 }
363 self
364 }
365
366 #[must_use]
368 pub fn deadline_at(mut self, deadline: Instant) -> Self {
369 self.set_deadline(DeadlineRequest::At(deadline));
370 self
371 }
372
373 #[must_use]
375 pub fn deadline_after(mut self, duration: Duration) -> Self {
376 self.set_deadline(DeadlineRequest::After(duration));
377 self
378 }
379
380 pub fn build(self) -> Result<Budget, BudgetBuildError> {
387 if let Some(resource) = self.duplicate_limit {
388 return Err(BudgetBuildError::DuplicateLimit { resource });
389 }
390 if self.duplicate_deadline {
391 return Err(BudgetBuildError::DuplicateDeadline);
392 }
393
394 let requested_deadline = match self.deadline {
395 None => None,
396 Some(DeadlineRequest::At(deadline)) => Some(deadline),
397 Some(DeadlineRequest::After(duration)) => Some(
398 Instant::now()
399 .checked_add(duration)
400 .ok_or(BudgetBuildError::DeadlineOverflow)?,
401 ),
402 };
403 let inherited_deadline = self.parent.as_ref().and_then(|parent| parent.node.deadline);
404 let deadline = match (inherited_deadline, requested_deadline) {
405 (Some(parent), Some(child)) => Some(parent.min(child)),
406 (Some(parent), None) => Some(parent),
407 (None, child) => child,
408 };
409
410 let id = NEXT_BUDGET_ID
411 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
412 .map(BudgetId)
413 .map_err(|_| BudgetBuildError::BudgetIdExhausted)?;
414
415 #[cfg(feature = "tokio")]
416 let cancellation = self
417 .parent
418 .as_ref()
419 .map_or_else(CancellationToken::new, |parent| {
420 parent.node.cancellation.child_token()
421 });
422
423 Ok(Budget {
424 node: Arc::new(Node {
425 id,
426 name: self.name,
427 parent: self.parent.map(|parent| parent.node),
428 limits: self.limits,
429 state: Mutex::new(NodeState::default()),
430 deadline,
431 #[cfg(feature = "tokio")]
432 cancellation,
433 }),
434 })
435 }
436
437 fn set_deadline(&mut self, deadline: DeadlineRequest) {
438 if self.deadline.replace(deadline).is_some() {
439 self.duplicate_deadline = true;
440 }
441 }
442}
443
444#[derive(Clone, Copy)]
445enum AccountingKind {
446 Consumed,
447 Reserved,
448}
449
450fn lock_lineage(lineage: &[Arc<Node>]) -> Vec<MutexGuard<'_, NodeState>> {
451 lineage
452 .iter()
453 .map(|node| {
454 node.state
455 .lock()
456 .unwrap_or_else(std::sync::PoisonError::into_inner)
457 })
458 .collect()
459}
460
461fn effective_remaining(
462 lineage: &[Arc<Node>],
463 guards: &[MutexGuard<'_, NodeState>],
464 resource: &Resource,
465) -> Remaining {
466 let mut effective = None;
467 for (node, state) in lineage.iter().zip(guards) {
468 if let Some(limit) = node.limits.get(resource) {
469 let usage = state.usage.get(resource).copied().unwrap_or_default();
470 let remaining = limit - usage.consumed - usage.reserved;
471 effective = Some(effective.map_or(remaining, |current: u64| current.min(remaining)));
472 }
473 }
474 effective.map_or(Remaining::Unlimited, Remaining::Limited)
475}