Skip to main content

mz_storage_client/
statistics.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! These structure represents a full set up updates for the `mz_source_statistics_raw`
11//! and `mz_sink_statistics_raw` tables for a specific source-worker/sink-worker pair.
12//! They are structured like this for simplicity
13//! and efficiency: Each storage worker can individually collect and consolidate metrics,
14//! then control how much `StorageResponse` traffic is produced when sending updates
15//! back to the controller to be written.
16//!
17//! The proto conversions for this types are in the `client` module, for now.
18
19use std::sync::LazyLock;
20use std::sync::atomic::{AtomicU64, Ordering};
21use std::time::{Duration, Instant};
22
23use mz_controller_types::ReplicaId;
24use serde::{Deserialize, Serialize};
25
26use mz_repr::{GlobalId, RelationDesc, Row, SqlScalarType};
27
28pub static MZ_SOURCE_STATISTICS_RAW_DESC: LazyLock<RelationDesc> = LazyLock::new(|| {
29    RelationDesc::builder()
30        // Id of the source (or subsource).
31        .with_column("id", SqlScalarType::String.nullable(false))
32        // Id of the replica producing these statistics.
33        .with_column("replica_id", SqlScalarType::String.nullable(true))
34        //
35        // Counters
36        //
37        // A counter of the messages we have read from upstream for this source.
38        // Never resets.
39        .with_column("messages_received", SqlScalarType::UInt64.nullable(false))
40        // A counter of the bytes we have read from upstream for this source.
41        // Never resets.
42        .with_column("bytes_received", SqlScalarType::UInt64.nullable(false))
43        // A counter of the updates we have staged to commit for this source.
44        // Never resets.
45        .with_column("updates_staged", SqlScalarType::UInt64.nullable(false))
46        // A counter of the updates we have committed for this source.
47        // Never resets.
48        .with_column("updates_committed", SqlScalarType::UInt64.nullable(false))
49        //
50        // Resetting gauges
51        //
52        // A gauge of the number of records in the envelope state. 0 for sources
53        // Resetted when the source is restarted, for any reason.
54        .with_column("records_indexed", SqlScalarType::UInt64.nullable(false))
55        // A gauge of the number of bytes in the envelope state. 0 for sources
56        // Resetted when the source is restarted, for any reason.
57        .with_column("bytes_indexed", SqlScalarType::UInt64.nullable(false))
58        // A gauge that shows the duration of rehydration. `NULL` before rehydration
59        // is done.
60        // Resetted when the source is restarted, for any reason.
61        .with_column(
62            "rehydration_latency",
63            SqlScalarType::Interval.nullable(true),
64        )
65        // A gauge of the number of _values_ (source defined unit) the _snapshot_ of this source
66        // contains.
67        // Sometimes resetted when the source can snapshot new pieces of upstream (like Postgres and
68        // MySql).
69        // (like pg and mysql) may repopulate this column when tables are added.
70        //
71        // `NULL` while we discover the snapshot size.
72        .with_column(
73            "snapshot_records_known",
74            SqlScalarType::UInt64.nullable(true),
75        )
76        // A gauge of the number of _values_ (source defined unit) we have read of the _snapshot_
77        // of this source.
78        // Sometimes resetted when the source can snapshot new pieces of upstream (like Postgres and
79        // MySql).
80        //
81        // `NULL` while we discover the snapshot size.
82        .with_column(
83            "snapshot_records_staged",
84            SqlScalarType::UInt64.nullable(true),
85        )
86        //
87        // Non-resetting gauges
88        //
89        // Whether or not the snapshot for the source has been committed. Never resets.
90        .with_column("snapshot_committed", SqlScalarType::Bool.nullable(false))
91        // The following are not yet reported by sources and have 0 or `NULL` values.
92        // They have been added here to reduce churn changing the schema of this collection.
93        //
94        // These are left nullable for now in case we want semantics for `NULL` values. We
95        // currently never expose null values.
96        //
97        // A gauge of the number of _values_ (source defined unit) available to be read from upstream.
98        // Never resets. Not to be confused with any of the counters above.
99        .with_column("offset_known", SqlScalarType::UInt64.nullable(true))
100        // A gauge of the number of _values_ (source defined unit) we have committed.
101        // Never resets. Not to be confused with any of the counters above.
102        .with_column("offset_committed", SqlScalarType::UInt64.nullable(true))
103        .finish()
104});
105
106pub static MZ_SINK_STATISTICS_RAW_DESC: LazyLock<RelationDesc> = LazyLock::new(|| {
107    RelationDesc::builder()
108        // Id of the sink.
109        .with_column("id", SqlScalarType::String.nullable(false))
110        // Id of the replica producing these statistics.
111        .with_column("replica_id", SqlScalarType::String.nullable(true))
112        //
113        // Counters
114        //
115        // A counter of the messages we have staged to upstream.
116        // Never resets.
117        .with_column("messages_staged", SqlScalarType::UInt64.nullable(false))
118        // A counter of the messages we have committed.
119        // Never resets.
120        .with_column("messages_committed", SqlScalarType::UInt64.nullable(false))
121        // A counter of the bytes we have staged to upstream.
122        // Never resets.
123        .with_column("bytes_staged", SqlScalarType::UInt64.nullable(false))
124        // A counter of the bytes we have committed.
125        // Never resets.
126        .with_column("bytes_committed", SqlScalarType::UInt64.nullable(false))
127        .finish()
128});
129
130// Types of statistics (counter and various types of gauges), that have different semantics
131// when sources/sinks are reset.
132
133/// A trait that defines the semantics storage statistics are able to have
134pub trait StorageMetric {
135    /// Summarizes a set of measurements into a single representative value.
136    /// Typically this function is used to summarize the measurements collected from each worker.
137    fn summarize<'a, I>(values: I) -> Self
138    where
139        I: IntoIterator<Item = &'a Self>,
140        Self: Sized + 'a;
141
142    /// Incorporate this value with another.
143    ///
144    /// `id` and `field_name` identify the metric in regression logs.
145    fn incorporate(&mut self, other: Self, id: GlobalId, field_name: &'static str);
146}
147
148/// A counter that never resets.
149#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
150pub struct Counter(u64);
151
152impl StorageMetric for Counter {
153    fn summarize<'a, I>(values: I) -> Self
154    where
155        I: IntoIterator<Item = &'a Self>,
156        Self: Sized + 'a,
157    {
158        // Sum across workers.
159        Self(values.into_iter().map(|c| c.0).sum())
160    }
161
162    fn incorporate(&mut self, other: Self, _id: GlobalId, _field_name: &'static str) {
163        // Always add the new value to the existing one.
164        self.0 += other.0
165    }
166}
167
168impl From<u64> for Counter {
169    fn from(f: u64) -> Self {
170        Counter(f)
171    }
172}
173
174/// A latency gauge that is reset on every restart.
175#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
176pub struct ResettingLatency(Option<i64>);
177
178impl From<Option<i64>> for ResettingLatency {
179    fn from(f: Option<i64>) -> Self {
180        ResettingLatency(f)
181    }
182}
183
184impl StorageMetric for ResettingLatency {
185    fn summarize<'a, I>(values: I) -> Self
186    where
187        I: IntoIterator<Item = &'a Self>,
188        Self: Sized + 'a,
189    {
190        let mut max = 0;
191        for value in values {
192            match value.0 {
193                // If any of the values are `NULL`, then we don't yet know the max.
194                None => return Self(None),
195                // Pick the worst latency across workers.
196                Some(value) => max = std::cmp::max(max, value),
197            }
198        }
199
200        Self(Some(max))
201    }
202
203    fn incorporate(&mut self, other: Self, _id: GlobalId, _field_name: &'static str) {
204        // Reset to the new value.
205        self.0 = other.0;
206    }
207}
208
209impl ResettingLatency {
210    fn reset(&mut self) {
211        self.0 = None;
212    }
213}
214
215/// A numerical gauge that is always resets, but can start out as `NULL`.
216#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
217pub struct ResettingNullableTotal(Option<u64>);
218
219impl StorageMetric for ResettingNullableTotal {
220    fn summarize<'a, I>(values: I) -> Self
221    where
222        I: IntoIterator<Item = &'a Self>,
223        Self: Sized + 'a,
224    {
225        let mut sum = 0;
226        for value in values {
227            match value.0 {
228                // If any of the values are `NULL`, then we merge to `NULL`
229                None => return Self(None),
230                // Pick the worst latency across workers.
231                Some(value) => sum += value,
232            }
233        }
234
235        Self(Some(sum))
236    }
237
238    fn incorporate(&mut self, other: Self, _id: GlobalId, _field_name: &'static str) {
239        match (&mut self.0, other.0) {
240            (None, other) => {
241                self.0 = other;
242            }
243            // Override the total.
244            (Some(this), Some(other)) => *this = other,
245            (Some(_), None) => {
246                // `NULL`'s don't reset the value.
247            }
248        }
249    }
250}
251
252impl From<Option<u64>> for ResettingNullableTotal {
253    fn from(f: Option<u64>) -> Self {
254        ResettingNullableTotal(f)
255    }
256}
257
258impl ResettingNullableTotal {
259    fn reset(&mut self) {
260        self.0 = None;
261    }
262}
263
264/// A numerical gauge that is always resets.
265#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
266pub struct ResettingTotal(u64);
267
268impl StorageMetric for ResettingTotal {
269    fn summarize<'a, I>(values: I) -> Self
270    where
271        I: IntoIterator<Item = &'a Self>,
272        Self: Sized + 'a,
273    {
274        // Sum across workers.
275        Self(values.into_iter().map(|c| c.0).sum())
276    }
277
278    fn incorporate(&mut self, other: Self, _id: GlobalId, _field_name: &'static str) {
279        // Reset the pre-existing value.
280        self.0 = other.0;
281    }
282}
283
284impl From<u64> for ResettingTotal {
285    fn from(f: u64) -> Self {
286        ResettingTotal(f)
287    }
288}
289
290impl ResettingTotal {
291    fn reset(&mut self) {
292        self.0 = 0;
293    }
294}
295
296/// Throttles the `error!` log for gauge regressions.
297///
298/// A regressed gauge keeps regressing on every statistics update, every few
299/// seconds, until the source restarts. Only the first regression in each
300/// interval is logged at `error!`, which reaches Sentry, the rest at `warn!`.
301#[derive(Clone, Debug, Default)]
302struct RegressionLog {
303    last_error: Option<Instant>,
304}
305
306impl RegressionLog {
307    const ERROR_INTERVAL: Duration = Duration::from_secs(15 * 60);
308
309    fn log(&mut self, message: std::fmt::Arguments<'_>) {
310        let now = Instant::now();
311        match self.last_error {
312            Some(last) if now.duration_since(last) < Self::ERROR_INTERVAL => {
313                tracing::warn!("{message}")
314            }
315            _ => {
316                self.last_error = Some(now);
317                tracing::error!("{message}")
318            }
319        }
320    }
321}
322
323// The throttle state is bookkeeping, not part of the gauge's value.
324impl PartialEq for RegressionLog {
325    fn eq(&self, _other: &Self) -> bool {
326        true
327    }
328}
329
330/// A boolean gauge that is never reset.
331#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
332pub struct Boolean(bool, #[serde(skip)] RegressionLog);
333
334impl StorageMetric for Boolean {
335    fn summarize<'a, I>(values: I) -> Self
336    where
337        I: IntoIterator<Item = &'a Self>,
338        Self: Sized + 'a,
339    {
340        // All workers must be true for this gauge to be true.
341        Self(
342            values.into_iter().fold(true, |base, new| base & new.0),
343            RegressionLog::default(),
344        )
345    }
346
347    fn incorporate(&mut self, other: Self, id: GlobalId, field_name: &'static str) {
348        // A boolean regressing to `false` is a bug.
349        //
350        // Clippy's suggestion here is not good!
351        #[allow(clippy::bool_comparison)]
352        if other.0 < self.0 {
353            self.1.log(format_args!(
354                "boolean gauge for field {field_name} of {id} erroneously regressed from true to false",
355            ));
356            return;
357        }
358        self.0 = other.0;
359    }
360}
361
362impl From<bool> for Boolean {
363    fn from(f: bool) -> Self {
364        Boolean(f, RegressionLog::default())
365    }
366}
367
368/// A numerical gauge that never regresses.
369#[derive(Debug, Serialize, Deserialize, Default)]
370pub struct Total {
371    /// Defaults to 0. Can be skipped on updates from clusterd.
372    total: Option<u64>,
373    /// If provided, it is bumped on regressions, as opposed to `error!`
374    /// logs.
375    #[serde(skip)]
376    regressions:
377        Option<mz_ore::metrics::DeleteOnDropCounter<prometheus::core::AtomicU64, Vec<String>>>,
378    #[serde(skip)]
379    regression_log: RegressionLog,
380}
381
382impl From<Option<u64>> for Total {
383    fn from(f: Option<u64>) -> Self {
384        Total {
385            total: f,
386            regressions: None,
387            regression_log: RegressionLog::default(),
388        }
389    }
390}
391
392impl Clone for Total {
393    fn clone(&self) -> Self {
394        Self {
395            total: self.total,
396            regressions: None,
397            regression_log: self.regression_log.clone(),
398        }
399    }
400}
401
402impl PartialEq for Total {
403    fn eq(&self, other: &Self) -> bool {
404        self.total == other.total
405    }
406}
407
408impl Total {
409    /// Pack this `Total` into a `u64`, defaulting to 0.
410    fn pack(&self) -> u64 {
411        self.total.unwrap_or_default()
412    }
413}
414
415impl StorageMetric for Total {
416    fn summarize<'a, I>(values: I) -> Self
417    where
418        I: IntoIterator<Item = &'a Self>,
419        Self: Sized + 'a,
420    {
421        // Sum across workers, if all workers have participated
422        // a non-`None` value.
423        let mut any_none = false;
424
425        let inner = values
426            .into_iter()
427            .filter_map(|i| {
428                any_none |= i.total.is_none();
429                i.total.as_ref()
430            })
431            .sum();
432
433        // If any are none, we can't aggregate.
434        // self.regressions is only meaningful in incorporation.
435        Self {
436            total: (!any_none).then_some(inner),
437            regressions: None,
438            regression_log: RegressionLog::default(),
439        }
440    }
441
442    fn incorporate(&mut self, other: Self, id: GlobalId, field_name: &'static str) {
443        match (&mut self.total, other.total) {
444            (_, None) => {}
445            (None, Some(other)) => self.total = Some(other),
446            (Some(this), Some(other)) => {
447                if other < *this {
448                    if let Some(metric) = &self.regressions {
449                        metric.inc()
450                    } else {
451                        self.regression_log.log(format_args!(
452                            "total gauge {field_name} of {id} erroneously regressed from {} to {}",
453                            this, other
454                        ));
455                    }
456                    return;
457                }
458                *this = other
459            }
460        }
461    }
462}
463
464/// A gauge that has semantics based on the `StorageMetric` implementation of its inner.
465#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
466pub struct Gauge<T>(T);
467
468impl<T> Gauge<T> {
469    // This can't be a `From` impl cause of coherence issues :(
470    pub fn gauge<F>(f: F) -> Self
471    where
472        T: From<F>,
473    {
474        Gauge(f.into())
475    }
476}
477
478impl<T: StorageMetric> StorageMetric for Gauge<T> {
479    fn summarize<'a, I>(values: I) -> Self
480    where
481        I: IntoIterator<Item = &'a Self>,
482        Self: Sized + 'a,
483    {
484        Gauge(T::summarize(values.into_iter().map(|i| &i.0)))
485    }
486
487    fn incorporate(&mut self, other: Self, id: GlobalId, field_name: &'static str) {
488        self.0.incorporate(other.0, id, field_name)
489    }
490}
491
492/// A trait that abstracts over user-facing statistics objects that can be
493/// packed into a `Row` and unpacked again. That is they round-trip.
494pub trait PackableStats {
495    /// Pack `self` into the `Row`.
496    fn pack(&self, packer: mz_repr::RowPacker<'_>);
497    /// Unpack a `Row` back into a `Self`.
498    fn unpack(
499        row: Row,
500        metrics: &crate::metrics::StorageControllerMetrics,
501    ) -> (GlobalId, Option<ReplicaId>, Self);
502}
503
504/// Stats that can get out-of-date and should then get reaped.
505pub trait ExpirableStats {
506    /// The last time this bag of stats was updated (via an update coming in
507    /// from a replica, presumably).
508    fn last_updated(&self) -> Instant;
509}
510
511/// Stats that should be initialized with a row of zeroes, that is we want the
512/// first data to show up in the builtin stats collection to be zeroes.
513///
514/// We do the tracking within the stat object itself instead of adding external
515/// tracking. That's a bit of a roundabout way of doing it, but it works.
516///
517/// This is an odd requirement, and you would think we should just report what
518/// is there when we get it, but this was consciously changed a while back and
519/// the current Console incarnation depends on this behavior.
520pub trait ZeroInitializedStats {
521    /// `True` if we have have not emitted a "zero update" for this stats blob.
522    fn needs_zero_initialization(&self) -> bool;
523
524    /// Marks that we have emitted a "zero update" for this stats blob.
525    fn mark_zero_initialized(&mut self);
526
527    /// Returns a "all zero" instance of this stat, with collection id and/or
528    /// replica id retained.
529    fn zero_stat(&self) -> Self;
530}
531
532/// An update as reported from a storage instance. The semantics
533/// of each field are documented above in `MZ_SOURCE_STATISTICS_RAW_DESC`,
534/// and encoded in the field types.
535#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
536pub struct SourceStatisticsUpdate {
537    pub id: GlobalId,
538
539    pub messages_received: Counter,
540    pub bytes_received: Counter,
541    pub updates_staged: Counter,
542    pub updates_committed: Counter,
543
544    pub records_indexed: Gauge<ResettingTotal>,
545    pub bytes_indexed: Gauge<ResettingTotal>,
546    pub rehydration_latency_ms: Gauge<ResettingLatency>,
547    pub snapshot_records_known: Gauge<ResettingNullableTotal>,
548    pub snapshot_records_staged: Gauge<ResettingNullableTotal>,
549
550    pub snapshot_committed: Gauge<Boolean>,
551    // `offset_known` is enriched with a counter in `unpack` and `with_metrics` that is
552    // bumped whenever it regresses. This is distinct from `offset_committed`, which
553    // `error!` logs.
554    //
555    // `offset_committed` is entirely in our control: it is calculated from source frontiers
556    // that are guaranteed to never go backwards. Therefore, it regresses is a bug in how we
557    // calculate it.
558    //
559    // `offset_known` is calculated based on information the upstream service of the source gives
560    // us. This is meaningfully less reliable, and can cause regressions in the value. Some known
561    // cases that cause this are:
562    // - A Kafka topic being deleted and recreated.
563    // - A Postgres source being restored to a backup.
564    //
565    // We attempt to communicate both of these to the user using the source status system tables.
566    // While emitting a regressed `offset_known` can be at least partially avoided in the source
567    // implementation, we avoid noisy sentry alerts by instead bumping a counter that can be used
568    // if a scenario requires more investigation.
569    pub offset_known: Gauge<Total>,
570    pub offset_committed: Gauge<Total>,
571}
572
573impl SourceStatisticsUpdate {
574    pub fn new(id: GlobalId) -> Self {
575        Self {
576            id,
577            messages_received: Default::default(),
578            bytes_received: Default::default(),
579            updates_staged: Default::default(),
580            updates_committed: Default::default(),
581            records_indexed: Default::default(),
582            bytes_indexed: Default::default(),
583            rehydration_latency_ms: Default::default(),
584            snapshot_records_known: Default::default(),
585            snapshot_records_staged: Default::default(),
586            snapshot_committed: Default::default(),
587            offset_known: Default::default(),
588            offset_committed: Default::default(),
589        }
590    }
591
592    pub fn summarize<'a, I, F>(values: F) -> Self
593    where
594        I: IntoIterator<Item = &'a Self>,
595        F: Fn() -> I,
596        Self: 'a,
597    {
598        SourceStatisticsUpdate {
599            id: values().into_iter().next().unwrap().id,
600            messages_received: Counter::summarize(
601                values().into_iter().map(|s| &s.messages_received),
602            ),
603            bytes_received: Counter::summarize(values().into_iter().map(|s| &s.bytes_received)),
604            updates_staged: Counter::summarize(values().into_iter().map(|s| &s.updates_staged)),
605            updates_committed: Counter::summarize(
606                values().into_iter().map(|s| &s.updates_committed),
607            ),
608            records_indexed: Gauge::summarize(values().into_iter().map(|s| &s.records_indexed)),
609            bytes_indexed: Gauge::summarize(values().into_iter().map(|s| &s.bytes_indexed)),
610            rehydration_latency_ms: Gauge::summarize(
611                values().into_iter().map(|s| &s.rehydration_latency_ms),
612            ),
613            snapshot_records_known: Gauge::summarize(
614                values().into_iter().map(|s| &s.snapshot_records_known),
615            ),
616            snapshot_records_staged: Gauge::summarize(
617                values().into_iter().map(|s| &s.snapshot_records_staged),
618            ),
619            snapshot_committed: Gauge::summarize(
620                values().into_iter().map(|s| &s.snapshot_committed),
621            ),
622            offset_known: Gauge::summarize(values().into_iter().map(|s| &s.offset_known)),
623            offset_committed: Gauge::summarize(values().into_iter().map(|s| &s.offset_committed)),
624        }
625    }
626
627    /// Reset counters so that we continue to ship diffs to the controller.
628    pub fn reset_counters(&mut self) {
629        self.messages_received.0 = 0;
630        self.bytes_received.0 = 0;
631        self.updates_staged.0 = 0;
632        self.updates_committed.0 = 0;
633    }
634
635    /// Reset all _resetable_ gauges to their default values.
636    pub fn reset_gauges(&mut self) {
637        self.records_indexed.0.reset();
638        self.bytes_indexed.0.reset();
639        self.rehydration_latency_ms.0.reset();
640        self.snapshot_records_known.0.reset();
641        self.snapshot_records_staged.0.reset();
642    }
643
644    pub fn incorporate(&mut self, other: SourceStatisticsUpdate) {
645        let SourceStatisticsUpdate {
646            id,
647            messages_received,
648            bytes_received,
649            updates_staged,
650            updates_committed,
651            records_indexed,
652            bytes_indexed,
653            rehydration_latency_ms,
654            snapshot_records_known,
655            snapshot_records_staged,
656            snapshot_committed,
657            offset_known,
658            offset_committed,
659            ..
660        } = self;
661
662        messages_received.incorporate(other.messages_received, *id, "messages_received");
663        bytes_received.incorporate(other.bytes_received, *id, "bytes_received");
664        updates_staged.incorporate(other.updates_staged, *id, "updates_staged");
665        updates_committed.incorporate(other.updates_committed, *id, "updates_committed");
666        records_indexed.incorporate(other.records_indexed, *id, "records_indexed");
667        bytes_indexed.incorporate(other.bytes_indexed, *id, "bytes_indexed");
668        rehydration_latency_ms.incorporate(
669            other.rehydration_latency_ms,
670            *id,
671            "rehydration_latency_ms",
672        );
673        snapshot_records_known.incorporate(
674            other.snapshot_records_known,
675            *id,
676            "snapshot_records_known",
677        );
678        snapshot_records_staged.incorporate(
679            other.snapshot_records_staged,
680            *id,
681            "snapshot_records_staged",
682        );
683        snapshot_committed.incorporate(other.snapshot_committed, *id, "snapshot_committed");
684        offset_known.incorporate(other.offset_known, *id, "offset_known");
685        offset_committed.incorporate(other.offset_committed, *id, "offset_committed");
686    }
687
688    /// Incorporate only the counters of the given update, ignoring gauge values.
689    pub fn incorporate_counters(&mut self, other: SourceStatisticsUpdate) {
690        let SourceStatisticsUpdate {
691            id,
692            messages_received,
693            bytes_received,
694            updates_staged,
695            updates_committed,
696            ..
697        } = self;
698
699        messages_received.incorporate(other.messages_received, *id, "messages_received");
700        bytes_received.incorporate(other.bytes_received, *id, "bytes_received");
701        updates_staged.incorporate(other.updates_staged, *id, "updates_staged");
702        updates_committed.incorporate(other.updates_committed, *id, "updates_committed");
703    }
704
705    /// Enrich statistics that use prometheus metrics.
706    pub fn with_metrics(mut self, metrics: &crate::metrics::StorageControllerMetrics) -> Self {
707        self.offset_known.0.regressions = Some(metrics.regressed_offset_known(self.id));
708        self
709    }
710}
711
712impl PackableStats for SourceStatisticsUpdate {
713    fn pack(&self, mut packer: mz_repr::RowPacker<'_>) {
714        use mz_repr::Datum;
715        // id
716        packer.push(Datum::from(self.id.to_string().as_str()));
717        // Counters.
718        packer.push(Datum::from(self.messages_received.0));
719        packer.push(Datum::from(self.bytes_received.0));
720        packer.push(Datum::from(self.updates_staged.0));
721        packer.push(Datum::from(self.updates_committed.0));
722        // Resetting gauges.
723        packer.push(Datum::from(self.records_indexed.0.0));
724        packer.push(Datum::from(self.bytes_indexed.0.0));
725        let rehydration_latency = self
726            .rehydration_latency_ms
727            .0
728            .0
729            .map(|ms| mz_repr::adt::interval::Interval::new(0, 0, ms * 1000));
730        packer.push(Datum::from(rehydration_latency));
731        packer.push(Datum::from(self.snapshot_records_known.0.0));
732        packer.push(Datum::from(self.snapshot_records_staged.0.0));
733        // Gauges
734        packer.push(Datum::from(self.snapshot_committed.0.0));
735        packer.push(Datum::from(self.offset_known.0.pack()));
736        packer.push(Datum::from(self.offset_committed.0.pack()));
737    }
738
739    fn unpack(
740        row: Row,
741        metrics: &crate::metrics::StorageControllerMetrics,
742    ) -> (GlobalId, Option<ReplicaId>, Self) {
743        let mut iter = row.iter();
744        let mut s = Self {
745            id: iter.next().unwrap().unwrap_str().parse().unwrap(),
746
747            messages_received: iter.next().unwrap().unwrap_uint64().into(),
748            bytes_received: iter.next().unwrap().unwrap_uint64().into(),
749            updates_staged: iter.next().unwrap().unwrap_uint64().into(),
750            updates_committed: iter.next().unwrap().unwrap_uint64().into(),
751
752            records_indexed: Gauge::gauge(iter.next().unwrap().unwrap_uint64()),
753            bytes_indexed: Gauge::gauge(iter.next().unwrap().unwrap_uint64()),
754            rehydration_latency_ms: Gauge::gauge(
755                <Option<mz_repr::adt::interval::Interval>>::try_from(iter.next().unwrap())
756                    .unwrap()
757                    .map(|int| int.micros / 1000),
758            ),
759            snapshot_records_known: Gauge::gauge(
760                <Option<u64>>::try_from(iter.next().unwrap()).unwrap(),
761            ),
762            snapshot_records_staged: Gauge::gauge(
763                <Option<u64>>::try_from(iter.next().unwrap()).unwrap(),
764            ),
765
766            snapshot_committed: Gauge::gauge(iter.next().unwrap().unwrap_bool()),
767            offset_known: Gauge::gauge(Some(iter.next().unwrap().unwrap_uint64())),
768            offset_committed: Gauge::gauge(Some(iter.next().unwrap().unwrap_uint64())),
769        };
770
771        s.offset_known.0.regressions = Some(metrics.regressed_offset_known(s.id));
772        (s.id, None, s)
773    }
774}
775
776/// Statistics that we keep in the controller about a given collection
777/// (identified by id) on a given replica (identified by replica_id).
778///
779/// This mirrors [SourceStatisticsUpdate] and we update ourselve by
780/// incorporating them using [ControllerSourceStatistics::incorporate]
781#[derive(Clone, Debug, PartialEq)]
782pub struct ControllerSourceStatistics {
783    pub id: GlobalId,
784
785    // This is an Option because of Webhook "sources", which don't really run on
786    // a cluster, but we decided a while ago that they are "sources", so here we
787    // are and these show up in the source statistics collection.
788    pub replica_id: Option<ReplicaId>,
789
790    pub messages_received: Counter,
791    pub bytes_received: Counter,
792    pub updates_staged: Counter,
793    pub updates_committed: Counter,
794
795    pub records_indexed: Gauge<ResettingTotal>,
796    pub bytes_indexed: Gauge<ResettingTotal>,
797    pub rehydration_latency_ms: Gauge<ResettingLatency>,
798    pub snapshot_records_known: Gauge<ResettingNullableTotal>,
799    pub snapshot_records_staged: Gauge<ResettingNullableTotal>,
800
801    pub snapshot_committed: Gauge<Boolean>,
802    pub offset_known: Gauge<Total>,
803    pub offset_committed: Gauge<Total>,
804
805    // We don't try to be cute with updating and removing stats when dropping
806    // replicas, which might run into problems with race conditions or when
807    // crashes occur.
808    //
809    // Instead, we mark when a statistic was last updated and periodically prune
810    // old statistics. It's fine if this is not 100% accurate or when the
811    // "counter" resets when the controller restarts. Eventually things will get
812    // dropped.
813    pub last_updated: Instant,
814
815    // For implementing `ZeroInitializedStats`.
816    needs_zero_initialization: bool,
817}
818
819impl ControllerSourceStatistics {
820    pub fn new(id: GlobalId, replica_id: Option<ReplicaId>) -> Self {
821        Self {
822            id,
823            replica_id,
824            messages_received: Default::default(),
825            bytes_received: Default::default(),
826            updates_staged: Default::default(),
827            updates_committed: Default::default(),
828            records_indexed: Default::default(),
829            bytes_indexed: Default::default(),
830            rehydration_latency_ms: Default::default(),
831            snapshot_records_known: Default::default(),
832            snapshot_records_staged: Default::default(),
833            snapshot_committed: Default::default(),
834            offset_known: Default::default(),
835            offset_committed: Default::default(),
836            last_updated: Instant::now(),
837            needs_zero_initialization: true,
838        }
839    }
840
841    /// Incorporate updates from the given [SourceStatisticsUpdate] into
842    /// ourselves
843    pub fn incorporate(&mut self, update: SourceStatisticsUpdate) {
844        let ControllerSourceStatistics {
845            id,
846            replica_id: _,
847            messages_received,
848            bytes_received,
849            updates_staged,
850            updates_committed,
851            records_indexed,
852            bytes_indexed,
853            rehydration_latency_ms,
854            snapshot_records_known,
855            snapshot_records_staged,
856            snapshot_committed,
857            offset_known,
858            offset_committed,
859            last_updated,
860            needs_zero_initialization: _,
861        } = self;
862
863        messages_received.incorporate(update.messages_received, *id, "messages_received");
864        bytes_received.incorporate(update.bytes_received, *id, "bytes_received");
865        updates_staged.incorporate(update.updates_staged, *id, "updates_staged");
866        updates_committed.incorporate(update.updates_committed, *id, "updates_committed");
867        records_indexed.incorporate(update.records_indexed, *id, "records_indexed");
868        bytes_indexed.incorporate(update.bytes_indexed, *id, "bytes_indexed");
869        rehydration_latency_ms.incorporate(
870            update.rehydration_latency_ms,
871            *id,
872            "rehydration_latency_ms",
873        );
874        snapshot_records_known.incorporate(
875            update.snapshot_records_known,
876            *id,
877            "snapshot_records_known",
878        );
879        snapshot_records_staged.incorporate(
880            update.snapshot_records_staged,
881            *id,
882            "snapshot_records_staged",
883        );
884        snapshot_committed.incorporate(update.snapshot_committed, *id, "snapshot_committed");
885        offset_known.incorporate(update.offset_known, *id, "offset_known");
886        offset_committed.incorporate(update.offset_committed, *id, "offset_committed");
887
888        *last_updated = Instant::now();
889    }
890}
891
892impl PackableStats for ControllerSourceStatistics {
893    fn pack(&self, mut packer: mz_repr::RowPacker<'_>) {
894        use mz_repr::Datum;
895        // id
896        packer.push(Datum::from(self.id.to_string().as_str()));
897        // replica_id
898        if let Some(replica_id) = self.replica_id {
899            packer.push(Datum::from(replica_id.to_string().as_str()));
900        } else {
901            packer.push(Datum::Null);
902        }
903        // Counters.
904        packer.push(Datum::from(self.messages_received.0));
905        packer.push(Datum::from(self.bytes_received.0));
906        packer.push(Datum::from(self.updates_staged.0));
907        packer.push(Datum::from(self.updates_committed.0));
908        // Resetting gauges.
909        packer.push(Datum::from(self.records_indexed.0.0));
910        packer.push(Datum::from(self.bytes_indexed.0.0));
911        let rehydration_latency = self
912            .rehydration_latency_ms
913            .0
914            .0
915            .map(|ms| mz_repr::adt::interval::Interval::new(0, 0, ms * 1000));
916        packer.push(Datum::from(rehydration_latency));
917        packer.push(Datum::from(self.snapshot_records_known.0.0));
918        packer.push(Datum::from(self.snapshot_records_staged.0.0));
919        // Gauges
920        packer.push(Datum::from(self.snapshot_committed.0.0));
921        packer.push(Datum::from(self.offset_known.0.pack()));
922        packer.push(Datum::from(self.offset_committed.0.pack()));
923    }
924
925    fn unpack(
926        row: Row,
927        metrics: &crate::metrics::StorageControllerMetrics,
928    ) -> (GlobalId, Option<ReplicaId>, Self) {
929        let mut iter = row.iter();
930        let id = iter.next().unwrap().unwrap_str().parse().unwrap();
931        let replica_id_or_null = iter.next().unwrap();
932
933        let replica_id = if replica_id_or_null.is_null() {
934            None
935        } else {
936            Some(
937                replica_id_or_null
938                    .unwrap_str()
939                    .parse::<ReplicaId>()
940                    .unwrap(),
941            )
942        };
943
944        let mut s = Self {
945            id,
946            replica_id,
947
948            messages_received: iter.next().unwrap().unwrap_uint64().into(),
949            bytes_received: iter.next().unwrap().unwrap_uint64().into(),
950            updates_staged: iter.next().unwrap().unwrap_uint64().into(),
951            updates_committed: iter.next().unwrap().unwrap_uint64().into(),
952
953            records_indexed: Gauge::gauge(iter.next().unwrap().unwrap_uint64()),
954            bytes_indexed: Gauge::gauge(iter.next().unwrap().unwrap_uint64()),
955            rehydration_latency_ms: Gauge::gauge(
956                <Option<mz_repr::adt::interval::Interval>>::try_from(iter.next().unwrap())
957                    .unwrap()
958                    .map(|int| int.micros / 1000),
959            ),
960            snapshot_records_known: Gauge::gauge(
961                <Option<u64>>::try_from(iter.next().unwrap()).unwrap(),
962            ),
963            snapshot_records_staged: Gauge::gauge(
964                <Option<u64>>::try_from(iter.next().unwrap()).unwrap(),
965            ),
966
967            snapshot_committed: Gauge::gauge(iter.next().unwrap().unwrap_bool()),
968            offset_known: Gauge::gauge(Some(iter.next().unwrap().unwrap_uint64())),
969            offset_committed: Gauge::gauge(Some(iter.next().unwrap().unwrap_uint64())),
970            last_updated: Instant::now(),
971            // This is coming from a Row, so we have definitely already written
972            // out a "zero update" at some point.
973            needs_zero_initialization: false,
974        };
975
976        s.offset_known.0.regressions = Some(metrics.regressed_offset_known(s.id));
977        (s.id, replica_id, s)
978    }
979}
980
981impl ExpirableStats for ControllerSourceStatistics {
982    fn last_updated(&self) -> Instant {
983        self.last_updated
984    }
985}
986
987impl ZeroInitializedStats for ControllerSourceStatistics {
988    fn needs_zero_initialization(&self) -> bool {
989        self.needs_zero_initialization
990    }
991
992    fn mark_zero_initialized(&mut self) {
993        self.needs_zero_initialization = false;
994    }
995
996    fn zero_stat(&self) -> Self {
997        ControllerSourceStatistics::new(self.id, self.replica_id)
998    }
999}
1000
1001/// Statistics that we keep in the controller about a given collection
1002/// (identified by id) on a given replica (identified by replica_id).
1003///
1004/// This mirrors [SinkStatisticsUpdate] and we update ourselve by incorporating
1005/// them using [ControllerSinkStatistics::incorporate]
1006#[derive(Clone, Debug, PartialEq)]
1007pub struct ControllerSinkStatistics {
1008    pub id: GlobalId,
1009    pub replica_id: ReplicaId,
1010
1011    pub messages_staged: Counter,
1012    pub messages_committed: Counter,
1013    pub bytes_staged: Counter,
1014    pub bytes_committed: Counter,
1015
1016    // See comment on ControllerSourceStatistics.last_updated.
1017    pub last_updated: Instant,
1018
1019    // For implementing `ZeroInitializedStats`.
1020    needs_zero_initialization: bool,
1021}
1022
1023impl ControllerSinkStatistics {
1024    pub fn new(id: GlobalId, replica_id: ReplicaId) -> Self {
1025        Self {
1026            id,
1027            replica_id,
1028            messages_staged: Default::default(),
1029            messages_committed: Default::default(),
1030            bytes_staged: Default::default(),
1031            bytes_committed: Default::default(),
1032            last_updated: Instant::now(),
1033            needs_zero_initialization: true,
1034        }
1035    }
1036
1037    /// Incorporate updates from the given [SinkStatisticsUpdate] into ourselves
1038    pub fn incorporate(&mut self, update: SinkStatisticsUpdate) {
1039        let ControllerSinkStatistics {
1040            id,
1041            replica_id: _,
1042            messages_staged,
1043            messages_committed,
1044            bytes_staged,
1045            bytes_committed,
1046            last_updated,
1047            needs_zero_initialization: _,
1048        } = self;
1049
1050        messages_staged.incorporate(update.messages_staged, *id, "messages_staged");
1051        bytes_staged.incorporate(update.bytes_staged, *id, "bytes_staged");
1052        messages_committed.incorporate(update.messages_committed, *id, "messages_committed");
1053        bytes_committed.incorporate(update.bytes_committed, *id, "bytes_committed");
1054
1055        *last_updated = Instant::now();
1056    }
1057}
1058
1059impl PackableStats for ControllerSinkStatistics {
1060    fn pack(&self, mut packer: mz_repr::RowPacker<'_>) {
1061        use mz_repr::Datum;
1062        packer.push(Datum::from(self.id.to_string().as_str()));
1063        packer.push(Datum::from(self.replica_id.to_string().as_str()));
1064        packer.push(Datum::from(self.messages_staged.0));
1065        packer.push(Datum::from(self.messages_committed.0));
1066        packer.push(Datum::from(self.bytes_staged.0));
1067        packer.push(Datum::from(self.bytes_committed.0));
1068    }
1069
1070    fn unpack(
1071        row: Row,
1072        _metrics: &crate::metrics::StorageControllerMetrics,
1073    ) -> (GlobalId, Option<ReplicaId>, Self) {
1074        let mut iter = row.iter();
1075        let s = Self {
1076            id: iter.next().unwrap().unwrap_str().parse().unwrap(),
1077            replica_id: iter.next().unwrap().unwrap_str().parse().unwrap(),
1078            messages_staged: iter.next().unwrap().unwrap_uint64().into(),
1079            messages_committed: iter.next().unwrap().unwrap_uint64().into(),
1080            bytes_staged: iter.next().unwrap().unwrap_uint64().into(),
1081            bytes_committed: iter.next().unwrap().unwrap_uint64().into(),
1082            last_updated: Instant::now(),
1083            // This is coming from a Row, so we have definitely already written
1084            // out a "zero update" at some point.
1085            needs_zero_initialization: false,
1086        };
1087        (s.id, Some(s.replica_id), s)
1088    }
1089}
1090
1091impl ExpirableStats for ControllerSinkStatistics {
1092    fn last_updated(&self) -> Instant {
1093        self.last_updated
1094    }
1095}
1096
1097impl ZeroInitializedStats for ControllerSinkStatistics {
1098    fn needs_zero_initialization(&self) -> bool {
1099        self.needs_zero_initialization
1100    }
1101
1102    fn mark_zero_initialized(&mut self) {
1103        self.needs_zero_initialization = false;
1104    }
1105
1106    fn zero_stat(&self) -> Self {
1107        ControllerSinkStatistics::new(self.id, self.replica_id)
1108    }
1109}
1110
1111/// An update as reported from a storage instance. The semantics
1112/// of each field are documented above in `MZ_SINK_STATISTICS_RAW_DESC`,
1113/// and encoded in the field types.
1114#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1115pub struct SinkStatisticsUpdate {
1116    pub id: GlobalId,
1117
1118    pub messages_staged: Counter,
1119    pub messages_committed: Counter,
1120    pub bytes_staged: Counter,
1121    pub bytes_committed: Counter,
1122}
1123
1124impl SinkStatisticsUpdate {
1125    pub fn new(id: GlobalId) -> Self {
1126        Self {
1127            id,
1128            messages_staged: Default::default(),
1129            messages_committed: Default::default(),
1130            bytes_staged: Default::default(),
1131            bytes_committed: Default::default(),
1132        }
1133    }
1134
1135    pub fn incorporate(&mut self, other: SinkStatisticsUpdate) {
1136        let SinkStatisticsUpdate {
1137            id,
1138            messages_staged,
1139            messages_committed,
1140            bytes_staged,
1141            bytes_committed,
1142            ..
1143        } = self;
1144
1145        messages_staged.incorporate(other.messages_staged, *id, "messages_staged");
1146        messages_committed.incorporate(other.messages_committed, *id, "messages_committed");
1147        bytes_staged.incorporate(other.bytes_staged, *id, "bytes_staged");
1148        bytes_committed.incorporate(other.bytes_committed, *id, "bytes_committed");
1149    }
1150
1151    /// Incorporate only the counters of the given update, ignoring gauge values.
1152    pub fn incorporate_counters(&mut self, other: SinkStatisticsUpdate) {
1153        let SinkStatisticsUpdate {
1154            id,
1155            messages_staged,
1156            messages_committed,
1157            bytes_staged,
1158            bytes_committed,
1159            ..
1160        } = self;
1161
1162        messages_staged.incorporate(other.messages_staged, *id, "messages_staged");
1163        messages_committed.incorporate(other.messages_committed, *id, "messages_committed");
1164        bytes_staged.incorporate(other.bytes_staged, *id, "bytes_staged");
1165        bytes_committed.incorporate(other.bytes_committed, *id, "bytes_committed");
1166    }
1167
1168    pub fn summarize<'a, I, F>(values: F) -> Self
1169    where
1170        I: IntoIterator<Item = &'a Self>,
1171        F: Fn() -> I,
1172        Self: 'a,
1173    {
1174        SinkStatisticsUpdate {
1175            id: values().into_iter().next().unwrap().id,
1176            messages_staged: Counter::summarize(values().into_iter().map(|s| &s.messages_staged)),
1177            messages_committed: Counter::summarize(
1178                values().into_iter().map(|s| &s.messages_committed),
1179            ),
1180            bytes_staged: Counter::summarize(values().into_iter().map(|s| &s.bytes_staged)),
1181            bytes_committed: Counter::summarize(values().into_iter().map(|s| &s.bytes_committed)),
1182        }
1183    }
1184
1185    /// Reset counters so that we continue to ship diffs to the controller.
1186    pub fn reset_counters(&mut self) {
1187        self.messages_staged.0 = 0;
1188        self.messages_committed.0 = 0;
1189        self.bytes_staged.0 = 0;
1190        self.bytes_committed.0 = 0;
1191    }
1192
1193    /// Reset all _resetable_ gauges to their default values.
1194    pub fn reset_gauges(&self) {}
1195}
1196
1197impl PackableStats for SinkStatisticsUpdate {
1198    fn pack(&self, mut packer: mz_repr::RowPacker<'_>) {
1199        use mz_repr::Datum;
1200        packer.push(Datum::from(self.id.to_string().as_str()));
1201        packer.push(Datum::from(self.messages_staged.0));
1202        packer.push(Datum::from(self.messages_committed.0));
1203        packer.push(Datum::from(self.bytes_staged.0));
1204        packer.push(Datum::from(self.bytes_committed.0));
1205    }
1206
1207    fn unpack(
1208        row: Row,
1209        _metrics: &crate::metrics::StorageControllerMetrics,
1210    ) -> (GlobalId, Option<ReplicaId>, Self) {
1211        let mut iter = row.iter();
1212        let s = Self {
1213            // Id
1214            id: iter.next().unwrap().unwrap_str().parse().unwrap(),
1215            // Counters
1216            messages_staged: iter.next().unwrap().unwrap_uint64().into(),
1217            messages_committed: iter.next().unwrap().unwrap_uint64().into(),
1218            bytes_staged: iter.next().unwrap().unwrap_uint64().into(),
1219            bytes_committed: iter.next().unwrap().unwrap_uint64().into(),
1220        };
1221        (s.id, None, s)
1222    }
1223}
1224
1225/// Statistics for webhooks.
1226#[derive(Default, Debug)]
1227pub struct WebhookStatistics {
1228    pub messages_received: AtomicU64,
1229    pub bytes_received: AtomicU64,
1230    pub updates_staged: AtomicU64,
1231    pub updates_committed: AtomicU64,
1232}
1233
1234impl WebhookStatistics {
1235    /// Drain the current statistics into a `SourceStatisticsUpdate` with
1236    /// other values defaulted, resetting the atomic counters.
1237    pub fn drain_into_update(&self, id: GlobalId) -> SourceStatisticsUpdate {
1238        SourceStatisticsUpdate {
1239            id,
1240            messages_received: self.messages_received.swap(0, Ordering::Relaxed).into(),
1241            bytes_received: self.bytes_received.swap(0, Ordering::Relaxed).into(),
1242            updates_staged: self.updates_staged.swap(0, Ordering::Relaxed).into(),
1243            updates_committed: self.updates_committed.swap(0, Ordering::Relaxed).into(),
1244            records_indexed: Gauge::gauge(0),
1245            bytes_indexed: Gauge::gauge(0),
1246            rehydration_latency_ms: Gauge::gauge(None),
1247            snapshot_records_known: Gauge::gauge(None),
1248            snapshot_records_staged: Gauge::gauge(None),
1249            snapshot_committed: Gauge::gauge(true),
1250            offset_known: Gauge::gauge(None::<u64>),
1251            offset_committed: Gauge::gauge(None::<u64>),
1252        }
1253    }
1254}
1255
1256#[cfg(test)]
1257mod tests {
1258    use super::*;
1259
1260    #[mz_ore::test]
1261    fn regression_log_errors_once_per_interval() {
1262        let mut log = RegressionLog::default();
1263        log.log(format_args!("first"));
1264        let first = log.last_error.expect("first regression logs an error");
1265        log.log(format_args!("second"));
1266        assert_eq!(
1267            log.last_error,
1268            Some(first),
1269            "second regression within the interval only warns"
1270        );
1271    }
1272}