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