saluki_core/accounting/
registry.rs

1use std::marker::PhantomData;
2use std::{
3    collections::HashMap,
4    sync::{Arc, Mutex, MutexGuard},
5};
6
7use saluki_common::resource_tracking::{ResourceGroupRegistry, ResourceGroupToken};
8
9use super::{
10    api::ResourceAPIHandler, BoundsVerifier, ComponentBounds, MemoryBounds, MemoryGrant, UsageExpr, VerifiedBounds,
11    VerifierError,
12};
13use crate::support::SubsystemIdentifier;
14
15pub(crate) struct ComponentMetadata {
16    full_name: Option<String>,
17    bounds: ComponentBounds,
18    token: Option<ResourceGroupToken>,
19    subcomponents: HashMap<String, Arc<Mutex<ComponentMetadata>>>,
20}
21
22impl ComponentMetadata {
23    fn from_full_name(full_name: Option<String>) -> Self {
24        Self {
25            full_name,
26            bounds: ComponentBounds::default(),
27            token: None,
28            subcomponents: HashMap::new(),
29        }
30    }
31
32    pub fn get_or_create<S>(&mut self, name: S) -> Arc<Mutex<Self>>
33    where
34        S: AsRef<str>,
35    {
36        // Split the name into the current level name and the remaining name.
37        //
38        // This lets us handle names which refer to a target nested component instead of having to chain a ton of calls
39        // together.
40        let name = name.as_ref();
41        let (current_level_name, remaining_name) = match name.split_once('.') {
42            Some((current_level_name, remaining_name)) => (current_level_name, Some(remaining_name)),
43            None => (name, None),
44        };
45
46        // Now we need to see if we have an existing component here or if we need to create a new one.
47        match self.subcomponents.get(current_level_name) {
48            Some(existing) => match remaining_name {
49                Some(remaining_name) => {
50                    // We found an intermediate subcomponent, so keep recursing.
51                    existing.lock().unwrap().get_or_create(remaining_name)
52                }
53                None => {
54                    // We've found the leaf subcomponent.
55                    Arc::clone(existing)
56                }
57            },
58            None => {
59                // We couldn't find the component at this level, so we need to create it.
60                //
61                // We do all of our name calculation and so on, but we also leave the token empty for now. We do this to
62                // avoid registering intermediate components that aren't actually used by the code, but are simply a
63                // consequence of wanting to having a nicely nested structure.
64                //
65                // We'll register a token for the component the first time it's requested.
66                let full_name = match self.full_name.as_ref() {
67                    Some(parent_full_name) => format!("{}.{}", parent_full_name, current_level_name),
68                    None => current_level_name.to_string(),
69                };
70
71                let inner = self
72                    .subcomponents
73                    .entry(current_level_name.to_string())
74                    .or_insert_with(|| Arc::new(Mutex::new(Self::from_full_name(Some(full_name)))));
75
76                // If we still need to recurse further, do so here.. otherwise, return the subcomponent we just created
77                // as-is.
78                match remaining_name {
79                    Some(remaining_name) => inner.lock().unwrap().get_or_create(remaining_name),
80                    None => Arc::clone(inner),
81                }
82            }
83        }
84    }
85
86    fn token(&mut self) -> ResourceGroupToken {
87        match self.token {
88            Some(token) => token,
89            None => match self.full_name.as_deref() {
90                Some(full_name) => {
91                    let allocator_component_registry = ResourceGroupRegistry::global();
92                    let token = allocator_component_registry.register_resource_group(full_name);
93                    self.token = Some(token);
94
95                    token
96                }
97                None => ResourceGroupToken::root(),
98            },
99        }
100    }
101
102    fn reset(&mut self) {
103        self.bounds = ComponentBounds::default();
104        self.subcomponents.clear();
105    }
106
107    pub fn self_bounds(&self) -> &ComponentBounds {
108        &self.bounds
109    }
110
111    pub fn as_bounds(&self) -> ComponentBounds {
112        let mut bounds = ComponentBounds::default();
113        bounds.self_firm_limit_bytes = self.bounds.self_firm_limit_bytes.clone();
114        bounds.self_minimum_required_bytes = self.bounds.self_minimum_required_bytes.clone();
115
116        for (name, subcomponent) in self.subcomponents.iter() {
117            let subcomponent = subcomponent.lock().unwrap();
118            let subcomponent_bounds = subcomponent.as_bounds();
119            bounds.subcomponents.insert(name.clone(), subcomponent_bounds);
120        }
121
122        bounds
123    }
124}
125
126/// A registry for components for tracking memory bounds and runtime memory usage.
127///
128/// This registry provides a unified interface for declaring the memory bounds of a _component_, as well as registering
129/// that component for runtime memory usage tracking when using the tracking allocator implementation in `saluki-common`.
130///
131/// ## Components
132///
133/// **Components** are any logical grouping of memory usage within a program, and they can be arbitrarily nested.
134///
135/// For example, a data plane will generally have a topology that defines the components used to accept, process, and
136/// forward data. The topology itself could be considered a component, and each individual source, transform, and
137/// destination within it could be subcomponents of the topology.
138///
139/// Components are generally meant to be tied to something that has its own memory bounds and is somewhat standalone,
140/// but this isn't an absolute requirement and components can be nested more granularly for organizational/aesthetic
141/// purposes. Again, for example, one might opt to create a component in their topology for each component type --
142/// sources, transforms, and destinations -- and then add the actual instances of those components as subcomponents to
143/// each grouping, leading to a nested structure such as `topology/sources/source1`, `topology/transforms/transform1`,
144/// and so on.
145///
146/// ## Bounds
147///
148/// Every component is able to define memory bounds for itself and its subcomponents. A builder-style API is exposed to
149/// allow for ergonomically defining these bounds -- both minimum and firm -- for components, as well as extending the
150/// nestable aspect of the registry itself to the bounds builder, allowing for flexibility in where components are
151/// defined from and how they're nested.
152///
153/// ## Allocation tracking
154///
155/// Every component is also able to be registered with its own resource group when using the tracking allocator
156/// implementation. This is done on demand when the component's token is requested, which avoids polluting the tracking
157/// allocator with components that are never actually used, such as those used for organizational/aesthetic purposes.
158#[derive(Clone)]
159pub struct ComponentRegistry {
160    root: Arc<Mutex<ComponentMetadata>>,
161}
162
163impl ComponentRegistry {
164    /// Creates a handle to this registry.
165    ///
166    /// The handle provides read-only access to root-level operations like creating API handlers and verifying bounds. It
167    /// can be freely cloned and shared.
168    pub fn root(&self) -> ComponentRegistryHandle {
169        ComponentRegistryHandle {
170            inner: Arc::clone(&self.root),
171        }
172    }
173
174    /// Gets a bounds builder for the component identified by `id`, creating the component -- and any intermediate
175    /// components -- if it doesn't already exist.
176    ///
177    /// `id` is a fully qualified, absolute identifier: the component is addressed directly from the root, so callers
178    /// never need to track how deeply nested a registry is.
179    pub fn bounds_builder(&self, id: &SubsystemIdentifier) -> MemoryBoundsBuilder<'_> {
180        let node = self.root.lock().unwrap().get_or_create(id.to_string());
181        MemoryBoundsBuilder { node, _lt: PhantomData }
182    }
183
184    /// Gets the resource group tracking token for the component identified by `id`, creating the component -- and any
185    /// intermediate components -- if it doesn't already exist.
186    ///
187    /// The component's resource group is registered (using its full name) the first time its token is requested. `id`
188    /// is a fully qualified, absolute identifier, addressed directly from the root.
189    pub fn get_resource_group_token(&self, id: &SubsystemIdentifier) -> ResourceGroupToken {
190        let node = self.root.lock().unwrap().get_or_create(id.to_string());
191        let mut node = node.lock().unwrap();
192        node.token()
193    }
194
195    /// Gets the memory bounds for all components in the registry.
196    pub fn as_bounds(&self) -> ComponentBounds {
197        self.root.lock().unwrap().as_bounds()
198    }
199}
200
201/// A cloneable, read-only handle to a component registry.
202///
203/// This handle provides access to read-only operations such as creating an API handler or verifying bounds. Unlike
204/// [`ComponentRegistry`], it can be freely cloned and shared across ownership boundaries.
205///
206/// Obtained via [`ComponentRegistry::root`].
207#[derive(Clone)]
208pub struct ComponentRegistryHandle {
209    inner: Arc<Mutex<ComponentMetadata>>,
210}
211
212impl ComponentRegistryHandle {
213    /// Validates that all components are able to respect the calculated effective limit.
214    ///
215    /// If validation succeeds, `VerifiedBounds` is returned, which provides information about the effective limit that
216    /// can be used for allocating memory.
217    ///
218    /// ## Errors
219    ///
220    /// A number of invalid conditions are checked and will cause an error to be returned:
221    ///
222    /// - when a component has invalid bounds (for example, minimum required bytes higher than firm limit)
223    /// - when the combined total of the firm limit for all components exceeds the effective limit
224    pub fn verify_bounds(&self, initial_grant: MemoryGrant) -> Result<VerifiedBounds, VerifierError> {
225        let bounds = self.inner.lock().unwrap().as_bounds();
226        BoundsVerifier::new(initial_grant, bounds).verify()
227    }
228
229    /// Gets an API handler for reporting the memory bounds and resource usage of all component in the registry.
230    ///
231    /// This handler exposes routes for querying the memory bounds and usage of all registered components. See
232    /// [`ResourceAPIHandler`] for more information about routes and responses.
233    pub fn api_handler(&self) -> ResourceAPIHandler {
234        ResourceAPIHandler::from_state(Arc::clone(&self.inner))
235    }
236
237    /// Returns a JSON snapshot of the current memory bounds and live usage for all registered components.
238    ///
239    /// Each component appears as a key in the returned JSON object with `minimum_required_bytes`,
240    /// `firm_limit_bytes`, and `actual_live_bytes` fields. This produces the same data as the `/memory/status`
241    /// HTTP endpoint but is collected directly from the in-process registry for use outside of the HTTP handler
242    /// path (for example, when building a diagnostic artifact).
243    pub fn memory_snapshot_json(&self) -> String {
244        use std::collections::BTreeMap;
245
246        use saluki_common::resource_tracking::{ResourceGroupRegistry, ResourceStatsSnapshot};
247
248        #[derive(serde::Serialize)]
249        struct ComponentUsage {
250            minimum_required_bytes: usize,
251            firm_limit_bytes: usize,
252            actual_live_bytes: usize,
253        }
254
255        let empty_snapshot = ResourceStatsSnapshot::empty();
256        let mut component_usage: BTreeMap<String, ComponentUsage> = BTreeMap::new();
257        let mut inner = self.inner.lock().unwrap();
258
259        ResourceGroupRegistry::global().visit_resource_groups(|component_name, component_stats| {
260            let component_meta = inner.get_or_create(component_name);
261            let component_meta = component_meta.lock().unwrap();
262            let bounds = component_meta.self_bounds();
263            let stats_snapshot = component_stats.snapshot_delta(&empty_snapshot);
264
265            component_usage.insert(
266                component_name.to_string(),
267                ComponentUsage {
268                    minimum_required_bytes: bounds.total_minimum_required_bytes(),
269                    firm_limit_bytes: bounds.total_firm_limit_bytes(),
270                    actual_live_bytes: stats_snapshot.live_bytes(),
271                },
272            );
273        });
274
275        serde_json::to_string_pretty(&component_usage).unwrap_or_else(|e| format!("{{\"error\": \"{e}\"}}"))
276    }
277
278    /// Gets the total minimum required bytes for all components in the registry.
279    ///
280    /// See [`ComponentRegistry::as_bounds`] for more details.
281    pub fn as_bounds(&self) -> ComponentBounds {
282        self.inner.lock().unwrap().as_bounds()
283    }
284}
285
286impl Default for ComponentRegistry {
287    fn default() -> Self {
288        Self {
289            root: Arc::new(Mutex::new(ComponentMetadata::from_full_name(None))),
290        }
291    }
292}
293
294pub struct Minimum;
295pub struct Firm;
296
297pub(crate) mod private {
298    pub trait Sealed {}
299
300    impl Sealed for super::Minimum {}
301    impl Sealed for super::Firm {}
302}
303
304// Simple trait-based builder state approach so we can use a single builder view to modify either the minimum required
305// or firm limit amounts.
306pub trait BoundsMutator: private::Sealed {
307    fn add_usage(bounds: &mut ComponentBounds, expr: UsageExpr);
308}
309
310impl BoundsMutator for Minimum {
311    fn add_usage(bounds: &mut ComponentBounds, expr: UsageExpr) {
312        bounds.self_minimum_required_bytes.push(expr)
313    }
314}
315
316impl BoundsMutator for Firm {
317    fn add_usage(bounds: &mut ComponentBounds, expr: UsageExpr) {
318        bounds.self_firm_limit_bytes.push(expr)
319    }
320}
321
322/// Builder for defining the memory bounds of a component and its subcomponents.
323///
324/// This builder provides a simple interface for defining the minimum and firm bounds of a component, as well as
325/// declaring subcomponents. For example, a topology can contain its own "self" memory bounds, and then define the
326/// individual bounds for each component in the topology.
327pub struct MemoryBoundsBuilder<'a> {
328    node: Arc<Mutex<ComponentMetadata>>,
329    _lt: PhantomData<&'a ()>,
330}
331
332impl MemoryBoundsBuilder<'static> {
333    #[cfg(test)]
334    pub(crate) fn for_test() -> Self {
335        Self {
336            node: Arc::new(Mutex::new(ComponentMetadata::from_full_name(None))),
337            _lt: PhantomData,
338        }
339    }
340}
341
342impl MemoryBoundsBuilder<'_> {
343    /// Resets the bounds of the current component to a default state.
344    ///
345    /// This can be used in scenarios where the bounds of a component need to be redefined after they have been
346    /// specified, as not all components are able to be defined in a single pass.
347    pub fn reset(&mut self) {
348        let mut node = self.node.lock().unwrap();
349        node.reset();
350    }
351
352    /// Gets a builder object for defining the minimum bounds of the current component.
353    pub fn minimum(&mut self) -> BoundsBuilder<'_, Minimum> {
354        let bounds = self.node.lock().unwrap();
355        BoundsBuilder::<'_, Minimum>::new(bounds)
356    }
357
358    /// Gets a builder object for defining the firm bounds of the current component.
359    ///
360    /// The firm limit is additive with the minimum required memory, so entries that are added via `minimum` don't need
361    /// to be added again here.
362    pub fn firm(&mut self) -> BoundsBuilder<'_, Firm> {
363        let bounds = self.node.lock().unwrap();
364        BoundsBuilder::<'_, Firm>::new(bounds)
365    }
366
367    /// Creates a nested subcomponent and gets a builder object for it.
368    ///
369    /// This allows for defining the bounds of various subcomponents within a larger component, which are then rolled up
370    /// into the calculated bounds for the parent component.
371    ///
372    /// `name` is a relative label (single segment, or a dotted path for further nesting) that is sanitized and appended
373    /// beneath the current component.
374    pub fn subcomponent<S>(&mut self, name: S) -> MemoryBoundsBuilder<'_>
375    where
376        S: AsRef<str>,
377    {
378        let node = self
379            .node
380            .lock()
381            .unwrap()
382            .get_or_create(SubsystemIdentifier::from_dotted(name.as_ref()).to_string());
383        MemoryBoundsBuilder { node, _lt: PhantomData }
384    }
385
386    /// Creates a nested subcomponent based on the given component.
387    ///
388    /// This allows for defining a subcomponent whose bounds come from an object that implements `MemoryBounds` directly.
389    pub fn with_subcomponent<S, C>(&mut self, name: S, component: &C) -> &mut Self
390    where
391        S: AsRef<str>,
392        C: MemoryBounds,
393    {
394        let mut builder = self.subcomponent(name);
395        component.specify_bounds(&mut builder);
396
397        self
398    }
399
400    #[cfg(test)]
401    pub(crate) fn as_bounds(&self) -> ComponentBounds {
402        self.node.lock().unwrap().as_bounds()
403    }
404}
405
406/// Bounds builder.
407///
408/// Helper type for defining the bounds of a component in a field-driven manner.
409pub struct BoundsBuilder<'a, S> {
410    inner: MutexGuard<'a, ComponentMetadata>,
411    _state: PhantomData<S>,
412}
413
414impl<'a, S: BoundsMutator> BoundsBuilder<'a, S> {
415    fn new(inner: MutexGuard<'a, ComponentMetadata>) -> Self {
416        Self {
417            inner,
418            _state: PhantomData,
419        }
420    }
421
422    /// Accounts for the in-memory size of a single value.
423    ///
424    /// This is useful for tracking the expected memory usage of a single instance of a type if that type is heap
425    /// allocated. For example, components that are spawned by a topology generally end up being boxed, which means a
426    /// heap allocation exists that's the size of the component type.
427    pub fn with_single_value<T>(&mut self, name: impl Into<String>) -> &mut Self {
428        S::add_usage(&mut self.inner.bounds, UsageExpr::struct_size::<T>(name));
429        self
430    }
431
432    /// Accounts for a fixed amount of memory usage.
433    ///
434    /// This is a catch-all for directly accounting for a specific number of bytes.
435    pub fn with_fixed_amount(&mut self, name: impl Into<String>, chunk_size: usize) -> &mut Self {
436        S::add_usage(&mut self.inner.bounds, UsageExpr::constant(name, chunk_size));
437        self
438    }
439
440    /// Accounts for an item container of the given length.
441    ///
442    /// This can be used to track the expected memory usage of generalized containers like `Vec<T>`, where items are
443    /// homogeneous and allocated contiguously.
444    pub fn with_array<T>(&mut self, name: impl Into<String>, len: usize) -> &mut Self {
445        S::add_usage(
446            &mut self.inner.bounds,
447            UsageExpr::product(
448                "array",
449                UsageExpr::struct_size::<T>(name),
450                UsageExpr::constant("len", len),
451            ),
452        );
453        self
454    }
455
456    /// Accounts for a map container of the given length.
457    ///
458    /// This can be used to track the expected memory usage of generalized maps like `HashMap<K, V>`, where keys and
459    /// values are
460    pub fn with_map<K, V>(&mut self, name: impl Into<String>, len: usize) -> &mut Self {
461        S::add_usage(
462            &mut self.inner.bounds,
463            UsageExpr::product(
464                "map",
465                UsageExpr::sum(
466                    name,
467                    UsageExpr::struct_size::<K>("key"),
468                    UsageExpr::struct_size::<V>("value"),
469                ),
470                UsageExpr::constant("len", len),
471            ),
472        );
473        self
474    }
475
476    pub fn with_expr(&mut self, expr: UsageExpr) -> &mut Self {
477        S::add_usage(&mut self.inner.bounds, expr);
478        self
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485
486    fn id(dotted: &str) -> SubsystemIdentifier {
487        SubsystemIdentifier::from_dotted(dotted)
488    }
489
490    #[test]
491    fn bounds_builder_records_bounds_under_absolute_id() {
492        let registry = ComponentRegistry::default();
493        registry
494            .bounds_builder(&id("topology.primary.sources.dsd_in"))
495            .firm()
496            .with_fixed_amount("buffer", 128);
497
498        assert_eq!(registry.as_bounds().total_firm_limit_bytes(), 128);
499    }
500
501    #[test]
502    fn repeated_bounds_builder_for_same_id_targets_the_same_node() {
503        // Two `bounds_builder` calls with the same identifier resolve to the same component node, so their bounds
504        // accumulate rather than replace.
505        let registry = ComponentRegistry::default();
506        let component = id("topology.primary.transforms.enrich");
507
508        registry
509            .bounds_builder(&component)
510            .firm()
511            .with_fixed_amount("first", 100);
512        registry
513            .bounds_builder(&component)
514            .firm()
515            .with_fixed_amount("second", 200);
516
517        assert_eq!(registry.as_bounds().total_firm_limit_bytes(), 300);
518    }
519
520    #[test]
521    fn distinct_absolute_ids_roll_up_to_the_root() {
522        // Distinct absolute identifiers that share a prefix create a nested structure whose bounds roll up.
523        let registry = ComponentRegistry::default();
524        registry
525            .bounds_builder(&id("topology.primary.sources.in"))
526            .firm()
527            .with_fixed_amount("a", 100);
528        registry
529            .bounds_builder(&id("topology.primary.destinations.out"))
530            .firm()
531            .with_fixed_amount("b", 200);
532
533        assert_eq!(registry.as_bounds().total_firm_limit_bytes(), 300);
534    }
535
536    #[test]
537    fn relative_subcomponents_roll_up_under_their_component() {
538        // The bounds builder's relative `subcomponent` nests beneath the component addressed by its absolute id.
539        let registry = ComponentRegistry::default();
540        {
541            let mut builder = registry.bounds_builder(&id("env_provider.workload.remote_agent"));
542            builder.firm().with_fixed_amount("self", 10);
543            builder
544                .subcomponent("string_interner")
545                .firm()
546                .with_fixed_amount("interner", 90);
547        }
548
549        assert_eq!(registry.as_bounds().total_firm_limit_bytes(), 100);
550    }
551
552    #[test]
553    fn resource_group_token_and_bounds_builder_share_the_same_node() {
554        // Taking a resource group token creates the component node; declaring bounds for the same identifier targets
555        // that same node, and the read-only handle observes the result.
556        let registry = ComponentRegistry::default();
557        let component = id("topology.primary.sources.dsd_in");
558
559        let _token = registry.get_resource_group_token(&component);
560        registry
561            .bounds_builder(&component)
562            .minimum()
563            .with_fixed_amount("buffer", 64);
564
565        assert_eq!(registry.root().as_bounds().total_minimum_required_bytes(), 64);
566    }
567
568    #[test]
569    fn with_array_accounts_for_element_size_times_length() {
570        // `with_array` builds a `product(struct_size::<T>, len)` expression, so a 10-element array of `u64` should
571        // account for exactly `size_of::<u64>() * 10` bytes.
572        let mut builder = MemoryBoundsBuilder::for_test();
573        builder.firm().with_array::<u64>("array", 10);
574
575        let bounds = builder.as_bounds();
576        assert_eq!(bounds.total_firm_limit_bytes(), std::mem::size_of::<u64>() * 10);
577        assert_eq!(bounds.total_minimum_required_bytes(), 0);
578    }
579
580    #[test]
581    fn with_map_accounts_for_entry_size_times_length() {
582        // `with_map` builds a `product(sum(struct_size::<K>, struct_size::<V>), len)` expression, so a 5-entry map
583        // of `u32 -> u64` should account for exactly `(size_of::<u32>() + size_of::<u64>()) * 5` bytes.
584        let mut builder = MemoryBoundsBuilder::for_test();
585        builder.firm().with_map::<u32, u64>("map", 5);
586
587        let bounds = builder.as_bounds();
588        let expected = (std::mem::size_of::<u32>() + std::mem::size_of::<u64>()) * 5;
589        assert_eq!(bounds.total_firm_limit_bytes(), expected);
590        assert_eq!(bounds.total_minimum_required_bytes(), 0);
591    }
592
593    #[test]
594    fn minimum_bounds_are_included_in_the_firm_total() {
595        // The firm limit is documented as additive with the minimum required bytes, so an array added to `minimum`
596        // and a map added to `firm` should both roll up into `total_firm_limit_bytes`.
597        let mut builder = MemoryBoundsBuilder::for_test();
598        builder.minimum().with_array::<u64>("array", 10);
599        builder.firm().with_map::<u32, u64>("map", 5);
600
601        let bounds = builder.as_bounds();
602        let array_bytes = std::mem::size_of::<u64>() * 10;
603        let map_bytes = (std::mem::size_of::<u32>() + std::mem::size_of::<u64>()) * 5;
604
605        assert_eq!(bounds.total_minimum_required_bytes(), array_bytes);
606        assert_eq!(bounds.total_firm_limit_bytes(), array_bytes + map_bytes);
607    }
608}