saluki_core/runtime/tree/snapshot.rs
1//! Serialized form of a supervision-tree snapshot.
2//!
3//! These are the wire types: the stable shape that [`SupervisionTreeHandle::snapshot`][super::SupervisionTreeHandle]
4//! produces, the API route serves, and the CLI decodes. They are deliberately separate from the live bookkeeping in
5//! the parent module -- a rename here changes a payload that shipped binaries parse, which is a very different kind of
6//! change from adjusting how the tree is tracked at runtime.
7
8use saluki_common::resource_tracking::ResourceStatsSnapshot;
9use serde::{Deserialize, Serialize};
10
11use crate::runtime::{restart::RestartType, supervisor::AutoShutdown, RestartMode};
12
13/// A point in time, as milliseconds since the Unix epoch.
14///
15/// Serialized as a plain integer. The standard library's own representation of a timestamp serializes as a pair of
16/// fields, which is awkward for a consumer that just wants to render a time.
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
18#[serde(transparent)]
19pub struct UnixMillis(pub u64);
20
21/// A point-in-time view of a supervision tree.
22#[derive(Clone, Debug, Deserialize, Serialize)]
23pub struct TreeSnapshot {
24 /// When the snapshot was taken.
25 pub captured_at: UnixMillis,
26
27 /// Whether allocations are being tracked at all.
28 ///
29 /// When false, every byte count in the snapshot reads zero because nothing is measuring, rather than because
30 /// nothing has been allocated. Distinguishing the two matters: the tracking allocator has to be installed as the
31 /// process's global allocator, which not every embedding does.
32 pub resource_tracking_enabled: bool,
33
34 /// Aggregate counts across the whole tree.
35 pub totals: TreeTotals,
36
37 /// The supervisor the snapshot was taken from, and everything beneath it.
38 pub root: NodeSnapshot,
39}
40
41/// Aggregate counts across a whole [`TreeSnapshot`].
42#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
43pub struct TreeTotals {
44 /// Number of supervisors in the tree.
45 pub supervisors: usize,
46
47 /// Number of leaf workers in the tree.
48 pub workers: usize,
49
50 /// Number of nodes currently running.
51 pub running: usize,
52
53 /// Number of nodes that ran and have since exited without being restarted.
54 pub exited: usize,
55
56 /// Number of nodes that are declared but have never run.
57 pub registered: usize,
58
59 /// Total restarts across every node in the tree.
60 pub restarts: u64,
61
62 /// Total live bytes across every distinct resource group in the tree.
63 ///
64 /// Summed over groups rather than over nodes: several nodes can share one group, and their usage is one figure
65 /// rather than one per node.
66 pub live_bytes: u64,
67
68 /// Total CPU time across every distinct resource group in the tree, in nanoseconds.
69 pub cpu_time_nanos: u64,
70
71 /// Depth of the deepest node, counting the root as 1.
72 pub max_depth: usize,
73}
74
75/// Whether a node supervises other nodes or performs work itself.
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
77#[serde(rename_all = "snake_case")]
78pub enum NodeKind {
79 /// A supervisor, which manages other nodes.
80 Supervisor,
81
82 /// A worker, which performs work itself.
83 ///
84 /// The children of a worker, if any, are the children that it spawned into its own scope. The children can also
85 /// include a supervisor that the worker drives inside its own future.
86 Worker,
87}
88
89/// Where a node is in its lifecycle.
90#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
91#[serde(rename_all = "snake_case")]
92pub enum NodeState {
93 /// Declared, but not currently running.
94 ///
95 /// Either it has never run, or -- for a supervisor being restarted -- it has stopped and its next generation has
96 /// not yet started.
97 Registered,
98
99 /// Currently running.
100 Running,
101
102 /// Ran, exited, and was not restarted.
103 Exited,
104}
105
106/// One node -- a supervisor or a worker -- in a [`TreeSnapshot`].
107#[derive(Clone, Debug, Deserialize, Serialize)]
108pub struct NodeSnapshot {
109 /// The node's bare name, as registered with its supervisor.
110 pub name: String,
111
112 /// Whether the node is a supervisor or a worker.
113 pub kind: NodeKind,
114
115 /// The node's fully qualified, dot-scoped process name. `None` if the node has never run.
116 pub process_name: Option<String>,
117
118 /// Identifier of the node's most recent process. `None` if the node has never run.
119 ///
120 /// A restart gives the node a new process, and so a new identifier. A node that has stopped keeps the identifier
121 /// it last ran under, which is what `state` is for: this says what the node ran as, and `state` says whether it
122 /// still is.
123 pub process_id: Option<u64>,
124
125 /// Where the node is in its lifecycle.
126 pub state: NodeState,
127
128 /// The node's restart policy.
129 pub restart: RestartType,
130
131 /// Whether the node's termination can drive its supervisor to shut down.
132 pub significant: bool,
133
134 /// When the node first became part of the tree.
135 ///
136 /// Constant across restarts, so the difference between this and `started_at` is the time the node has spent not
137 /// running since it was created.
138 pub created_at: UnixMillis,
139
140 /// When the node's most recent process started. `None` if the node has never run.
141 pub started_at: Option<UnixMillis>,
142
143 /// How long the node's current process has been running, in milliseconds. `None` unless it is running.
144 pub uptime_ms: Option<u64>,
145
146 /// How many times the node has been restarted since it was created.
147 ///
148 /// Counts every restart that gave the node a new process, whether it was restarted on its own account or brought
149 /// back as part of its supervisor restarting -- either by a group restart or by the supervisor itself being
150 /// restarted from above. Only statically declared nodes accumulate this across a supervisor restart, since only
151 /// they have an identity that survives one; a dynamically spawned node is never restored.
152 pub restart_count: u32,
153
154 /// When the node exited without being restarted. `None` unless it has exited.
155 pub exited_at: Option<UnixMillis>,
156
157 /// The resource group the node's allocations are attributed to. `None` if the node has never run.
158 ///
159 /// For a supervisor this is its own group. For a worker it is its supervisor's, since a worker inherits its
160 /// supervisor's group rather than owning one.
161 pub resource_group: Option<String>,
162
163 /// Resource usage attributed to this node.
164 ///
165 /// Populated for a supervisor, which owns a resource group covering itself and its workers. Always absent for a
166 /// worker, whose usage is counted against the supervisor named by `resource_group`.
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub resources: Option<ResourceUsage>,
169
170 /// How the node supervises its children. Absent for a worker.
171 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub supervision: Option<SupervisionSettings>,
173
174 /// The node's children.
175 ///
176 /// For a worker, this holds the children that it spawned into its own scope, and any supervisor that it drives.
177 /// For most workers, it is empty.
178 pub children: Vec<NodeSnapshot>,
179}
180
181/// Cumulative resource usage for one resource group.
182///
183/// Counts are since the process started. Both allocation counts and CPU time depend on process-wide facilities that
184/// may not be available: see [`TreeSnapshot::resource_tracking_enabled`].
185#[derive(Clone, Debug, Default, Deserialize, Serialize)]
186pub struct ResourceUsage {
187 /// Bytes allocated.
188 pub allocated_bytes: u64,
189
190 /// Objects allocated.
191 pub allocated_objects: u64,
192
193 /// Bytes deallocated.
194 pub deallocated_bytes: u64,
195
196 /// Objects deallocated.
197 pub deallocated_objects: u64,
198
199 /// Bytes allocated and not yet deallocated.
200 pub live_bytes: u64,
201
202 /// Objects allocated and not yet deallocated.
203 pub live_objects: u64,
204
205 /// CPU time consumed, in nanoseconds.
206 ///
207 /// Always zero where per-thread CPU time is unavailable.
208 pub cpu_time_nanos: u64,
209}
210
211impl From<&ResourceStatsSnapshot> for ResourceUsage {
212 fn from(stats: &ResourceStatsSnapshot) -> Self {
213 Self {
214 allocated_bytes: stats.allocated_bytes as u64,
215 allocated_objects: stats.allocated_objects as u64,
216 deallocated_bytes: stats.deallocated_bytes as u64,
217 deallocated_objects: stats.deallocated_objects as u64,
218 live_bytes: stats.live_bytes() as u64,
219 live_objects: stats.live_objects() as u64,
220 cpu_time_nanos: stats.cpu_time_nanos,
221 }
222 }
223}
224
225/// How a supervisor supervises its children, and how it has fared.
226#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
227pub struct SupervisionSettings {
228 /// Whether a failing child is restarted alone or together with its siblings.
229 pub restart_mode: RestartMode,
230
231 /// How many restarts the supervisor tolerates within `restart_period_ms` before giving up.
232 pub restart_intensity: usize,
233
234 /// The window over which `restart_intensity` is measured, in milliseconds.
235 pub restart_period_ms: u64,
236
237 /// Whether the termination of a significant child drives the supervisor to shut down.
238 pub auto_shutdown: AutoShutdown,
239
240 /// Worker threads on the supervisor's own runtime. `None` if it runs on its parent's runtime.
241 pub dedicated_threads: Option<usize>,
242
243 /// How many child restarts the supervisor has performed, across all of its own generations.
244 ///
245 /// A group restart counts once here however many children it brought back, which is what distinguishes a
246 /// supervisor restarting its whole group repeatedly from a single child restarting repeatedly.
247 pub restarts_performed: u64,
248
249 /// How many times the supervisor has started running.
250 pub generation: u64,
251}