Skip to main content

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}