rocketmq_runtime/resource_budget/dynamic.rs
1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Retirement for dynamic budget keys.
16//!
17//! A dynamic key is a budget child whose name comes from runtime data, such as
18//! a cluster ordering key. `ResourceBudget::child` creates the node but nothing
19//! owns its lifetime afterwards, so the key becomes unreachable while permits
20//! acquired from it may still be live, and a later child with the same name
21//! silently creates a second generation beside the first.
22//!
23//! [`DynamicBudgetKey`] closes both gaps. The registry keeps the name reserved
24//! until retirement completes, so a retired generation cannot be reused while
25//! its work is still running, and retirement waits for the real reservations on
26//! the key's budget instead of assuming that a closed handle means the work
27//! stopped. Every release path in the budget tree already notifies the tree's
28//! capacity notification, so the wait needs no extra primitive.
29
30use std::collections::HashMap;
31use std::sync::atomic::AtomicU64;
32use std::sync::atomic::Ordering;
33use std::sync::Arc;
34use std::sync::Mutex;
35
36use tokio::time::Instant;
37
38use super::budget::BudgetRejection;
39use super::budget::ResourceBudget;
40use super::budget::ResourcePermit;
41use super::limit::BudgetClass;
42use crate::RuntimeContractViolation;
43
44/// The default bound on live dynamic keys per budget tree.
45///
46/// It matches the metadata target registry default so that a process has one
47/// recognizable order of magnitude for its bounded registrations.
48pub const DEFAULT_MAX_DYNAMIC_BUDGET_KEYS: usize = 4_096;
49
50/// Why a dynamic key could not be registered.
51#[derive(Debug)]
52pub enum DynamicKeyRegistrationFailure {
53 /// The parent generation has closed admission.
54 Closed,
55 /// The name or limit violated the budget contract.
56 Invalid(RuntimeContractViolation),
57 /// The name is already held by a live or retiring key.
58 ///
59 /// Reusing it would let the next generation of work run beside the
60 /// generation it replaced, so retirement has to complete first.
61 NameInUse {
62 /// The generation that currently holds the name.
63 generation: u64,
64 },
65 /// The tree already holds the maximum number of dynamic keys.
66 CapacityExhausted {
67 /// The configured bound.
68 max_entries: usize,
69 },
70}
71
72/// Why a dynamic key refused an admission.
73#[derive(Debug)]
74pub enum DynamicKeyAdmissionRejection {
75 /// Admission is closed because the key was retired.
76 Closed,
77 /// The key's budget rejected the admission.
78 Budget(BudgetRejection),
79}
80
81/// The outcome of retiring a dynamic key.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct DynamicKeyRetirement {
84 /// Whether the registration was released and the name is free again.
85 ///
86 /// A released key has no outstanding work. When the deadline elapses
87 /// first, the registration stays held so the name cannot be reused while
88 /// reservations from this generation are still live.
89 pub released: bool,
90 /// Reservations still held on the key's budget when the caller stopped
91 /// waiting.
92 pub outstanding_reservations: usize,
93 /// Bytes still reserved on the key's budget when the caller stopped
94 /// waiting.
95 pub outstanding_bytes: usize,
96}
97
98/// One registered dynamic key under a budget tree.
99///
100/// Cloning shares the same registration and generation. Every clone observes
101/// the same closed state, and only the last generation that holds a name can
102/// release it.
103#[derive(Clone)]
104pub struct DynamicBudgetKey {
105 inner: Arc<DynamicKeyInner>,
106}
107
108struct DynamicKeyInner {
109 registry: DynamicKeyRegistry,
110 budget: ResourceBudget,
111 name: Arc<str>,
112 generation: u64,
113}
114
115impl std::fmt::Debug for DynamicBudgetKey {
116 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 formatter
118 .debug_struct("DynamicBudgetKey")
119 .field("name", &self.inner.name)
120 .field("generation", &self.inner.generation)
121 .field("closed", &self.is_closed())
122 .finish_non_exhaustive()
123 }
124}
125
126impl DynamicBudgetKey {
127 /// Returns the name this key holds.
128 #[must_use]
129 pub fn name(&self) -> &str {
130 &self.inner.name
131 }
132
133 /// Returns the unique generation of this registration.
134 #[must_use]
135 pub fn generation(&self) -> u64 {
136 self.inner.generation
137 }
138
139 /// Returns the budget this key owns.
140 ///
141 /// The key is the admission boundary for a dynamic key: work that has to be
142 /// waited for by [`Self::retire_until`] must be admitted through
143 /// [`Self::try_acquire`] or against this budget, because retirement waits
144 /// for the reservations on this node.
145 #[must_use]
146 pub fn budget(&self) -> ResourceBudget {
147 self.inner.budget.clone()
148 }
149
150 /// Returns whether admission is closed for this key.
151 #[must_use]
152 pub fn is_closed(&self) -> bool {
153 self.inner.budget.is_closed()
154 }
155
156 /// Closes admission for this key without waiting for its work.
157 pub fn close(&self) {
158 self.inner.budget.close_dynamic();
159 }
160
161 /// Acquires capacity from this key.
162 ///
163 /// # Errors
164 ///
165 /// Returns [`DynamicKeyAdmissionRejection::Closed`] once the key is
166 /// retiring, or the budget rejection when the key's budget is full.
167 pub fn try_acquire(
168 &self,
169 bytes: usize,
170 class: BudgetClass,
171 ) -> Result<ResourcePermit, DynamicKeyAdmissionRejection> {
172 self.inner.budget.try_acquire(bytes, class).map_err(|rejection| {
173 if rejection.is_closed() {
174 DynamicKeyAdmissionRejection::Closed
175 } else {
176 DynamicKeyAdmissionRejection::Budget(rejection)
177 }
178 })
179 }
180
181 /// Closes admission and waits for the reservations on this key to drain.
182 ///
183 /// Retirement completes only when the node holds no reservations, which is
184 /// what makes it safe for the caller to stop accounting for this key. When
185 /// `deadline` elapses first, the registration stays held and the returned
186 /// [`DynamicKeyRetirement`] reports the real remaining work.
187 pub async fn retire_until(&self, deadline: Instant) -> DynamicKeyRetirement {
188 self.close();
189 loop {
190 // The registration is released on this branch only, so a name can
191 // never be freed while reservations from this generation are live.
192 let snapshot = self.inner.budget.snapshot();
193 if snapshot.current_count == 0 {
194 let released = self.inner.registry.release(&self.inner.name, self.inner.generation);
195 return DynamicKeyRetirement {
196 released,
197 outstanding_reservations: 0,
198 outstanding_bytes: 0,
199 };
200 }
201 if Instant::now() >= deadline {
202 return DynamicKeyRetirement {
203 released: false,
204 outstanding_reservations: snapshot.current_count,
205 outstanding_bytes: snapshot.current_bytes,
206 };
207 }
208
209 let released = self.inner.budget.capacity_notify().notified();
210 tokio::pin!(released);
211 released.as_mut().enable();
212 // Re-check after registering the waiter, so a release that happened
213 // in between is not lost.
214 let snapshot = self.inner.budget.snapshot();
215 if snapshot.current_count == 0 {
216 continue;
217 }
218
219 let expired = tokio::time::sleep_until(deadline);
220 tokio::pin!(expired);
221 tokio::select! {
222 biased;
223 () = &mut expired => {}
224 () = &mut released => {}
225 }
226 }
227 }
228}
229
230/// The bounded set of dynamic key names held by one budget tree.
231#[derive(Clone)]
232pub(super) struct DynamicKeyRegistry {
233 inner: Arc<DynamicKeyRegistryInner>,
234}
235
236struct DynamicKeyRegistryInner {
237 entries: Mutex<HashMap<Arc<str>, u64>>,
238 next_generation: AtomicU64,
239 max_entries: usize,
240}
241
242impl std::fmt::Debug for DynamicKeyRegistry {
243 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 let entries = self
245 .inner
246 .entries
247 .lock()
248 .unwrap_or_else(std::sync::PoisonError::into_inner)
249 .len();
250 formatter
251 .debug_struct("DynamicKeyRegistry")
252 .field("entries", &entries)
253 .field("max_entries", &self.inner.max_entries)
254 .finish()
255 }
256}
257
258impl DynamicKeyRegistry {
259 pub(super) fn new() -> Self {
260 Self::with_max_entries(DEFAULT_MAX_DYNAMIC_BUDGET_KEYS)
261 }
262
263 pub(super) fn with_max_entries(max_entries: usize) -> Self {
264 Self {
265 inner: Arc::new(DynamicKeyRegistryInner {
266 entries: Mutex::new(HashMap::new()),
267 next_generation: AtomicU64::new(1),
268 max_entries,
269 }),
270 }
271 }
272
273 pub(super) fn register(
274 &self,
275 budget: ResourceBudget,
276 name: Arc<str>,
277 ) -> Result<DynamicBudgetKey, DynamicKeyRegistrationFailure> {
278 let mut entries = self
279 .inner
280 .entries
281 .lock()
282 .unwrap_or_else(std::sync::PoisonError::into_inner);
283 if let Some(generation) = entries.get(&name) {
284 return Err(DynamicKeyRegistrationFailure::NameInUse {
285 generation: *generation,
286 });
287 }
288 if entries.len() >= self.inner.max_entries {
289 return Err(DynamicKeyRegistrationFailure::CapacityExhausted {
290 max_entries: self.inner.max_entries,
291 });
292 }
293 let generation = self.inner.next_generation.fetch_add(1, Ordering::Relaxed);
294 entries.insert(Arc::clone(&name), generation);
295 drop(entries);
296
297 Ok(DynamicBudgetKey {
298 inner: Arc::new(DynamicKeyInner {
299 registry: self.clone(),
300 budget,
301 name,
302 generation,
303 }),
304 })
305 }
306
307 fn release(&self, name: &Arc<str>, generation: u64) -> bool {
308 let mut entries = self
309 .inner
310 .entries
311 .lock()
312 .unwrap_or_else(std::sync::PoisonError::into_inner);
313 // A stale handle from a previous generation must not release the name
314 // that a newer registration now owns.
315 if entries.get(name) != Some(&generation) {
316 return false;
317 }
318 entries.remove(name);
319 true
320 }
321}