saluki_env/workload/collectors/
cgroups.rs1use std::time::Duration;
2
3use async_trait::async_trait;
4use saluki_common::{
5 collections::{FastHashMap, FastHashSet},
6 sync::shutdown::ShutdownHandle,
7};
8use saluki_core::accounting::{MemoryBounds, MemoryBoundsBuilder};
9use saluki_core::health::Health;
10use saluki_error::{generic_error, ErrorContext as _, GenericError};
11use saluki_metrics::{static_metrics, Counter};
12use stringtheory::{interning::GenericMapInterner, MetaString};
13use tokio::{pin, select, sync::mpsc};
14use tracing::{debug, warn};
15
16use super::MetadataCollector;
17use crate::workload::{
18 entity::EntityId,
19 helpers::cgroups::{CgroupsConfiguration, CgroupsReader},
20 metadata::MetadataOperation,
21};
22
23#[static_metrics(prefix = cgroups_metadata_collector)]
24#[derive(Clone)]
25struct Telemetry {
26 traversals_complete_total: Counter,
31
32 traversals_incomplete_total: Counter,
34
35 traversal_paths_skipped_total: Counter,
37}
38
39pub struct CgroupsMetadataCollector {
49 reader: CgroupsReader,
50 health: Health,
51}
52
53impl CgroupsMetadataCollector {
54 pub async fn new(
60 cgroups_config: &CgroupsConfiguration, health: Health, interner: GenericMapInterner,
61 ) -> Result<Self, GenericError> {
62 let reader = match CgroupsReader::try_from_config(cgroups_config, interner)? {
63 Some(reader) => reader,
64 None => {
65 return Err(generic_error!("Failed to detect any cgroups v1/v2 hierarchy. "));
66 }
67 };
68
69 Ok(Self { reader, health })
70 }
71}
72
73#[async_trait]
74impl MetadataCollector for CgroupsMetadataCollector {
75 fn name(&self) -> &'static str {
76 "cgroups"
77 }
78
79 async fn watch(&mut self, operations_tx: &mut mpsc::Sender<MetadataOperation>) -> Result<(), GenericError> {
80 self.health.mark_ready();
81
82 let mut cgroups_manager = SynchronousCgroupsManager::from_reader(self.reader.clone());
86 let operations_tx = operations_tx.clone();
87
88 let (_shutdown_coordinator, shutdown_handle) = ShutdownHandle::paired();
91 let poller_handle = tokio::task::spawn_blocking(move || cgroups_manager.poll(operations_tx, shutdown_handle));
92 pin!(poller_handle);
93
94 debug!("Spawned cgroups background poller task.");
95
96 let final_result = loop {
97 select! {
98 _ = self.health.live() => {},
99 result = &mut poller_handle => match result {
100 Ok(Ok(())) => break Ok(()),
101 Ok(Err(e)) => break Err(e).error_context("Cgroups background poller task encountered an error."),
102 Err(e) => break Err(e).error_context("Cgroups background poller task panicked."),
103 }
104 }
105 };
106
107 self.health.mark_not_ready();
108
109 final_result
110 }
111}
112
113impl MemoryBounds for CgroupsMetadataCollector {
114 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
115 builder
116 .minimum()
117 .with_array::<MetadataOperation>("metadata operations", 64);
119 builder.firm().with_single_value::<Self>("component struct");
126 }
127}
128
129struct SynchronousCgroupsManager {
130 reader: CgroupsReader,
131 active_cgroups: FastHashMap<u64, MetaString>,
132 operations: Vec<MetadataOperation>,
133 telemetry: Telemetry,
134}
135
136impl SynchronousCgroupsManager {
137 fn from_reader(reader: CgroupsReader) -> Self {
138 Self {
139 reader,
140 active_cgroups: FastHashMap::default(),
141 operations: Vec::with_capacity(64),
142 telemetry: Telemetry::new(),
143 }
144 }
145
146 fn poll(
147 &mut self, operations_tx: mpsc::Sender<MetadataOperation>, shutdown_handle: ShutdownHandle,
148 ) -> Result<(), GenericError> {
149 let mut traversed_cgroups = FastHashSet::default();
150 let mut cgroups_to_delete = Vec::new();
151
152 loop {
153 if operations_tx.is_closed() || shutdown_handle.is_triggered() {
155 return Ok(());
156 }
157
158 traversed_cgroups.clear();
159
160 let start = std::time::Instant::now();
161
162 let traversal = self.reader.get_child_cgroups();
165 let traversal_complete = traversal.is_complete();
166 let skipped = traversal.skipped();
167
168 if skipped > 0 {
169 self.telemetry.traversal_paths_skipped_total().increment(skipped as u64);
170 }
171
172 let child_cgroups = traversal.into_cgroups();
173 let child_cgroups_len = child_cgroups.len();
174
175 for child_cgroup in child_cgroups {
176 if let Some(cgroup_inode) = child_cgroup.inode() {
177 traversed_cgroups.insert(cgroup_inode);
178
179 if !self.active_cgroups.contains_key(&cgroup_inode) {
181 let container_id = child_cgroup.into_container_id();
182 debug!(%cgroup_inode, %container_id, "Found new container-based cgroup.");
183
184 self.active_cgroups.insert(cgroup_inode, container_id.clone());
185
186 let entity_id = EntityId::ContainerInode(cgroup_inode);
188 let ancestor_entity_id = EntityId::Container(container_id);
189
190 let operation = MetadataOperation::add_alias(entity_id, ancestor_entity_id);
191 self.operations.push(operation);
192 }
193 } else {
194 let container_id = child_cgroup.into_container_id();
196 warn!(%container_id, "Encountered cgroup without controller inode during metadata traversal. This is unexpected.");
197 continue;
198 }
199 }
200
201 if traversal_complete {
208 self.telemetry.traversals_complete_total().increment(1);
209
210 for cgroup_inode in self.active_cgroups.keys() {
211 if !traversed_cgroups.contains(cgroup_inode) {
212 cgroups_to_delete.push(*cgroup_inode);
214 }
215 }
216 } else {
217 self.telemetry.traversals_incomplete_total().increment(1);
218 debug!(
219 skipped,
220 "Cgroups hierarchy traversal was incomplete. Skipping removal of cgroups that were not seen."
221 );
222 }
223
224 for cgroup_inode in cgroups_to_delete.drain(..) {
226 if let Some(container_id) = self.active_cgroups.remove(&cgroup_inode) {
227 debug!(%cgroup_inode, %container_id, "Removing old container-based cgroup.");
228
229 let entity_id = EntityId::ContainerInode(cgroup_inode);
231 let ancestor_entity_id = EntityId::Container(container_id);
232
233 let operation = MetadataOperation::remove_alias(entity_id, ancestor_entity_id);
234 self.operations.push(operation);
235 } else {
236 warn!(%cgroup_inode, "Tried to remove a cgroup that was not in the active set.");
237 }
238 }
239
240 let elapsed = start.elapsed();
241 debug!(elapsed = ?elapsed, child_cgroups_len, "Traversed cgroups.");
242
243 for operation in self.operations.drain(..) {
245 if operations_tx.blocking_send(operation).is_err() {
246 return Err(GenericError::msg("Operations channel unexpectedly closed."));
247 }
248 }
249
250 if operations_tx.is_closed() || shutdown_handle.is_triggered() {
252 return Ok(());
253 }
254
255 std::thread::sleep(Duration::from_secs(2));
256 }
257 }
258}