Skip to main content

ursula_runtime/
metrics.rs

1use std::sync::Arc;
2use std::sync::atomic::AtomicU64;
3use std::sync::atomic::Ordering;
4
5use ursula_shard::BucketStreamId;
6use ursula_shard::CoreId;
7use ursula_shard::RaftGroupId;
8use ursula_shard::ShardPlacement;
9use ursula_stream::StreamErrorCode;
10use ursula_stream::StreamErrorContext;
11
12use crate::engine::GroupEngine;
13use crate::engine::GroupEngineError;
14use crate::error::RuntimeError;
15use crate::request::AppendBatchRequest;
16use crate::request::ColdWriteAdmission;
17use crate::rt::time::Instant;
18
19pub(crate) const GROUP_ACTOR_MAX_WRITE_BATCH: usize = 64;
20pub(crate) const COLD_FLUSH_GROUP_BATCH_MAX_CHUNKS: usize = 4096;
21
22#[derive(Debug, Clone)]
23pub struct RuntimeMetrics {
24    pub(crate) inner: Arc<RuntimeMetricsInner>,
25}
26
27impl RuntimeMetrics {
28    /// Records one aggregate Raft-log pressure snapshot pass.
29    pub fn record_raft_snapshot_pressure(&self, groups: u64) {
30        self.inner.record_raft_snapshot_pressure(groups);
31    }
32}
33
34/// Declares every runtime metric once and expands the four sections that were
35/// previously hand-replicated per metric: the `RuntimeMetricsInner` counter
36/// fields, `RuntimeMetricsInner::new`, the `RuntimeMetrics::snapshot`
37/// collection logic, and the public serialized `RuntimeMetricsSnapshot`
38/// struct.
39///
40/// Manifest grammar (entries must be listed in serialized snapshot-field
41/// order; every counter and snapshot field name is spelled explicitly so
42/// serialized names stay grep-able and byte-stable):
43///
44/// - `sum GLOBAL: core PER_CORE, group PER_GROUP;` — per-core and per-group
45///   counters; the global value is the sum across cores.
46/// - `sum GLOBAL: core PER_CORE;` — per-core counters summed into the global.
47/// - `sum GLOBAL: group PER_GROUP;` — per-group counters summed into the
48///   global.
49/// - `max GLOBAL: group PER_GROUP;` — per-group values; the global is the
50///   maximum.
51/// - `summax SUM, MAX: group PER_GROUP;` — per-group values exposed both as a
52///   sum and as a maximum.
53/// - `counter GLOBAL;` — a single global counter.
54macro_rules! runtime_metrics {
55    (@munch
56        ctx { $ir:ident $cc:ident $gc:ident }
57        inner { $($inner:tt)* }
58        new { $($new:tt)* }
59        snap { $($snap:tt)* }
60        fields { $($fields:tt)* }
61        names { $($names:ident)* }
62        rest { sum $global:ident: core $core:ident, group $group:ident; $($rest:tt)* }
63    ) => {
64        runtime_metrics! {
65            @munch
66            ctx { $ir $cc $gc }
67            inner {
68                $($inner)*
69                pub(crate) $core: Vec<PaddedAtomicU64>,
70                pub(crate) $group: Vec<PaddedAtomicU64>,
71            }
72            new {
73                $($new)*
74                $core: zeroed_counters($cc),
75                $group: zeroed_counters($gc),
76            }
77            snap {
78                $($snap)*
79                let $core = load_counters(&$ir.$core);
80                let $global: u64 = $core.iter().sum();
81                let $group = load_counters(&$ir.$group);
82            }
83            fields {
84                $($fields)*
85                pub $global: u64,
86                pub $core: Vec<u64>,
87                pub $group: Vec<u64>,
88            }
89            names { $($names)* $global $core $group }
90            rest { $($rest)* }
91        }
92    };
93    (@munch
94        ctx { $ir:ident $cc:ident $gc:ident }
95        inner { $($inner:tt)* }
96        new { $($new:tt)* }
97        snap { $($snap:tt)* }
98        fields { $($fields:tt)* }
99        names { $($names:ident)* }
100        rest { sum $global:ident: core $core:ident; $($rest:tt)* }
101    ) => {
102        runtime_metrics! {
103            @munch
104            ctx { $ir $cc $gc }
105            inner {
106                $($inner)*
107                pub(crate) $core: Vec<PaddedAtomicU64>,
108            }
109            new {
110                $($new)*
111                $core: zeroed_counters($cc),
112            }
113            snap {
114                $($snap)*
115                let $core = load_counters(&$ir.$core);
116                let $global: u64 = $core.iter().sum();
117            }
118            fields {
119                $($fields)*
120                pub $global: u64,
121                pub $core: Vec<u64>,
122            }
123            names { $($names)* $global $core }
124            rest { $($rest)* }
125        }
126    };
127    (@munch
128        ctx { $ir:ident $cc:ident $gc:ident }
129        inner { $($inner:tt)* }
130        new { $($new:tt)* }
131        snap { $($snap:tt)* }
132        fields { $($fields:tt)* }
133        names { $($names:ident)* }
134        rest { sum $global:ident: group $group:ident; $($rest:tt)* }
135    ) => {
136        runtime_metrics! {
137            @munch
138            ctx { $ir $cc $gc }
139            inner {
140                $($inner)*
141                pub(crate) $group: Vec<PaddedAtomicU64>,
142            }
143            new {
144                $($new)*
145                $group: zeroed_counters($gc),
146            }
147            snap {
148                $($snap)*
149                let $group = load_counters(&$ir.$group);
150                let $global: u64 = $group.iter().sum();
151            }
152            fields {
153                $($fields)*
154                pub $global: u64,
155                pub $group: Vec<u64>,
156            }
157            names { $($names)* $global $group }
158            rest { $($rest)* }
159        }
160    };
161    (@munch
162        ctx { $ir:ident $cc:ident $gc:ident }
163        inner { $($inner:tt)* }
164        new { $($new:tt)* }
165        snap { $($snap:tt)* }
166        fields { $($fields:tt)* }
167        names { $($names:ident)* }
168        rest { max $global:ident: group $group:ident; $($rest:tt)* }
169    ) => {
170        runtime_metrics! {
171            @munch
172            ctx { $ir $cc $gc }
173            inner {
174                $($inner)*
175                pub(crate) $group: Vec<PaddedAtomicU64>,
176            }
177            new {
178                $($new)*
179                $group: zeroed_counters($gc),
180            }
181            snap {
182                $($snap)*
183                let $group = load_counters(&$ir.$group);
184                let $global = max_or_zero(&$group);
185            }
186            fields {
187                $($fields)*
188                pub $global: u64,
189                pub $group: Vec<u64>,
190            }
191            names { $($names)* $global $group }
192            rest { $($rest)* }
193        }
194    };
195    (@munch
196        ctx { $ir:ident $cc:ident $gc:ident }
197        inner { $($inner:tt)* }
198        new { $($new:tt)* }
199        snap { $($snap:tt)* }
200        fields { $($fields:tt)* }
201        names { $($names:ident)* }
202        rest { summax $sum:ident, $max:ident: group $group:ident; $($rest:tt)* }
203    ) => {
204        runtime_metrics! {
205            @munch
206            ctx { $ir $cc $gc }
207            inner {
208                $($inner)*
209                pub(crate) $group: Vec<PaddedAtomicU64>,
210            }
211            new {
212                $($new)*
213                $group: zeroed_counters($gc),
214            }
215            snap {
216                $($snap)*
217                let $group = load_counters(&$ir.$group);
218                let $sum: u64 = $group.iter().sum();
219                let $max = max_or_zero(&$group);
220            }
221            fields {
222                $($fields)*
223                pub $sum: u64,
224                pub $max: u64,
225                pub $group: Vec<u64>,
226            }
227            names { $($names)* $sum $max $group }
228            rest { $($rest)* }
229        }
230    };
231    (@munch
232        ctx { $ir:ident $cc:ident $gc:ident }
233        inner { $($inner:tt)* }
234        new { $($new:tt)* }
235        snap { $($snap:tt)* }
236        fields { $($fields:tt)* }
237        names { $($names:ident)* }
238        rest { counter $global:ident; $($rest:tt)* }
239    ) => {
240        runtime_metrics! {
241            @munch
242            ctx { $ir $cc $gc }
243            inner {
244                $($inner)*
245                pub(crate) $global: PaddedAtomicU64,
246            }
247            new {
248                $($new)*
249                $global: PaddedAtomicU64::new(0),
250            }
251            snap {
252                $($snap)*
253                let $global = $ir.$global.load_relaxed();
254            }
255            fields {
256                $($fields)*
257                pub $global: u64,
258            }
259            names { $($names)* $global }
260            rest { $($rest)* }
261        }
262    };
263    (@munch
264        ctx { $ir:ident $cc:ident $gc:ident }
265        inner { $($inner:tt)* }
266        new { $($new:tt)* }
267        snap { $($snap:tt)* }
268        fields { $($fields:tt)* }
269        names { $($names:ident)* }
270        rest { }
271    ) => {
272        #[derive(Debug)]
273        pub(crate) struct RuntimeMetricsInner {
274            $($inner)*
275        }
276
277        impl RuntimeMetricsInner {
278            pub(crate) fn new($cc: usize, $gc: usize) -> Self {
279                Self { $($new)* }
280            }
281        }
282
283        #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
284        pub struct RuntimeMetricsSnapshot {
285            $($fields)*
286        }
287
288        impl RuntimeMetrics {
289            pub fn snapshot(&self) -> RuntimeMetricsSnapshot {
290                let $ir = &self.inner;
291                $($snap)*
292                RuntimeMetricsSnapshot { $($names),* }
293            }
294        }
295    };
296    ( $($manifest:tt)* ) => {
297        runtime_metrics! {
298            @munch
299            ctx { inner_counters core_count raft_group_count }
300            inner {}
301            new {}
302            snap {}
303            fields {}
304            names {}
305            rest { $($manifest)* }
306        }
307    };
308}
309
310fn zeroed_counters(len: usize) -> Vec<PaddedAtomicU64> {
311    (0..len).map(|_| PaddedAtomicU64::new(0)).collect()
312}
313
314fn load_counters(counters: &[PaddedAtomicU64]) -> Vec<u64> {
315    counters.iter().map(PaddedAtomicU64::load_relaxed).collect()
316}
317
318fn max_or_zero(values: &[u64]) -> u64 {
319    values.iter().copied().max().unwrap_or(0)
320}
321
322runtime_metrics! {
323    sum accepted_appends: core per_core_appends, group per_group_appends;
324    sum applied_mutations:
325        core per_core_applied_mutations, group per_group_applied_mutations;
326    sum mutation_apply_ns:
327        core per_core_mutation_apply_ns, group per_group_mutation_apply_ns;
328    sum append_post_commit_ns:
329        core per_core_append_post_commit_ns, group per_group_append_post_commit_ns;
330    sum read_watcher_notify_calls:
331        core per_core_read_watcher_notify_calls, group per_group_read_watcher_notify_calls;
332    sum read_watcher_notify_ns:
333        core per_core_read_watcher_notify_ns, group per_group_read_watcher_notify_ns;
334    sum read_watcher_replans:
335        core per_core_read_watcher_replans, group per_group_read_watcher_replans;
336    sum group_lock_wait_ns:
337        core per_core_group_lock_wait_ns, group per_group_group_lock_wait_ns;
338    sum group_engine_exec_ns:
339        core per_core_group_engine_exec_ns, group per_group_group_engine_exec_ns;
340    sum group_mailbox_depth: group per_group_group_mailbox_depth;
341    max group_mailbox_max_depth: group per_group_group_mailbox_max_depth;
342    sum group_mailbox_full_events: group per_group_group_mailbox_full_events;
343    sum raft_write_many_batches:
344        core per_core_raft_write_many_batches, group per_group_raft_write_many_batches;
345    sum raft_write_many_commands:
346        core per_core_raft_write_many_commands, group per_group_raft_write_many_commands;
347    sum raft_write_many_logical_commands:
348        core per_core_raft_write_many_logical_commands,
349        group per_group_raft_write_many_logical_commands;
350    sum raft_write_many_responses:
351        core per_core_raft_write_many_responses, group per_group_raft_write_many_responses;
352    sum raft_write_many_submit_ns:
353        core per_core_raft_write_many_submit_ns, group per_group_raft_write_many_submit_ns;
354    sum raft_write_many_response_ns:
355        core per_core_raft_write_many_response_ns, group per_group_raft_write_many_response_ns;
356    sum raft_apply_entries: core per_core_raft_apply_entries, group per_group_raft_apply_entries;
357    sum raft_apply_ns: core per_core_raft_apply_ns, group per_group_raft_apply_ns;
358    sum raft_snapshot_builds: group per_group_raft_snapshot_builds;
359    sum raft_snapshot_build_ns: group per_group_raft_snapshot_build_ns;
360    summax raft_snapshot_body_bytes, raft_snapshot_body_bytes_max:
361        group per_group_raft_snapshot_body_bytes;
362    summax raft_snapshot_pointer_bytes, raft_snapshot_pointer_bytes_max:
363        group per_group_raft_snapshot_pointer_bytes;
364    summax raft_snapshot_streams, raft_snapshot_streams_max:
365        group per_group_raft_snapshot_streams;
366    sum raft_snapshot_external_uploads: group per_group_raft_snapshot_external_uploads;
367    sum raft_snapshot_inline_fallbacks: group per_group_raft_snapshot_inline_fallbacks;
368    sum live_read_waiters: core per_core_live_read_waiters;
369    sum live_read_backpressure_events: core per_core_live_read_backpressure_events;
370    sum routed_requests: core per_core_routed_requests;
371    sum mailbox_send_wait_ns: core per_core_mailbox_send_wait_ns;
372    sum mailbox_full_events: core per_core_mailbox_full_events;
373    sum wal_batches: core per_core_wal_batches, group per_group_wal_batches;
374    sum wal_records: core per_core_wal_records, group per_group_wal_records;
375    sum wal_write_ns: core per_core_wal_write_ns, group per_group_wal_write_ns;
376    sum wal_sync_ns: core per_core_wal_sync_ns, group per_group_wal_sync_ns;
377    counter cold_flush_uploads;
378    counter cold_flush_upload_bytes;
379    counter cold_flush_upload_ns;
380    counter cold_pack_uploads;
381    counter cold_pack_bytes;
382    counter cold_pack_slices;
383    counter cold_flush_publishes;
384    counter cold_flush_publish_bytes;
385    counter cold_flush_publish_ns;
386    counter cold_orphan_cleanup_attempts;
387    counter cold_orphan_cleanup_errors;
388    counter cold_orphan_bytes;
389    counter cold_gc_reclaimed;
390    counter cold_gc_errors;
391    counter cold_flush_write_errors;
392    counter cold_pressure_flush_passes;
393    counter cold_pressure_flush_candidates;
394    counter raft_snapshot_pressure_passes;
395    counter raft_snapshot_pressure_groups;
396    sum cold_hot_bytes: group per_group_cold_hot_bytes;
397    // Current largest per-group backlog. Cold-health consumes this gauge and
398    // must be able to recover after a flush. The separate per-group `*_max`
399    // series remains the lifetime high-water mark for diagnostics.
400    max cold_hot_group_bytes_max: group per_group_cold_hot_bytes_current_max;
401    max cold_hot_group_bytes_high_watermark: group per_group_cold_hot_bytes_max;
402    counter cold_hot_stream_bytes_max;
403    sum cold_backpressure_events:
404        core per_core_cold_backpressure_events, group per_group_cold_backpressure_events;
405    counter cold_backpressure_bytes;
406}
407
408#[derive(Debug, Clone, PartialEq, Eq)]
409pub struct RuntimeMailboxSnapshot {
410    pub depths: Vec<usize>,
411    pub capacities: Vec<usize>,
412}
413
414#[derive(Debug, Clone, Copy)]
415pub(crate) struct RaftWriteManySample {
416    pub(crate) command_count: u64,
417    pub(crate) logical_command_count: u64,
418    pub(crate) response_count: u64,
419    pub(crate) submit_ns: u64,
420    pub(crate) response_ns: u64,
421}
422
423#[derive(Debug, Clone, Copy)]
424pub(crate) struct RaftSnapshotBuildSample {
425    pub(crate) streams: u64,
426    pub(crate) body_bytes: u64,
427    pub(crate) pointer_bytes: u64,
428    pub(crate) build_ns: u64,
429    pub(crate) external_upload: bool,
430    pub(crate) inline_fallback: bool,
431}
432
433impl RuntimeMetricsInner {
434    pub(crate) fn record_routed_request(&self, core_id: CoreId, mailbox_send_wait_ns: u64) {
435        let index = usize::from(core_id.0);
436        self.per_core_routed_requests[index].fetch_add_relaxed(1);
437        self.per_core_mailbox_send_wait_ns[index].fetch_add_relaxed(mailbox_send_wait_ns);
438    }
439
440    pub(crate) fn record_mailbox_full(&self, core_id: CoreId) {
441        self.per_core_mailbox_full_events[usize::from(core_id.0)].fetch_add_relaxed(1);
442    }
443
444    pub(crate) fn cold_hot_bytes(&self) -> u64 {
445        self.per_group_cold_hot_bytes
446            .iter()
447            .map(PaddedAtomicU64::load_relaxed)
448            .sum()
449    }
450
451    pub(crate) fn record_cold_pressure_flush(&self, candidates: usize) {
452        self.cold_pressure_flush_passes.fetch_add_relaxed(1);
453        self.cold_pressure_flush_candidates
454            .fetch_add_relaxed(u64::try_from(candidates).unwrap_or(u64::MAX));
455    }
456
457    pub(crate) fn record_append(&self, core_id: CoreId, group_id: RaftGroupId) {
458        self.record_append_batch(core_id, group_id, 1);
459    }
460
461    pub(crate) fn record_append_batch(&self, core_id: CoreId, group_id: RaftGroupId, count: u64) {
462        self.per_core_appends[usize::from(core_id.0)].fetch_add_relaxed(count);
463        self.per_group_appends[usize::try_from(group_id.0).expect("u32 fits usize")]
464            .fetch_add_relaxed(count);
465    }
466
467    pub(crate) fn record_applied_mutation(
468        &self,
469        core_id: CoreId,
470        group_id: RaftGroupId,
471        apply_ns: u64,
472    ) {
473        self.record_applied_mutation_batch(core_id, group_id, 1, apply_ns);
474    }
475
476    pub(crate) fn record_applied_mutation_batch(
477        &self,
478        core_id: CoreId,
479        group_id: RaftGroupId,
480        count: u64,
481        apply_ns: u64,
482    ) {
483        let core_index = usize::from(core_id.0);
484        let group_index = usize::try_from(group_id.0).expect("u32 fits usize");
485        self.per_core_applied_mutations[core_index].fetch_add_relaxed(count);
486        self.per_group_applied_mutations[group_index].fetch_add_relaxed(count);
487        self.per_core_mutation_apply_ns[core_index].fetch_add_relaxed(apply_ns);
488        self.per_group_mutation_apply_ns[group_index].fetch_add_relaxed(apply_ns);
489    }
490
491    pub(crate) fn record_group_engine_exec(
492        &self,
493        core_id: CoreId,
494        group_id: RaftGroupId,
495        exec_ns: u64,
496    ) {
497        let core_index = usize::from(core_id.0);
498        let group_index = usize::try_from(group_id.0).expect("u32 fits usize");
499        self.per_core_group_engine_exec_ns[core_index].fetch_add_relaxed(exec_ns);
500        self.per_group_group_engine_exec_ns[group_index].fetch_add_relaxed(exec_ns);
501    }
502
503    pub(crate) fn record_append_post_commit(
504        &self,
505        core_id: CoreId,
506        group_id: RaftGroupId,
507        elapsed_ns: u64,
508    ) {
509        let core_index = usize::from(core_id.0);
510        let group_index = usize::try_from(group_id.0).expect("u32 fits usize");
511        self.per_core_append_post_commit_ns[core_index].fetch_add_relaxed(elapsed_ns);
512        self.per_group_append_post_commit_ns[group_index].fetch_add_relaxed(elapsed_ns);
513    }
514
515    pub(crate) fn record_read_watcher_notify(
516        &self,
517        core_id: CoreId,
518        group_id: RaftGroupId,
519        replans: usize,
520        elapsed_ns: u64,
521    ) {
522        let core_index = usize::from(core_id.0);
523        let group_index = usize::try_from(group_id.0).expect("u32 fits usize");
524        let replans = u64::try_from(replans).unwrap_or(u64::MAX);
525        self.per_core_read_watcher_notify_calls[core_index].fetch_add_relaxed(1);
526        self.per_group_read_watcher_notify_calls[group_index].fetch_add_relaxed(1);
527        self.per_core_read_watcher_notify_ns[core_index].fetch_add_relaxed(elapsed_ns);
528        self.per_group_read_watcher_notify_ns[group_index].fetch_add_relaxed(elapsed_ns);
529        self.per_core_read_watcher_replans[core_index].fetch_add_relaxed(replans);
530        self.per_group_read_watcher_replans[group_index].fetch_add_relaxed(replans);
531    }
532
533    pub(crate) fn record_group_mailbox_enqueued(&self, group_id: RaftGroupId) {
534        let group_index = usize::try_from(group_id.0).expect("u32 fits usize");
535        let depth = self.per_group_group_mailbox_depth[group_index]
536            .fetch_add_relaxed(1)
537            .saturating_add(1);
538        self.per_group_group_mailbox_max_depth[group_index].fetch_max_relaxed(depth);
539    }
540
541    pub(crate) fn record_group_mailbox_dequeued(&self, group_id: RaftGroupId) {
542        let group_index = usize::try_from(group_id.0).expect("u32 fits usize");
543        self.per_group_group_mailbox_depth[group_index].fetch_sub_saturating_relaxed(1);
544    }
545
546    pub(crate) fn record_group_mailbox_full(&self, group_id: RaftGroupId) {
547        let group_index = usize::try_from(group_id.0).expect("u32 fits usize");
548        self.per_group_group_mailbox_full_events[group_index].fetch_add_relaxed(1);
549    }
550
551    pub(crate) fn record_raft_write_many(
552        &self,
553        core_id: CoreId,
554        group_id: RaftGroupId,
555        sample: RaftWriteManySample,
556    ) {
557        let core_index = usize::from(core_id.0);
558        let group_index = usize::try_from(group_id.0).expect("u32 fits usize");
559        self.per_core_raft_write_many_batches[core_index].fetch_add_relaxed(1);
560        self.per_group_raft_write_many_batches[group_index].fetch_add_relaxed(1);
561        self.per_core_raft_write_many_commands[core_index].fetch_add_relaxed(sample.command_count);
562        self.per_group_raft_write_many_commands[group_index]
563            .fetch_add_relaxed(sample.command_count);
564        self.per_core_raft_write_many_logical_commands[core_index]
565            .fetch_add_relaxed(sample.logical_command_count);
566        self.per_group_raft_write_many_logical_commands[group_index]
567            .fetch_add_relaxed(sample.logical_command_count);
568        self.per_core_raft_write_many_responses[core_index]
569            .fetch_add_relaxed(sample.response_count);
570        self.per_group_raft_write_many_responses[group_index]
571            .fetch_add_relaxed(sample.response_count);
572        self.per_core_raft_write_many_submit_ns[core_index].fetch_add_relaxed(sample.submit_ns);
573        self.per_group_raft_write_many_submit_ns[group_index].fetch_add_relaxed(sample.submit_ns);
574        self.per_core_raft_write_many_response_ns[core_index].fetch_add_relaxed(sample.response_ns);
575        self.per_group_raft_write_many_response_ns[group_index]
576            .fetch_add_relaxed(sample.response_ns);
577    }
578
579    pub(crate) fn record_raft_apply_batch(
580        &self,
581        core_id: CoreId,
582        group_id: RaftGroupId,
583        entry_count: u64,
584        apply_ns: u64,
585    ) {
586        let core_index = usize::from(core_id.0);
587        let group_index = usize::try_from(group_id.0).expect("u32 fits usize");
588        self.per_core_raft_apply_entries[core_index].fetch_add_relaxed(entry_count);
589        self.per_group_raft_apply_entries[group_index].fetch_add_relaxed(entry_count);
590        self.per_core_raft_apply_ns[core_index].fetch_add_relaxed(apply_ns);
591        self.per_group_raft_apply_ns[group_index].fetch_add_relaxed(apply_ns);
592    }
593
594    pub(crate) fn record_raft_snapshot_build(
595        &self,
596        group_id: RaftGroupId,
597        sample: RaftSnapshotBuildSample,
598    ) {
599        let group_index = usize::try_from(group_id.0).expect("u32 fits usize");
600        self.per_group_raft_snapshot_builds[group_index].fetch_add_relaxed(1);
601        self.per_group_raft_snapshot_build_ns[group_index].fetch_add_relaxed(sample.build_ns);
602        self.per_group_raft_snapshot_body_bytes[group_index].store_relaxed(sample.body_bytes);
603        self.per_group_raft_snapshot_pointer_bytes[group_index].store_relaxed(sample.pointer_bytes);
604        self.per_group_raft_snapshot_streams[group_index].store_relaxed(sample.streams);
605        if sample.external_upload {
606            self.per_group_raft_snapshot_external_uploads[group_index].fetch_add_relaxed(1);
607        }
608        if sample.inline_fallback {
609            self.per_group_raft_snapshot_inline_fallbacks[group_index].fetch_add_relaxed(1);
610        }
611    }
612
613    pub(crate) fn record_wal_batch(
614        &self,
615        core_id: CoreId,
616        group_id: RaftGroupId,
617        record_count: u64,
618        write_ns: u64,
619        sync_ns: u64,
620    ) {
621        let core_index = usize::from(core_id.0);
622        let group_index = usize::try_from(group_id.0).expect("u32 fits usize");
623        self.per_core_wal_batches[core_index].fetch_add_relaxed(1);
624        self.per_group_wal_batches[group_index].fetch_add_relaxed(1);
625        self.per_core_wal_records[core_index].fetch_add_relaxed(record_count);
626        self.per_group_wal_records[group_index].fetch_add_relaxed(record_count);
627        self.per_core_wal_write_ns[core_index].fetch_add_relaxed(write_ns);
628        self.per_group_wal_write_ns[group_index].fetch_add_relaxed(write_ns);
629        self.per_core_wal_sync_ns[core_index].fetch_add_relaxed(sync_ns);
630        self.per_group_wal_sync_ns[group_index].fetch_add_relaxed(sync_ns);
631    }
632
633    pub(crate) fn record_cold_upload(&self, bytes: u64, upload_ns: u64) {
634        self.cold_flush_uploads.fetch_add_relaxed(1);
635        self.cold_flush_upload_bytes.fetch_add_relaxed(bytes);
636        self.cold_flush_upload_ns.fetch_add_relaxed(upload_ns);
637    }
638
639    fn record_raft_snapshot_pressure(&self, groups: u64) {
640        self.raft_snapshot_pressure_passes.fetch_add_relaxed(1);
641        self.raft_snapshot_pressure_groups.fetch_add_relaxed(groups);
642    }
643
644    pub(crate) fn record_cold_pack(&self, bytes: u64, slices: u64) {
645        self.cold_pack_uploads.fetch_add_relaxed(1);
646        self.cold_pack_bytes.fetch_add_relaxed(bytes);
647        self.cold_pack_slices.fetch_add_relaxed(slices);
648    }
649
650    pub(crate) fn record_cold_publish(&self, bytes: u64, publish_ns: u64) {
651        self.cold_flush_publishes.fetch_add_relaxed(1);
652        self.cold_flush_publish_bytes.fetch_add_relaxed(bytes);
653        self.cold_flush_publish_ns.fetch_add_relaxed(publish_ns);
654    }
655
656    pub(crate) fn record_cold_gc_reclaimed(&self, entries: u64) {
657        self.cold_gc_reclaimed.fetch_add_relaxed(entries);
658    }
659
660    pub(crate) fn record_cold_flush_write_error(&self) {
661        self.cold_flush_write_errors.fetch_add_relaxed(1);
662    }
663
664    pub(crate) fn record_cold_gc_error(&self) {
665        self.cold_gc_errors.fetch_add_relaxed(1);
666    }
667
668    pub(crate) fn record_cold_hot_backlog(
669        &self,
670        group_id: RaftGroupId,
671        stream_hot_bytes: u64,
672        group_hot_bytes: u64,
673    ) {
674        let group_index = usize::try_from(group_id.0).expect("u32 fits usize");
675        self.per_group_cold_hot_bytes[group_index].store_relaxed(group_hot_bytes);
676        self.per_group_cold_hot_bytes_current_max[group_index].store_relaxed(group_hot_bytes);
677        self.per_group_cold_hot_bytes_max[group_index].fetch_max_relaxed(group_hot_bytes);
678        self.cold_hot_stream_bytes_max
679            .fetch_max_relaxed(stream_hot_bytes);
680    }
681
682    pub(crate) fn record_cold_backpressure(
683        &self,
684        core_id: CoreId,
685        group_id: RaftGroupId,
686        incoming_bytes: u64,
687        _limit: u64,
688    ) {
689        let core_index = usize::from(core_id.0);
690        let group_index = usize::try_from(group_id.0).expect("u32 fits usize");
691        self.per_core_cold_backpressure_events[core_index].fetch_add_relaxed(1);
692        self.per_group_cold_backpressure_events[group_index].fetch_add_relaxed(1);
693        self.cold_backpressure_bytes
694            .fetch_add_relaxed(incoming_bytes);
695    }
696
697    pub(crate) fn record_read_watcher_added(&self, core_id: CoreId) {
698        self.record_read_watchers_added(core_id, 1);
699    }
700
701    pub(crate) fn record_read_watchers_added(&self, core_id: CoreId, count: usize) {
702        self.per_core_live_read_waiters[usize::from(core_id.0)]
703            .fetch_add_relaxed(u64::try_from(count).expect("watcher count fits u64"));
704    }
705
706    pub(crate) fn record_read_watchers_removed(&self, core_id: CoreId, count: usize) {
707        self.per_core_live_read_waiters[usize::from(core_id.0)]
708            .fetch_sub_relaxed(u64::try_from(count).expect("watcher count fits u64"));
709    }
710
711    pub(crate) fn record_live_read_backpressure(&self, core_id: CoreId) {
712        self.per_core_live_read_backpressure_events[usize::from(core_id.0)].fetch_add_relaxed(1);
713    }
714}
715
716pub(crate) fn elapsed_ns(started_at: Instant) -> u64 {
717    u64::try_from(started_at.elapsed().as_nanos()).unwrap_or(u64::MAX)
718}
719
720pub(crate) fn append_batch_payload_bytes(request: &AppendBatchRequest) -> u64 {
721    request
722        .payloads
723        .iter()
724        .map(|payload| u64::try_from(payload.len()).expect("payload len fits u64"))
725        .sum()
726}
727
728pub(crate) fn record_cold_backpressure_error(
729    metrics: &RuntimeMetricsInner,
730    placement: ShardPlacement,
731    incoming_bytes: u64,
732    admission: ColdWriteAdmission,
733    err: &GroupEngineError,
734) {
735    if !err.is_cold_backpressure() {
736        return;
737    }
738    metrics.record_cold_backpressure(
739        placement.core_id,
740        placement.raft_group_id,
741        incoming_bytes,
742        admission.max_hot_bytes_per_group.unwrap_or(0),
743    );
744}
745
746pub(crate) fn is_stale_cold_flush_candidate_error(err: &RuntimeError) -> bool {
747    match err.stream_error_code() {
748        Some(StreamErrorCode::StreamGone | StreamErrorCode::StreamNotFound) => true,
749        Some(StreamErrorCode::InvalidColdFlush) => err
750            .stream_error_context()
751            .iter()
752            .any(|context| matches!(context, StreamErrorContext::StaleColdFlushCandidate)),
753        _ => false,
754    }
755}
756
757pub(crate) async fn record_cold_hot_backlog(
758    group: &mut Box<dyn GroupEngine>,
759    metrics: &RuntimeMetricsInner,
760    stream_id: BucketStreamId,
761    placement: ShardPlacement,
762) {
763    if let Ok(backlog) = group.cold_hot_backlog(stream_id, placement).await {
764        metrics.record_cold_hot_backlog(
765            placement.raft_group_id,
766            backlog.stream_hot_bytes,
767            backlog.group_hot_bytes,
768        );
769    }
770}
771
772#[derive(Debug)]
773#[repr(align(128))]
774pub(crate) struct PaddedAtomicU64 {
775    value: AtomicU64,
776}
777
778impl PaddedAtomicU64 {
779    pub(crate) fn new(value: u64) -> Self {
780        Self {
781            value: AtomicU64::new(value),
782        }
783    }
784
785    pub(crate) fn load_relaxed(&self) -> u64 {
786        self.value.load(Ordering::Relaxed)
787    }
788
789    pub(crate) fn fetch_add_relaxed(&self, value: u64) -> u64 {
790        self.value.fetch_add(value, Ordering::Relaxed)
791    }
792
793    pub(crate) fn fetch_sub_relaxed(&self, value: u64) {
794        self.value.fetch_sub(value, Ordering::Relaxed);
795    }
796
797    pub(crate) fn fetch_sub_saturating_relaxed(&self, value: u64) {
798        let mut current = self.value.load(Ordering::Relaxed);
799        loop {
800            let next = current.saturating_sub(value);
801            match self.value.compare_exchange_weak(
802                current,
803                next,
804                Ordering::Relaxed,
805                Ordering::Relaxed,
806            ) {
807                Ok(_) => return,
808                Err(observed) => current = observed,
809            }
810        }
811    }
812
813    pub(crate) fn fetch_max_relaxed(&self, value: u64) {
814        self.value.fetch_max(value, Ordering::Relaxed);
815    }
816
817    pub(crate) fn store_relaxed(&self, value: u64) {
818        self.value.store(value, Ordering::Relaxed);
819    }
820}
821
822#[cfg(test)]
823mod metric_manifest_tests {
824    use std::sync::Arc;
825
826    use crate::metrics::RuntimeMetrics;
827    use crate::metrics::RuntimeMetricsInner;
828
829    /// The serialized field names of [`RuntimeMetricsSnapshot`] in declaration
830    /// order, captured from the pre-macro hand-written struct. Metrics
831    /// endpoints and `ursulactl` depend on these names staying byte-identical.
832    const EXPECTED_SNAPSHOT_KEYS: [&str; 126] = [
833        "accepted_appends",
834        "per_core_appends",
835        "per_group_appends",
836        "applied_mutations",
837        "per_core_applied_mutations",
838        "per_group_applied_mutations",
839        "mutation_apply_ns",
840        "per_core_mutation_apply_ns",
841        "per_group_mutation_apply_ns",
842        "append_post_commit_ns",
843        "per_core_append_post_commit_ns",
844        "per_group_append_post_commit_ns",
845        "read_watcher_notify_calls",
846        "per_core_read_watcher_notify_calls",
847        "per_group_read_watcher_notify_calls",
848        "read_watcher_notify_ns",
849        "per_core_read_watcher_notify_ns",
850        "per_group_read_watcher_notify_ns",
851        "read_watcher_replans",
852        "per_core_read_watcher_replans",
853        "per_group_read_watcher_replans",
854        "group_lock_wait_ns",
855        "per_core_group_lock_wait_ns",
856        "per_group_group_lock_wait_ns",
857        "group_engine_exec_ns",
858        "per_core_group_engine_exec_ns",
859        "per_group_group_engine_exec_ns",
860        "group_mailbox_depth",
861        "per_group_group_mailbox_depth",
862        "group_mailbox_max_depth",
863        "per_group_group_mailbox_max_depth",
864        "group_mailbox_full_events",
865        "per_group_group_mailbox_full_events",
866        "raft_write_many_batches",
867        "per_core_raft_write_many_batches",
868        "per_group_raft_write_many_batches",
869        "raft_write_many_commands",
870        "per_core_raft_write_many_commands",
871        "per_group_raft_write_many_commands",
872        "raft_write_many_logical_commands",
873        "per_core_raft_write_many_logical_commands",
874        "per_group_raft_write_many_logical_commands",
875        "raft_write_many_responses",
876        "per_core_raft_write_many_responses",
877        "per_group_raft_write_many_responses",
878        "raft_write_many_submit_ns",
879        "per_core_raft_write_many_submit_ns",
880        "per_group_raft_write_many_submit_ns",
881        "raft_write_many_response_ns",
882        "per_core_raft_write_many_response_ns",
883        "per_group_raft_write_many_response_ns",
884        "raft_apply_entries",
885        "per_core_raft_apply_entries",
886        "per_group_raft_apply_entries",
887        "raft_apply_ns",
888        "per_core_raft_apply_ns",
889        "per_group_raft_apply_ns",
890        "raft_snapshot_builds",
891        "per_group_raft_snapshot_builds",
892        "raft_snapshot_build_ns",
893        "per_group_raft_snapshot_build_ns",
894        "raft_snapshot_body_bytes",
895        "raft_snapshot_body_bytes_max",
896        "per_group_raft_snapshot_body_bytes",
897        "raft_snapshot_pointer_bytes",
898        "raft_snapshot_pointer_bytes_max",
899        "per_group_raft_snapshot_pointer_bytes",
900        "raft_snapshot_streams",
901        "raft_snapshot_streams_max",
902        "per_group_raft_snapshot_streams",
903        "raft_snapshot_external_uploads",
904        "per_group_raft_snapshot_external_uploads",
905        "raft_snapshot_inline_fallbacks",
906        "per_group_raft_snapshot_inline_fallbacks",
907        "live_read_waiters",
908        "per_core_live_read_waiters",
909        "live_read_backpressure_events",
910        "per_core_live_read_backpressure_events",
911        "routed_requests",
912        "per_core_routed_requests",
913        "mailbox_send_wait_ns",
914        "per_core_mailbox_send_wait_ns",
915        "mailbox_full_events",
916        "per_core_mailbox_full_events",
917        "wal_batches",
918        "per_core_wal_batches",
919        "per_group_wal_batches",
920        "wal_records",
921        "per_core_wal_records",
922        "per_group_wal_records",
923        "wal_write_ns",
924        "per_core_wal_write_ns",
925        "per_group_wal_write_ns",
926        "wal_sync_ns",
927        "per_core_wal_sync_ns",
928        "per_group_wal_sync_ns",
929        "cold_flush_uploads",
930        "cold_flush_upload_bytes",
931        "cold_flush_upload_ns",
932        "cold_pack_uploads",
933        "cold_pack_bytes",
934        "cold_pack_slices",
935        "cold_flush_publishes",
936        "cold_flush_publish_bytes",
937        "cold_flush_publish_ns",
938        "cold_orphan_cleanup_attempts",
939        "cold_orphan_cleanup_errors",
940        "cold_orphan_bytes",
941        "cold_gc_reclaimed",
942        "cold_gc_errors",
943        "cold_flush_write_errors",
944        "cold_pressure_flush_passes",
945        "cold_pressure_flush_candidates",
946        "raft_snapshot_pressure_passes",
947        "raft_snapshot_pressure_groups",
948        "cold_hot_bytes",
949        "per_group_cold_hot_bytes",
950        "cold_hot_group_bytes_max",
951        "per_group_cold_hot_bytes_current_max",
952        "cold_hot_group_bytes_high_watermark",
953        "per_group_cold_hot_bytes_max",
954        "cold_hot_stream_bytes_max",
955        "cold_backpressure_events",
956        "per_core_cold_backpressure_events",
957        "per_group_cold_backpressure_events",
958        "cold_backpressure_bytes",
959    ];
960
961    fn metrics_for_test() -> RuntimeMetrics {
962        RuntimeMetrics {
963            inner: Arc::new(RuntimeMetricsInner::new(2, 3)),
964        }
965    }
966
967    #[test]
968    fn snapshot_serializes_expected_field_names_in_order() {
969        let json =
970            serde_json::to_string(&metrics_for_test().snapshot()).expect("snapshot serializes");
971        // Every value is a number or an array of numbers, so each `":`
972        // occurrence in the output belongs to exactly one field key.
973        assert_eq!(
974            json.matches("\":").count(),
975            EXPECTED_SNAPSHOT_KEYS.len(),
976            "unexpected number of serialized fields: {json}"
977        );
978        let mut last_position = None;
979        for name in EXPECTED_SNAPSHOT_KEYS {
980            let needle = format!("\"{name}\":");
981            let position = json
982                .find(&needle)
983                .unwrap_or_else(|| panic!("missing serialized key {name}"));
984            assert!(
985                last_position < Some(position),
986                "serialized key {name} out of declaration order"
987            );
988            last_position = Some(position);
989        }
990    }
991
992    #[test]
993    fn snapshot_vector_lengths_follow_metric_scope() {
994        let snapshot = metrics_for_test().snapshot();
995        assert_eq!(snapshot.per_core_appends.len(), 2);
996        assert_eq!(snapshot.per_group_appends.len(), 3);
997        assert_eq!(snapshot.per_core_routed_requests.len(), 2);
998        assert_eq!(snapshot.per_group_raft_snapshot_streams.len(), 3);
999    }
1000
1001    #[test]
1002    fn snapshot_aggregates_sum_and_max_per_manifest() {
1003        let metrics = metrics_for_test();
1004        metrics.inner.per_core_appends[0].fetch_add_relaxed(3);
1005        metrics.inner.per_core_appends[1].fetch_add_relaxed(4);
1006        metrics.inner.per_group_group_mailbox_max_depth[1].fetch_max_relaxed(9);
1007        metrics.inner.per_group_raft_snapshot_body_bytes[0].store_relaxed(5);
1008        metrics.inner.per_group_raft_snapshot_body_bytes[2].store_relaxed(11);
1009        let snapshot = metrics.snapshot();
1010        assert_eq!(snapshot.accepted_appends, 7);
1011        assert_eq!(snapshot.group_mailbox_max_depth, 9);
1012        assert_eq!(snapshot.raft_snapshot_body_bytes, 16);
1013        assert_eq!(snapshot.raft_snapshot_body_bytes_max, 11);
1014    }
1015}