Skip to main content

mz_adapter/
active_compute_sink.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//! Coordinator bookkeeping for active compute sinks.
11
12use std::cmp::Ordering;
13use std::collections::{BTreeSet, VecDeque};
14use std::num::NonZeroUsize;
15use std::sync::{Arc, Mutex};
16
17use mz_adapter_types::connection::ConnectionId;
18use mz_compute_client::protocol::response::SubscribeBatch;
19use mz_controller_types::ClusterId;
20use mz_expr::row::RowCollection;
21use mz_expr::{RowComparator, compare_columns};
22use mz_ore::cast::CastFrom;
23use mz_ore::now::EpochMillis;
24use mz_repr::adt::numeric;
25use mz_repr::{CatalogItemId, Datum, Diff, GlobalId, IntoRowIterator, Row, RowRef, Timestamp};
26use mz_sql::plan::SubscribeOutput;
27use mz_storage_types::instances::StorageInstanceId;
28use timely::progress::Antichain;
29use tokio::sync::{mpsc, oneshot};
30use uuid::Uuid;
31
32use crate::coord::peek::{DroppedDependency, PeekResponseUnary};
33use crate::{AdapterError, ExecuteContext, ExecuteResponse};
34
35#[derive(Debug)]
36/// A description of an active compute sink from the coordinator's perspective.
37pub enum ActiveComputeSink {
38    /// An active subscribe sink.
39    Subscribe(ActiveSubscribe),
40    /// An active copy to sink.
41    CopyTo(ActiveCopyTo),
42}
43
44impl ActiveComputeSink {
45    /// Reports the ID of the cluster on which the sink is running.
46    pub fn cluster_id(&self) -> ClusterId {
47        match &self {
48            ActiveComputeSink::Subscribe(subscribe) => subscribe.cluster_id,
49            ActiveComputeSink::CopyTo(copy_to) => copy_to.cluster_id,
50        }
51    }
52
53    /// Reports the ID of the connection which created the sink.
54    pub fn connection_id(&self) -> Option<&ConnectionId> {
55        match &self {
56            ActiveComputeSink::Subscribe(subscribe) => subscribe.connection_id(),
57            ActiveComputeSink::CopyTo(copy_to) => Some(&copy_to.conn_id),
58        }
59    }
60
61    /// Reports the IDs of the objects on which the sink depends.
62    pub fn depends_on(&self) -> &BTreeSet<GlobalId> {
63        match &self {
64            ActiveComputeSink::Subscribe(subscribe) => &subscribe.depends_on,
65            ActiveComputeSink::CopyTo(copy_to) => &copy_to.depends_on,
66        }
67    }
68
69    /// Retires the sink with the specified reason.
70    ///
71    /// This method must be called on every sink before it is dropped. It
72    /// informs the end client that the sink is finished for the specified
73    /// reason.
74    pub fn retire(self, reason: ActiveComputeSinkRetireReason) {
75        match self {
76            ActiveComputeSink::Subscribe(subscribe) => subscribe.retire(reason),
77            ActiveComputeSink::CopyTo(copy_to) => copy_to.retire(reason),
78        }
79    }
80}
81
82/// The reason for removing an [`ActiveComputeSink`].
83#[derive(Debug, Clone)]
84pub enum ActiveComputeSinkRetireReason {
85    /// The compute sink completed successfully.
86    Finished,
87    /// The compute sink was canceled due to a user request.
88    Canceled,
89    /// The compute sink was forcibly terminated because an object it depended on
90    /// was dropped.
91    DependencyDropped(DroppedDependency),
92    /// The compute sink was retired because its coordinator-side buffer exceeded
93    /// its budget while the client was not reading fast enough. Carries the
94    /// buffered and budget byte counts for the terminal error.
95    BufferExceeded {
96        buffered_bytes: usize,
97        max_buffered_bytes: usize,
98    },
99}
100
101/// Overhead charged to every queued message on top of its payload.
102///
103/// A frontier-only advance carries no rows, so its payload is zero bytes. It
104/// still costs real memory: a node in the unbounded channel and an entry in
105/// `footprints`. Charging payload alone would leave the backlog flat while
106/// those two grow without bound, so the retire check would never trip on a
107/// stalled client that only receives progress messages. The fixed charge makes
108/// the budget bound message *count* as well as payload bytes.
109const SUBSCRIBE_MESSAGE_OVERHEAD_BYTES: usize = 1024;
110
111/// Footprints of the subscribe messages queued to the client but not yet
112/// drained, oldest first. The producer pushes one per message sent, the
113/// client-writer task pops as it drains. FIFO delivery keeps the queue aligned
114/// with the channel.
115///
116/// The message at the front is the one the client is currently draining. It is
117/// always tolerated, however large, so a client working through one big batch is
118/// not retired. Only what is queued behind it counts as backlog.
119#[derive(Debug, Default)]
120pub struct SubscribeBacklogAccounting {
121    /// Per-message footprints, in send order.
122    footprints: VecDeque<usize>,
123    /// Sum of `footprints`.
124    total: usize,
125}
126
127impl SubscribeBacklogAccounting {
128    /// Records a queued message of the given footprint.
129    pub fn push(&mut self, footprint: usize) {
130        self.footprints.push_back(footprint);
131        self.total = self.total.saturating_add(footprint);
132    }
133
134    /// Records the oldest queued message being drained by the client writer.
135    pub fn pop(&mut self) {
136        if let Some(footprint) = self.footprints.pop_front() {
137            self.total = self.total.saturating_sub(footprint);
138        }
139    }
140
141    /// Bytes queued behind the message the client is currently draining. That
142    /// message is tolerated whatever its size, so only this counts against the
143    /// budget.
144    pub fn backlog_size(&self) -> usize {
145        self.total
146            .saturating_sub(self.footprints.front().copied().unwrap_or(0))
147    }
148}
149
150/// Ownership and cleanup scope of an active subscribe.
151#[derive(Debug)]
152pub enum ActiveSubscribeOwner {
153    /// The subscribe belongs to a SQL session.
154    Session {
155        conn_id: ConnectionId,
156        session_uuid: Uuid,
157    },
158    /// The subscribe belongs to a coordinator background task.
159    ///
160    /// Always `internal`, since there is no session to attribute a
161    /// `mz_subscriptions` row to.
162    Background,
163}
164
165/// A description of an active subscribe from coord's perspective
166#[derive(Debug)]
167pub struct ActiveSubscribe {
168    /// The owner responsible for retiring the subscribe.
169    pub owner: ActiveSubscribeOwner,
170    /// The ID of the cluster on which the subscribe is running.
171    pub cluster_id: ClusterId,
172    /// The IDs of the objects on which the subscribe depends.
173    pub depends_on: BTreeSet<GlobalId>,
174    /// Channel on which to send responses to the client.
175    // The responses have the form `PeekResponseUnary` but should perhaps
176    // become `SubscribeResponse`.
177    pub channel: mpsc::UnboundedSender<PeekResponseUnary>,
178    /// Footprints of the messages queued in `channel` but not yet drained by the
179    /// client writer. Shared with the receiver side, which pops as it drains.
180    ///
181    /// The producer runs on the non-blockable coordinator loop and cannot block
182    /// on a slow client, so instead of applying backpressure the coordinator
183    /// watches `backlog_size` against `max_buffered_bytes` and retires the
184    /// subscribe once the backlog exceeds it.
185    pub backlog_accounting: Arc<Mutex<SubscribeBacklogAccounting>>,
186    /// Budget for the buffered backlog. A snapshot of `subscribe_max_buffered_bytes`
187    /// taken when the subscribe was created.
188    pub max_buffered_bytes: usize,
189    /// Whether progress information should be emitted.
190    pub emit_progress: bool,
191    /// The logical timestamp at which the subscribe began execution.
192    pub as_of: Timestamp,
193    /// The number of columns in the relation that was subscribed to.
194    pub arity: usize,
195    /// The time when the subscribe started.
196    pub start_time: EpochMillis,
197    /// How to present the subscribe's output.
198    pub output: SubscribeOutput,
199    /// If true, this is an internal subscribe that should not appear in
200    /// introspection tables like mz_subscriptions.
201    pub internal: bool,
202}
203
204impl ActiveSubscribe {
205    /// The session uuid for this subscribe's `mz_subscriptions` row, or `None`
206    /// if it does not appear there.
207    pub fn introspection_session_uuid(&self) -> Option<Uuid> {
208        match &self.owner {
209            ActiveSubscribeOwner::Session { session_uuid, .. } if !self.internal => {
210                Some(*session_uuid)
211            }
212            _ => None,
213        }
214    }
215
216    /// Returns the owning connection, if this is a session subscribe.
217    pub fn connection_id(&self) -> Option<&ConnectionId> {
218        match &self.owner {
219            ActiveSubscribeOwner::Session { conn_id, .. } => Some(conn_id),
220            ActiveSubscribeOwner::Background => None,
221        }
222    }
223
224    /// Initializes the subscription.
225    ///
226    /// This method must be called exactly once, after constructing an
227    /// `ActiveSubscribe` and before calling `process_response`.
228    pub fn initialize(&self) {
229        // Always emit progress message indicating snapshot timestamp.
230        self.send_progress_message(&Antichain::from_elem(self.as_of));
231    }
232
233    fn send_progress_message(&self, upper: &Antichain<Timestamp>) {
234        if !self.emit_progress {
235            return;
236        }
237        if let Some(upper) = upper.as_option() {
238            let mut row_buf = Row::default();
239            let mut packer = row_buf.packer();
240            packer.push(Datum::from(numeric::Numeric::from(*upper)));
241            packer.push(Datum::True);
242
243            // Fill in the mz_diff or mz_state column
244            packer.push(Datum::Null);
245
246            // Fill all table columns with NULL.
247            for _ in 0..self.arity {
248                packer.push(Datum::Null);
249            }
250
251            if let SubscribeOutput::EnvelopeDebezium { order_by_keys } = &self.output {
252                for _ in 0..(self.arity - order_by_keys.len()) {
253                    packer.push(Datum::Null);
254                }
255            }
256
257            let bytes = row_buf.byte_len();
258            let row_iter = Box::new(row_buf.into_row_iter());
259            self.send(PeekResponseUnary::Rows(row_iter), bytes);
260        }
261    }
262
263    /// Processes a subscribe response from the controller.
264    ///
265    /// Returns `true` if the subscribe is finished.
266    pub fn process_response(&self, batch: SubscribeBatch) -> bool {
267        let comparator = RowComparator::new(self.output.row_order());
268        let rows = match batch.updates {
269            Ok(ref rows) => {
270                let iters = rows.iter().map(|r| r.iter());
271                let merged = mz_ore::iter::merge_iters_by(
272                    iters,
273                    |(left_row, left_time, _), (right_row, right_time, _)| {
274                        left_time.cmp(right_time).then_with(|| {
275                            comparator.compare_rows(left_row, right_row, || left_row.cmp(right_row))
276                        })
277                    },
278                );
279                mz_ore::iter::consolidate_update_iter(merged)
280            }
281            Err(s) => {
282                self.send(
283                    PeekResponseUnary::Error(AdapterError::Unstructured(anyhow::Error::msg(s))),
284                    0,
285                );
286                return true;
287            }
288        };
289
290        // Sort results by time. We use stable sort here because it will produce
291        // deterministic results since the cursor will always produce rows in
292        // the same order. Compute doesn't guarantee that the results are sorted
293        // (materialize#18936)
294        let mut output_buf = Row::default();
295        let mut output_builder = RowCollection::builder(0, 0);
296        let mut left_datum_vec = mz_repr::DatumVec::new();
297        let mut right_datum_vec = mz_repr::DatumVec::new();
298        let mut push_row = |row: &RowRef, time: Timestamp, diff: Diff| {
299            assert!(self.as_of <= time);
300            let mut packer = output_buf.packer();
301            // TODO: Change to MzTimestamp.
302            packer.push(Datum::from(numeric::Numeric::from(time)));
303            if self.emit_progress {
304                // When sinking with PROGRESS, the output includes an
305                // additional column that indicates whether a timestamp is
306                // complete. For regular "data" updates this is always
307                // `false`.
308                packer.push(Datum::False);
309            }
310
311            match &self.output {
312                SubscribeOutput::EnvelopeUpsert { .. }
313                | SubscribeOutput::EnvelopeDebezium { .. } => {}
314                SubscribeOutput::Diffs | SubscribeOutput::WithinTimestampOrderBy { .. } => {
315                    packer.push(Datum::Int64(diff.into_inner()));
316                }
317            }
318
319            packer.extend_by_row_ref(row);
320
321            output_builder.push(output_buf.as_row_ref(), NonZeroUsize::MIN);
322        };
323
324        match &self.output {
325            SubscribeOutput::WithinTimestampOrderBy { order_by } => {
326                let mut rows: Vec<_> = rows.collect();
327                // Since the diff is inserted as the first column, we can't take advantage of the
328                // known ordering. (Aside from timestamp, I suppose.)
329                rows.sort_by(
330                    |(left_row, left_time, left_diff), (right_row, right_time, right_diff)| {
331                        left_time.cmp(right_time).then_with(|| {
332                            let mut left_datums = left_datum_vec.borrow();
333                            left_datums.extend(&[Datum::Int64(left_diff.into_inner())]);
334                            left_datums.extend(left_row.iter());
335                            let mut right_datums = right_datum_vec.borrow();
336                            right_datums.extend(&[Datum::Int64(right_diff.into_inner())]);
337                            right_datums.extend(right_row.iter());
338                            compare_columns(order_by, &left_datums, &right_datums, || {
339                                left_row.cmp(right_row).then(left_diff.cmp(right_diff))
340                            })
341                        })
342                    },
343                );
344                for (row, time, diff) in rows {
345                    push_row(row, *time, diff);
346                }
347            }
348            SubscribeOutput::EnvelopeUpsert { order_by_keys }
349            | SubscribeOutput::EnvelopeDebezium { order_by_keys } => {
350                let debezium = matches!(self.output, SubscribeOutput::EnvelopeDebezium { .. });
351                let mut it = rows.peekable();
352                let mut datum_vec = mz_repr::DatumVec::new();
353                let mut old_datum_vec = mz_repr::DatumVec::new();
354                let comparator = RowComparator::new(order_by_keys.as_slice());
355                let mut group = Vec::with_capacity(2);
356                let mut row_buf = Row::default();
357                // The iterator is sorted by time and key, so elements in the same group should be
358                // adjacent already.
359                while let Some(start) = it.next() {
360                    group.clear();
361                    group.push(start);
362                    while let Some(row) = it.peek()
363                        && start.1 == row.1
364                        && {
365                            comparator
366                                .compare_rows(start.0, row.0, || Ordering::Equal)
367                                .is_eq()
368                        }
369                    {
370                        group.extend(it.next());
371                    }
372                    group.sort_by_key(|(_, _, d)| *d);
373
374                    // Four cases:
375                    // [(key, value, +1)] => ("insert", key, NULL, value)
376                    // [(key, v1, -1), (key, v2, +1)] => ("upsert", key, v1, v2)
377                    // [(key, value, -1)] => ("delete", key, value, NULL)
378                    // everything else => ("key_violation", key, NULL, NULL)
379                    // Defense in depth: the planner ensures that KEY columns are
380                    // distinct columns of the underlying relation, so this
381                    // subtraction must never underflow. If it does, we'd OOM
382                    // the coordinator with a giant loop, so check it here.
383                    mz_ore::soft_assert_or_log!(
384                        order_by_keys.len() <= self.arity,
385                        "SUBSCRIBE ENVELOPE has more KEY columns ({}) than \
386                         relation arity ({}); planner should have rejected this",
387                        order_by_keys.len(),
388                        self.arity,
389                    );
390                    let value_columns = self.arity.saturating_sub(order_by_keys.len());
391                    let mut packer = row_buf.packer();
392                    match &group[..] {
393                        [(row, _, Diff::ONE)] => {
394                            packer.push(if debezium {
395                                Datum::String("insert")
396                            } else {
397                                Datum::String("upsert")
398                            });
399                            let datums = datum_vec.borrow_with(row);
400                            for column_order in order_by_keys {
401                                packer.push(datums[column_order.column]);
402                            }
403                            if debezium {
404                                for _ in 0..value_columns {
405                                    packer.push(Datum::Null);
406                                }
407                            }
408                            for idx in 0..self.arity {
409                                if !order_by_keys.iter().any(|co| co.column == idx) {
410                                    packer.push(datums[idx]);
411                                }
412                            }
413                            push_row(row_buf.as_row_ref(), *start.1, Diff::ZERO)
414                        }
415                        [(_, _, Diff::MINUS_ONE)] => {
416                            packer.push(Datum::String("delete"));
417                            let datums = datum_vec.borrow_with(start.0);
418                            for column_order in order_by_keys {
419                                packer.push(datums[column_order.column]);
420                            }
421                            if debezium {
422                                for idx in 0..self.arity {
423                                    if !order_by_keys.iter().any(|co| co.column == idx) {
424                                        packer.push(datums[idx]);
425                                    }
426                                }
427                            }
428                            for _ in 0..value_columns {
429                                packer.push(Datum::Null);
430                            }
431                            push_row(row_buf.as_row_ref(), *start.1, Diff::ZERO)
432                        }
433                        [(old_row, _, Diff::MINUS_ONE), (row, _, Diff::ONE)] => {
434                            packer.push(Datum::String("upsert"));
435                            let datums = datum_vec.borrow_with(row);
436                            let old_datums = old_datum_vec.borrow_with(old_row);
437
438                            for column_order in order_by_keys {
439                                packer.push(datums[column_order.column]);
440                            }
441                            if debezium {
442                                for idx in 0..self.arity {
443                                    if !order_by_keys.iter().any(|co| co.column == idx) {
444                                        packer.push(old_datums[idx]);
445                                    }
446                                }
447                            }
448                            for idx in 0..self.arity {
449                                if !order_by_keys.iter().any(|co| co.column == idx) {
450                                    packer.push(datums[idx]);
451                                }
452                            }
453                            push_row(row_buf.as_row_ref(), *start.1, Diff::ZERO)
454                        }
455                        _ => {
456                            packer.push(Datum::String("key_violation"));
457                            let datums = datum_vec.borrow_with(start.0);
458                            for column_order in order_by_keys {
459                                packer.push(datums[column_order.column]);
460                            }
461                            if debezium {
462                                for _ in 0..value_columns {
463                                    packer.push(Datum::Null);
464                                }
465                            }
466                            for _ in 0..value_columns {
467                                packer.push(Datum::Null);
468                            }
469                            push_row(row_buf.as_row_ref(), *start.1, Diff::ZERO)
470                        }
471                    };
472                }
473            }
474            SubscribeOutput::Diffs => {
475                // Diffs output is sorted by time and row, so it can be pushed directly.
476                for (row, time, diff) in rows {
477                    push_row(row, *time, diff)
478                }
479            }
480        };
481
482        let rows = output_builder.build();
483        let bytes = rows.byte_len();
484        let rows = Box::new(rows.into_row_iter());
485        self.send(PeekResponseUnary::Rows(rows), bytes);
486
487        // Emit progress message if requested. Don't emit progress for the first
488        // batch if the upper is exactly `as_of` (we're guaranteed it is not
489        // less than `as_of`, but it might be exactly `as_of`) as we've already
490        // emitted that progress message in `initialize`.
491        if !batch.upper.less_equal(&self.as_of) {
492            self.send_progress_message(&batch.upper);
493        }
494
495        batch.upper.is_empty()
496    }
497
498    /// Retires the subscribe with the specified reason.
499    ///
500    /// This method must be called on every subscribe before it is dropped. It
501    /// informs the end client that the subscribe is finished for the specified
502    /// reason.
503    pub fn retire(self, reason: ActiveComputeSinkRetireReason) {
504        let message = match reason {
505            ActiveComputeSinkRetireReason::Finished => return,
506            ActiveComputeSinkRetireReason::Canceled => PeekResponseUnary::Canceled,
507            ActiveComputeSinkRetireReason::DependencyDropped(d) => {
508                PeekResponseUnary::DependencyDropped(d)
509            }
510            ActiveComputeSinkRetireReason::BufferExceeded {
511                buffered_bytes,
512                max_buffered_bytes,
513            } => PeekResponseUnary::Error(AdapterError::SubscribeFellBehind {
514                buffered_bytes,
515                max_buffered_bytes,
516            }),
517        };
518        self.send(message, 0);
519    }
520
521    /// Sends a message to the client if the subscribe has not already completed
522    /// and if the client has not already gone away.
523    ///
524    /// `bytes` is the message's payload size. Its footprint (payload plus a fixed
525    /// per-message overhead) is recorded in `backlog_accounting` here and
526    /// released by the receiver side when the message is drained. Overflow of
527    /// the budget is detected by the coordinator after `process_response`
528    /// returns, not here, because this method cannot retire the sink.
529    fn send(&self, response: PeekResponseUnary, bytes: usize) {
530        let footprint = bytes.saturating_add(SUBSCRIBE_MESSAGE_OVERHEAD_BYTES);
531        self.backlog_accounting
532            .lock()
533            .expect("subscribe backlog accounting poisoned")
534            .push(footprint);
535        let _ = self.channel.send(response);
536    }
537}
538
539/// A description of an active copy to sink from the coordinator's perspective.
540#[derive(Debug)]
541pub struct ActiveCopyTo {
542    /// The ID of the connection which created the subscribe.
543    pub conn_id: ConnectionId,
544    /// The result channel for the `COPY ... TO` statement that created the copy to sink.
545    pub tx: oneshot::Sender<Result<ExecuteResponse, AdapterError>>,
546    /// The ID of the cluster on which the copy to is running.
547    pub cluster_id: ClusterId,
548    /// The IDs of the objects on which the copy to depends.
549    pub depends_on: BTreeSet<GlobalId>,
550}
551
552impl ActiveCopyTo {
553    /// Retires the copy to with a response from the controller.
554    ///
555    /// Unlike subscribes, copy tos only expect a single response from the
556    /// controller, so `process_response` and `retire` are unified into a single
557    /// operation.
558    ///
559    /// Either this method or `retire` must be called on every copy to before it
560    /// is dropped.
561    pub fn retire_with_response(self, response: Result<u64, anyhow::Error>) {
562        let response = match response {
563            Ok(n) => Ok(ExecuteResponse::Copied(usize::cast_from(n))),
564            Err(error) => Err(AdapterError::Unstructured(error)),
565        };
566        let _ = self.tx.send(response);
567    }
568
569    /// Retires the copy to with the specified reason.
570    ///
571    /// Either this method or `retire_with_response` must be called on every
572    /// copy to before it is dropped.
573    pub fn retire(self, reason: ActiveComputeSinkRetireReason) {
574        let message = match reason {
575            ActiveComputeSinkRetireReason::Finished => return,
576            ActiveComputeSinkRetireReason::Canceled => Err(AdapterError::Canceled),
577            ActiveComputeSinkRetireReason::DependencyDropped(dep) => {
578                Err(dep.to_concurrent_dependency_drop())
579            }
580            ActiveComputeSinkRetireReason::BufferExceeded {
581                buffered_bytes,
582                max_buffered_bytes,
583            } => Err(AdapterError::SubscribeFellBehind {
584                buffered_bytes,
585                max_buffered_bytes,
586            }),
587        };
588        let _ = self.tx.send(message);
589    }
590}
591
592/// State we keep in the `Coordinator` to track active `COPY FROM` statements.
593#[derive(Debug)]
594pub(crate) struct ActiveCopyFrom {
595    /// ID of the ingestion running in clusterd.
596    pub ingestion_id: uuid::Uuid,
597    /// The cluster this is currently running on.
598    pub cluster_id: StorageInstanceId,
599    /// The table we're currently copying into.
600    pub table_id: CatalogItemId,
601    /// Context of the SQL session that ran the statement.
602    pub ctx: ExecuteContext,
603}
604
605#[cfg(test)]
606mod tests {
607    use crate::active_compute_sink::SubscribeBacklogAccounting;
608
609    /// The backlog excludes the message being drained, and zero-payload
610    /// messages (footprint = overhead only) still accumulate against it.
611    #[mz_ore::test]
612    fn test_subscribe_backlog_accounting() {
613        let mut acc = SubscribeBacklogAccounting::default();
614        assert_eq!(acc.backlog_size(), 0);
615
616        // A single large message is fully tolerated: nothing is queued behind it.
617        acc.push(10_000);
618        assert_eq!(acc.backlog_size(), 0);
619
620        // Near-empty messages (only per-message overhead) still build backlog, so
621        // a flood of frontier-only advances cannot grow without bound.
622        acc.push(1_024);
623        acc.push(1_024);
624        assert_eq!(acc.backlog_size(), 2_048);
625
626        // Draining the oldest message advances the tolerated front.
627        acc.pop();
628        assert_eq!(acc.backlog_size(), 1_024);
629
630        acc.pop();
631        acc.pop();
632        assert_eq!(acc.backlog_size(), 0);
633    }
634
635    /// A client that drains each message before the next is sent never
636    /// accumulates backlog, however many messages flow and however large they
637    /// are. This is the property that keeps a well-behaved subscribe from ever
638    /// being retired, so it is asserted after every step rather than at the end.
639    #[mz_ore::test]
640    fn test_subscribe_backlog_keeping_up_client() {
641        let mut acc = SubscribeBacklogAccounting::default();
642        for i in 0..1_000 {
643            acc.push(1_024 + i * 4_096);
644            assert_eq!(acc.backlog_size(), 0, "message {i} built backlog");
645            acc.pop();
646            assert_eq!(acc.backlog_size(), 0, "message {i} left backlog behind");
647        }
648    }
649}