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