1use std::collections::BTreeMap;
17use std::sync::Mutex;
18use std::time::Duration;
19
20use catio::Scheduler;
21use lazy_static::lazy_static;
22use prometheus::core::{Collector, Desc};
23use prometheus::proto::MetricFamily;
24use prometheus::*;
25
26use crate::global::{QUERY_TASK_CLASS, WRITE_TASK_CLASS};
27
28pub const THREAD_NAME_LABEL: &str = "thread_name";
29
30lazy_static! {
31 pub static ref METRIC_RUNTIME_THREADS_ALIVE: IntGaugeVec = register_int_gauge_vec!(
32 "greptime_runtime_threads_alive",
33 "runtime threads alive",
34 &[THREAD_NAME_LABEL]
35 )
36 .unwrap();
37 pub static ref METRIC_RUNTIME_THREADS_IDLE: IntGaugeVec = register_int_gauge_vec!(
38 "greptime_runtime_threads_idle",
39 "runtime threads idle",
40 &[THREAD_NAME_LABEL]
41 )
42 .unwrap();
43}
44
45#[derive(Clone, Default)]
46struct ClassSnapshot {
47 polls: u64,
48 total_admission_wait: Duration,
49}
50
51struct WorkloadSchedulerCollector {
52 scheduler: Scheduler,
53 enabled: IntGauge,
54 active: IntGauge,
55 weight: IntGaugeVec,
56 queued: IntGaugeVec,
57 polls: IntCounterVec,
58 total_admission_wait: CounterVec,
59 snapshots: Mutex<BTreeMap<&'static str, ClassSnapshot>>,
60}
61
62impl WorkloadSchedulerCollector {
63 fn new(scheduler: Scheduler) -> Self {
64 let workload_label = &["workload"];
65 Self {
66 scheduler,
67 enabled: IntGauge::new(
68 "greptime_workload_scheduler_enabled",
69 "Whether the workload scheduler is enabled",
70 )
71 .unwrap(),
72 active: IntGauge::new(
73 "greptime_workload_scheduler_active_polls",
74 "Task polls admitted to Tokio but not yet completed",
75 )
76 .unwrap(),
77 weight: IntGaugeVec::new(
78 Opts::new(
79 "greptime_workload_scheduler_weight",
80 "Configured workload scheduler weight",
81 ),
82 workload_label,
83 )
84 .unwrap(),
85 queued: IntGaugeVec::new(
86 Opts::new(
87 "greptime_workload_scheduler_queued_tasks",
88 "Tasks queued in the workload scheduler",
89 ),
90 workload_label,
91 )
92 .unwrap(),
93 polls: IntCounterVec::new(
94 Opts::new(
95 "greptime_workload_scheduler_polls_total",
96 "Cumulative task polls admitted by the workload scheduler",
97 ),
98 workload_label,
99 )
100 .unwrap(),
101 total_admission_wait: CounterVec::new(
102 Opts::new(
103 "greptime_workload_scheduler_admission_wait_seconds_total",
104 "Cumulative workload scheduler admission wait time in seconds",
105 ),
106 workload_label,
107 )
108 .unwrap(),
109 snapshots: Mutex::new(BTreeMap::new()),
110 }
111 }
112
113 fn update_locked(&self, snapshots: &mut BTreeMap<&'static str, ClassSnapshot>) {
114 let stats = self.scheduler.stats();
115 self.enabled.set(i64::from(self.scheduler.is_enabled()));
116 self.active.set(
117 stats
118 .active_polls
119 .min(i64::MAX as usize)
120 .try_into()
121 .unwrap_or(i64::MAX),
122 );
123
124 for (class, workload) in [(QUERY_TASK_CLASS, "query"), (WRITE_TASK_CLASS, "write")] {
125 let class_stats = stats.classes.get(&class).cloned().unwrap_or_default();
126 let labels = &[workload];
127 self.weight
128 .with_label_values(labels)
129 .set(i64::from(class_stats.weight));
130 self.queued.with_label_values(labels).set(
131 class_stats
132 .queued
133 .min(i64::MAX as usize)
134 .try_into()
135 .unwrap_or(i64::MAX),
136 );
137 let previous = snapshots.entry(workload).or_default();
138 self.polls
139 .with_label_values(labels)
140 .inc_by(class_stats.polls.saturating_sub(previous.polls));
141 self.total_admission_wait.with_label_values(labels).inc_by(
142 class_stats
143 .total_admission_wait
144 .saturating_sub(previous.total_admission_wait)
145 .as_secs_f64(),
146 );
147 *previous = ClassSnapshot {
148 polls: class_stats.polls,
149 total_admission_wait: class_stats.total_admission_wait,
150 };
151 }
152 }
153}
154
155impl Collector for WorkloadSchedulerCollector {
156 fn desc(&self) -> Vec<&Desc> {
157 let mut desc = self.enabled.desc();
158 desc.extend(self.active.desc());
159 desc.extend(self.weight.desc());
160 desc.extend(self.queued.desc());
161 desc.extend(self.polls.desc());
162 desc.extend(self.total_admission_wait.desc());
163 desc
164 }
165
166 fn collect(&self) -> Vec<MetricFamily> {
167 let mut snapshots = self.snapshots.lock().unwrap();
168 self.update_locked(&mut snapshots);
169 let mut families = self.enabled.collect();
170 families.extend(self.active.collect());
171 families.extend(self.weight.collect());
172 families.extend(self.queued.collect());
173 families.extend(self.polls.collect());
174 families.extend(self.total_admission_wait.collect());
175 families
176 }
177}
178
179pub(crate) fn register_workload_scheduler_metrics(scheduler: Scheduler) {
180 register(Box::new(WorkloadSchedulerCollector::new(scheduler)))
181 .expect("workload scheduler metrics collector registration must succeed");
182}
183
184#[cfg(test)]
185mod tests {
186 use std::collections::{BTreeMap, BTreeSet};
187 use std::time::Duration;
188
189 use prometheus::proto::{MetricFamily, MetricType};
190
191 use super::*;
192
193 fn family<'a>(families: &'a [MetricFamily], name: &str) -> &'a MetricFamily {
194 families
195 .iter()
196 .find(|family| family.name() == name)
197 .unwrap_or_else(|| panic!("missing metric family {name}"))
198 }
199
200 fn counter_values(families: &[MetricFamily]) -> BTreeMap<String, BTreeMap<String, f64>> {
201 [
202 "greptime_workload_scheduler_polls_total",
203 "greptime_workload_scheduler_admission_wait_seconds_total",
204 ]
205 .into_iter()
206 .map(|name| {
207 let values = family(families, name)
208 .get_metric()
209 .iter()
210 .map(|metric| {
211 (
212 metric.get_label()[0].value().to_string(),
213 metric.get_counter().value(),
214 )
215 })
216 .collect();
217 (name.to_string(), values)
218 })
219 .collect()
220 }
221
222 #[test]
223 fn workload_scheduler_collector_reports_class_metrics_and_deltas() {
224 let scheduler = Scheduler::builder()
225 .max_concurrent_polls(1)
226 .weight(QUERY_TASK_CLASS, 2)
227 .weight(WRITE_TASK_CLASS, 3)
228 .build();
229 scheduler.set_enabled(true);
230 let collector = WorkloadSchedulerCollector::new(scheduler.clone());
231
232 let first = collector.collect();
233 let expected = [
234 (
235 "greptime_workload_scheduler_enabled",
236 MetricType::GAUGE,
237 false,
238 ),
239 (
240 "greptime_workload_scheduler_active_polls",
241 MetricType::GAUGE,
242 false,
243 ),
244 (
245 "greptime_workload_scheduler_weight",
246 MetricType::GAUGE,
247 true,
248 ),
249 (
250 "greptime_workload_scheduler_queued_tasks",
251 MetricType::GAUGE,
252 true,
253 ),
254 (
255 "greptime_workload_scheduler_polls_total",
256 MetricType::COUNTER,
257 true,
258 ),
259 (
260 "greptime_workload_scheduler_admission_wait_seconds_total",
261 MetricType::COUNTER,
262 true,
263 ),
264 ];
265 let expected_names: BTreeSet<_> = expected.iter().map(|(name, _, _)| *name).collect();
266 let actual_names: BTreeSet<_> = first.iter().map(MetricFamily::name).collect();
267 assert_eq!(expected_names, actual_names);
268 for (name, metric_type, has_workload_label) in expected {
269 let metric_family = family(&first, name);
270 assert_eq!(metric_type, metric_family.get_field_type(), "{name}");
271 let workloads: BTreeSet<_> = metric_family
272 .get_metric()
273 .iter()
274 .flat_map(|metric| {
275 assert_eq!(has_workload_label, !metric.get_label().is_empty());
276 metric
277 .get_label()
278 .iter()
279 .map(|label| {
280 assert_eq!("workload", label.name());
281 label.value()
282 })
283 .collect::<Vec<_>>()
284 })
285 .collect();
286 if has_workload_label {
287 assert_eq!(BTreeSet::from(["query", "write"]), workloads);
288 } else {
289 assert!(workloads.is_empty());
290 }
291 }
292
293 let second = collector.collect();
294 assert_eq!(counter_values(&first), counter_values(&second));
295
296 scheduler.set_enabled(true);
297 let runtime = tokio::runtime::Builder::new_current_thread()
298 .enable_time()
299 .build()
300 .unwrap();
301 runtime.block_on(async {
302 let query = scheduler.spawn_in(QUERY_TASK_CLASS, async {
303 tokio::time::sleep(Duration::from_millis(1)).await;
304 });
305 let write = scheduler.spawn_in(WRITE_TASK_CLASS, async {
306 tokio::time::sleep(Duration::from_millis(1)).await;
307 });
308 tokio::time::timeout(Duration::from_secs(1), async {
309 query.await.unwrap();
310 write.await.unwrap();
311 })
312 .await
313 .expect("scheduled test tasks did not complete");
314 });
315
316 let third = collector.collect();
317 let before = counter_values(&second);
318 let after = counter_values(&third);
319 for workload in ["query", "write"] {
320 let metric = "greptime_workload_scheduler_polls_total";
321 assert!(
322 after[metric][workload] > before[metric][workload],
323 "{metric} did not increase for {workload}"
324 );
325 }
326 }
327}