Skip to main content

resource_accounting/
api.rs

1use std::{
2    collections::BTreeMap,
3    sync::{Arc, Mutex},
4};
5
6use saluki_api::{
7    extract::State,
8    response::IntoResponse,
9    routing::{get, Router},
10    APIHandler,
11};
12use serde::Serialize;
13
14use crate::{registry::ComponentMetadata, ResourceGroupRegistry, ResourceStatsSnapshot};
15
16#[derive(Serialize)]
17struct ComponentUsage {
18    minimum_required_bytes: usize,
19    firm_limit_bytes: usize,
20    actual_live_bytes: usize,
21}
22
23/// State used for the resource registry API handler.
24#[derive(Clone)]
25pub struct ResourceRegistryState {
26    inner: Arc<Mutex<ComponentMetadata>>,
27}
28
29impl ResourceRegistryState {
30    fn get_response(&self) -> String {
31        // The component registry is a nested structure, whereas the resource registry is a flat structure. We can
32        // only iterate via closure with the resource registry, so we do that, and for each entry, we look for it in
33        // the component registry. Luckily, the components in the resource registry use the same naming structure as
34        // the component registry supports for doing nested lookups in a single call, so we can just use that.
35
36        // TODO: This is only going to show components which have taken a token, which means some components in the
37        // registry, the ones whose bounds _are_ display when we verify bounds at startup, won't have an entry here
38        // because they don't use a token... _and_ even if we made sure to include all components in the registry, even
39        // if they didn't have an resource group associated, their usage might just be attributed to the root
40        // resource group so then you end up with components that say they have a minimum usage, etc, but show a
41        // current usage of zero bytes, etc.
42        //
43        // One option we could try is:
44        // - get the total minimum/firm bounds by rolling up all components
45        // - as we iterate over each resource group, we subtract the bounds of that component from the totals we
46        //   calculated before
47        // - at the end, we assign the remaining total bounds to the root component
48        //
49        // Essentially, because the memory usage for component with bounds that _don't_ use a token will inherently be
50        // attributed to the root resource group anyways, this would allow at least capturing those bounds for
51        // comparison against the otherwise-unattributed usage.
52        //
53        // Realistically, we _should_ still have our code set up to track all allocations for components that declare
54        // bounds, but this could be a reasonable stopgap.
55        let empty_snapshot = ResourceStatsSnapshot::empty();
56
57        let mut component_usage = BTreeMap::new();
58
59        let mut inner = self.inner.lock().unwrap();
60        ResourceGroupRegistry::global().visit_resource_groups(|component_name, component_stats| {
61            let component_meta = inner.get_or_create(component_name);
62            let component_meta = component_meta.lock().unwrap();
63            let bounds = component_meta.self_bounds();
64            let stats_snapshot = component_stats.snapshot_delta(&empty_snapshot);
65
66            component_usage.insert(
67                component_name.to_string(),
68                ComponentUsage {
69                    // TODO: figure out what's actually going on here, this might not be a valid change
70                    minimum_required_bytes: bounds.total_minimum_required_bytes(),
71                    firm_limit_bytes: bounds.total_firm_limit_bytes(),
72                    actual_live_bytes: stats_snapshot.live_bytes(),
73                },
74            );
75        });
76
77        serde_json::to_string(&component_usage).unwrap()
78    }
79}
80
81/// An API handler for reporting the resource usage and usage of all components.
82///
83/// This handler exposes a single route -- `/memory/status` -- which returns the overall bounds and live usage of each
84/// registered component.
85pub struct ResourceAPIHandler {
86    state: ResourceRegistryState,
87}
88
89impl ResourceAPIHandler {
90    pub(crate) fn from_state(state: Arc<Mutex<ComponentMetadata>>) -> Self {
91        Self {
92            state: ResourceRegistryState { inner: state },
93        }
94    }
95
96    async fn status_handler(State(state): State<ResourceRegistryState>) -> impl IntoResponse {
97        state.get_response()
98    }
99}
100
101impl APIHandler for ResourceAPIHandler {
102    type State = ResourceRegistryState;
103
104    fn generate_initial_state(&self) -> Self::State {
105        self.state.clone()
106    }
107
108    fn generate_routes(&self) -> Router<Self::State> {
109        Router::new().route("/memory/status", get(Self::status_handler))
110    }
111}