qubit_budget/resource/budget/resource_pool.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Defines finite releasable resource pools.
9
10use crate::resource::InsufficientBudgetError;
11use crate::resource::ResourceLimit;
12use crate::resource::ResourceQuantity;
13use crate::resource::ResourceReleaseError;
14
15/// A finite, non-synchronizing pool of releasable resource capacity.
16///
17/// Acquisition subtracts from `available`; release adds only after checking
18/// the amount is no greater than `in_use`. The object has no lifecycle state,
19/// waiting, fairness, permits or cancellation. An unconfigured dimension is
20/// represented by `Option<ResourcePool<R>> = None`.
21///
22/// # Type Parameters
23///
24/// * `R` - Caller-defined resource value retained for diagnostics.
25/// * `Q` - Exact unsigned quantity used for the capacity and accounting.
26///
27/// # Examples
28///
29/// ```
30/// use qubit_budget::ResourcePool;
31///
32/// let mut pool = ResourcePool::new("workers", 2_u64);
33/// pool.try_acquire(1).expect("one worker should fit");
34/// assert_eq!(pool.in_use(), 1);
35/// pool.release(1).expect("the worker is returned");
36/// assert_eq!(pool.available(), 2);
37/// ```
38#[derive(Debug, PartialEq, Eq)]
39pub struct ResourcePool<R, Q = u64>
40where
41 Q: ResourceQuantity,
42{
43 /// Finite total capacity of the pool.
44 limit: ResourceLimit<R, Q>,
45
46 /// Capacity that is currently available for acquisition.
47 available: Q,
48}
49
50impl<R, Q> ResourcePool<R, Q>
51where
52 Q: ResourceQuantity,
53{
54 /// Creates an entirely available finite pool.
55 ///
56 /// # Parameters
57 ///
58 /// * `resource` - Domain resource value retained in errors.
59 /// * `limit` - Finite pool capacity.
60 ///
61 /// # Returns
62 ///
63 /// A pool with `available == limit`.
64 ///
65 /// # Examples
66 ///
67 /// ```
68 /// use qubit_budget::ResourcePool;
69 ///
70 /// let mut pool = ResourcePool::new("readers", 2_u64);
71 /// pool.try_acquire(1).expect("one reader should fit");
72 /// pool.release(1).expect("the reader is returned explicitly");
73 /// assert_eq!(pool.in_use(), 0);
74 /// ```
75 #[inline]
76 #[must_use]
77 pub const fn new(resource: R, limit: Q) -> Self {
78 Self {
79 limit: ResourceLimit::new(resource, limit),
80 available: limit,
81 }
82 }
83
84 /// Creates an entirely available pool from an immutable resource limit.
85 ///
86 /// # Parameters
87 ///
88 /// * `limit` - Resource identity and finite capacity for this pool.
89 ///
90 /// # Returns
91 ///
92 /// A pool whose available capacity equals the limit maximum.
93 #[inline]
94 #[must_use]
95 pub fn from_limit(limit: ResourceLimit<R, Q>) -> Self {
96 let available = limit.maximum();
97 Self { limit, available }
98 }
99
100 /// Acquires capacity when enough units are available.
101 ///
102 /// # Parameters
103 ///
104 /// * `amount` - Quantity to acquire.
105 ///
106 /// # Returns
107 ///
108 /// `Ok(())` after subtracting the amount, or
109 /// [`InsufficientBudgetError`] with no state change when it does
110 /// not fit.
111 ///
112 /// # Errors
113 ///
114 /// Returns [`InsufficientBudgetError`] when `amount` exceeds current
115 /// availability. The pool remains unchanged in that case.
116 pub fn try_acquire(&mut self, amount: Q) -> Result<(), InsufficientBudgetError<R, Q>>
117 where
118 R: Clone,
119 {
120 if amount > self.available {
121 return Err(InsufficientBudgetError {
122 resource: self.limit.resource().clone(),
123 limit: self.limit.maximum(),
124 remaining: self.available,
125 requested: amount,
126 });
127 }
128 self.available = self.available - amount;
129 Ok(())
130 }
131
132 /// Releases previously acquired capacity.
133 ///
134 /// # Parameters
135 ///
136 /// * `amount` - Quantity to return to the pool.
137 ///
138 /// # Returns
139 ///
140 /// `Ok(())` after increasing availability, or
141 /// [`ResourceReleaseError`] with no state change when the
142 /// amount exceeds current occupancy.
143 ///
144 /// # Errors
145 ///
146 /// Returns [`ResourceReleaseError`] when `amount` exceeds
147 /// current occupancy. The pool remains unchanged in that case.
148 pub fn release(&mut self, amount: Q) -> Result<(), ResourceReleaseError<R, Q>>
149 where
150 R: Clone,
151 {
152 let in_use = self.in_use();
153 if amount > in_use {
154 return Err(ResourceReleaseError::InvalidRelease {
155 resource: self.limit.resource().clone(),
156 limit: self.limit.maximum(),
157 in_use,
158 requested: amount,
159 });
160 }
161 self.available = self.available + amount;
162 Ok(())
163 }
164
165 /// Returns the associated resource.
166 ///
167 /// # Returns
168 ///
169 /// Returns the associated resource.
170 #[must_use]
171 #[inline(always)]
172 pub const fn resource(&self) -> &R {
173 self.limit.resource()
174 }
175
176 /// Returns the immutable resource limit that configures this pool.
177 ///
178 /// # Returns
179 ///
180 /// Returns the immutable resource limit that configures this pool.
181 #[must_use]
182 #[inline(always)]
183 pub const fn resource_limit(&self) -> &ResourceLimit<R, Q> {
184 &self.limit
185 }
186
187 /// Returns the total finite capacity.
188 ///
189 /// # Returns
190 ///
191 /// Returns the total finite capacity.
192 #[must_use]
193 #[inline(always)]
194 pub const fn capacity(&self) -> Q {
195 self.limit.maximum()
196 }
197
198 /// Returns currently available capacity.
199 ///
200 /// # Returns
201 ///
202 /// Returns currently available capacity.
203 #[must_use]
204 #[inline(always)]
205 pub const fn available(&self) -> Q {
206 self.available
207 }
208
209 /// Returns currently acquired capacity.
210 ///
211 /// # Returns
212 ///
213 /// Returns currently acquired capacity.
214 #[inline(always)]
215 #[must_use]
216 pub fn in_use(&self) -> Q {
217 self.limit.maximum() - self.available
218 }
219}