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) -> &ConnectionId {
55        match &self {
56            ActiveComputeSink::Subscribe(subscribe) => &subscribe.conn_id,
57            ActiveComputeSink::CopyTo(copy_to) => &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/// A description of an active subscribe from coord's perspective
151#[derive(Debug)]
152pub struct ActiveSubscribe {
153    /// The ID of the connection which created the subscribe.
154    pub conn_id: ConnectionId,
155    /// The UUID of the session which created the subscribe.
156    pub session_uuid: Uuid,
157    /// The ID of the cluster on which the subscribe is running.
158    pub cluster_id: ClusterId,
159    /// The IDs of the objects on which the subscribe depends.
160    pub depends_on: BTreeSet<GlobalId>,
161    /// Channel on which to send responses to the client.
162    // The responses have the form `PeekResponseUnary` but should perhaps
163    // become `SubscribeResponse`.
164    pub channel: mpsc::UnboundedSender<PeekResponseUnary>,
165    /// Footprints of the messages queued in `channel` but not yet drained by the
166    /// client writer. Shared with the receiver side, which pops as it drains.
167    ///
168    /// The producer runs on the non-blockable coordinator loop and cannot block
169    /// on a slow client, so instead of applying backpressure the coordinator
170    /// watches `backlog_size` against `max_buffered_bytes` and retires the
171    /// subscribe once the backlog exceeds it.
172    pub backlog_accounting: Arc<Mutex<SubscribeBacklogAccounting>>,
173    /// Budget for the buffered backlog. A snapshot of `subscribe_max_buffered_bytes`
174    /// taken when the subscribe was created.
175    pub max_buffered_bytes: usize,
176    /// Whether progress information should be emitted.
177    pub emit_progress: bool,
178    /// The logical timestamp at which the subscribe began execution.
179    pub as_of: Timestamp,
180    /// The number of columns in the relation that was subscribed to.
181    pub arity: usize,
182    /// The time when the subscribe started.
183    pub start_time: EpochMillis,
184    /// How to present the subscribe's output.
185    pub output: SubscribeOutput,
186    /// If true, this is an internal subscribe that should not appear in
187    /// introspection tables like mz_subscriptions.
188    pub internal: bool,
189}
190
191impl ActiveSubscribe {
192    /// Initializes the subscription.
193    ///
194    /// This method must be called exactly once, after constructing an
195    /// `ActiveSubscribe` and before calling `process_response`.
196    pub fn initialize(&self) {
197        // Always emit progress message indicating snapshot timestamp.
198        self.send_progress_message(&Antichain::from_elem(self.as_of));
199    }
200
201    fn send_progress_message(&self, upper: &Antichain<Timestamp>) {
202        if !self.emit_progress {
203            return;
204        }
205        if let Some(upper) = upper.as_option() {
206            let mut row_buf = Row::default();
207            let mut packer = row_buf.packer();
208            packer.push(Datum::from(numeric::Numeric::from(*upper)));
209            packer.push(Datum::True);
210
211            // Fill in the mz_diff or mz_state column
212            packer.push(Datum::Null);
213
214            // Fill all table columns with NULL.
215            for _ in 0..self.arity {
216                packer.push(Datum::Null);
217            }
218
219            if let SubscribeOutput::EnvelopeDebezium { order_by_keys } = &self.output {
220                for _ in 0..(self.arity - order_by_keys.len()) {
221                    packer.push(Datum::Null);
222                }
223            }
224
225            let bytes = row_buf.byte_len();
226            let row_iter = Box::new(row_buf.into_row_iter());
227            self.send(PeekResponseUnary::Rows(row_iter), bytes);
228        }
229    }
230
231    /// Processes a subscribe response from the controller.
232    ///
233    /// Returns `true` if the subscribe is finished.
234    pub fn process_response(&self, batch: SubscribeBatch) -> bool {
235        let comparator = RowComparator::new(self.output.row_order());
236        let rows = match batch.updates {
237            Ok(ref rows) => {
238                let iters = rows.iter().map(|r| r.iter());
239                let merged = mz_ore::iter::merge_iters_by(
240                    iters,
241                    |(left_row, left_time, _), (right_row, right_time, _)| {
242                        left_time.cmp(right_time).then_with(|| {
243                            comparator.compare_rows(left_row, right_row, || left_row.cmp(right_row))
244                        })
245                    },
246                );
247                mz_ore::iter::consolidate_update_iter(merged)
248            }
249            Err(s) => {
250                self.send(PeekResponseUnary::Error(s), 0);
251                return true;
252            }
253        };
254
255        // Sort results by time. We use stable sort here because it will produce
256        // deterministic results since the cursor will always produce rows in
257        // the same order. Compute doesn't guarantee that the results are sorted
258        // (materialize#18936)
259        let mut output_buf = Row::default();
260        let mut output_builder = RowCollection::builder(0, 0);
261        let mut left_datum_vec = mz_repr::DatumVec::new();
262        let mut right_datum_vec = mz_repr::DatumVec::new();
263        let mut push_row = |row: &RowRef, time: Timestamp, diff: Diff| {
264            assert!(self.as_of <= time);
265            let mut packer = output_buf.packer();
266            // TODO: Change to MzTimestamp.
267            packer.push(Datum::from(numeric::Numeric::from(time)));
268            if self.emit_progress {
269                // When sinking with PROGRESS, the output includes an
270                // additional column that indicates whether a timestamp is
271                // complete. For regular "data" updates this is always
272                // `false`.
273                packer.push(Datum::False);
274            }
275
276            match &self.output {
277                SubscribeOutput::EnvelopeUpsert { .. }
278                | SubscribeOutput::EnvelopeDebezium { .. } => {}
279                SubscribeOutput::Diffs | SubscribeOutput::WithinTimestampOrderBy { .. } => {
280                    packer.push(Datum::Int64(diff.into_inner()));
281                }
282            }
283
284            packer.extend_by_row_ref(row);
285
286            output_builder.push(output_buf.as_row_ref(), NonZeroUsize::MIN);
287        };
288
289        match &self.output {
290            SubscribeOutput::WithinTimestampOrderBy { order_by } => {
291                let mut rows: Vec<_> = rows.collect();
292                // Since the diff is inserted as the first column, we can't take advantage of the
293                // known ordering. (Aside from timestamp, I suppose.)
294                rows.sort_by(
295                    |(left_row, left_time, left_diff), (right_row, right_time, right_diff)| {
296                        left_time.cmp(right_time).then_with(|| {
297                            let mut left_datums = left_datum_vec.borrow();
298                            left_datums.extend(&[Datum::Int64(left_diff.into_inner())]);
299                            left_datums.extend(left_row.iter());
300                            let mut right_datums = right_datum_vec.borrow();
301                            right_datums.extend(&[Datum::Int64(right_diff.into_inner())]);
302                            right_datums.extend(right_row.iter());
303                            compare_columns(order_by, &left_datums, &right_datums, || {
304                                left_row.cmp(right_row).then(left_diff.cmp(right_diff))
305                            })
306                        })
307                    },
308                );
309                for (row, time, diff) in rows {
310                    push_row(row, *time, diff);
311                }
312            }
313            SubscribeOutput::EnvelopeUpsert { order_by_keys }
314            | SubscribeOutput::EnvelopeDebezium { order_by_keys } => {
315                let debezium = matches!(self.output, SubscribeOutput::EnvelopeDebezium { .. });
316                let mut it = rows.peekable();
317                let mut datum_vec = mz_repr::DatumVec::new();
318                let mut old_datum_vec = mz_repr::DatumVec::new();
319                let comparator = RowComparator::new(order_by_keys.as_slice());
320                let mut group = Vec::with_capacity(2);
321                let mut row_buf = Row::default();
322                // The iterator is sorted by time and key, so elements in the same group should be
323                // adjacent already.
324                while let Some(start) = it.next() {
325                    group.clear();
326                    group.push(start);
327                    while let Some(row) = it.peek()
328                        && start.1 == row.1
329                        && {
330                            comparator
331                                .compare_rows(start.0, row.0, || Ordering::Equal)
332                                .is_eq()
333                        }
334                    {
335                        group.extend(it.next());
336                    }
337                    group.sort_by_key(|(_, _, d)| *d);
338
339                    // Four cases:
340                    // [(key, value, +1)] => ("insert", key, NULL, value)
341                    // [(key, v1, -1), (key, v2, +1)] => ("upsert", key, v1, v2)
342                    // [(key, value, -1)] => ("delete", key, value, NULL)
343                    // everything else => ("key_violation", key, NULL, NULL)
344                    // Defense in depth: the planner ensures that KEY columns are
345                    // distinct columns of the underlying relation, so this
346                    // subtraction must never underflow. If it does, we'd OOM
347                    // the coordinator with a giant loop, so check it here.
348                    mz_ore::soft_assert_or_log!(
349                        order_by_keys.len() <= self.arity,
350                        "SUBSCRIBE ENVELOPE has more KEY columns ({}) than \
351                         relation arity ({}); planner should have rejected this",
352                        order_by_keys.len(),
353                        self.arity,
354                    );
355                    let value_columns = self.arity.saturating_sub(order_by_keys.len());
356                    let mut packer = row_buf.packer();
357                    match &group[..] {
358                        [(row, _, Diff::ONE)] => {
359                            packer.push(if debezium {
360                                Datum::String("insert")
361                            } else {
362                                Datum::String("upsert")
363                            });
364                            let datums = datum_vec.borrow_with(row);
365                            for column_order in order_by_keys {
366                                packer.push(datums[column_order.column]);
367                            }
368                            if debezium {
369                                for _ in 0..value_columns {
370                                    packer.push(Datum::Null);
371                                }
372                            }
373                            for idx in 0..self.arity {
374                                if !order_by_keys.iter().any(|co| co.column == idx) {
375                                    packer.push(datums[idx]);
376                                }
377                            }
378                            push_row(row_buf.as_row_ref(), *start.1, Diff::ZERO)
379                        }
380                        [(_, _, Diff::MINUS_ONE)] => {
381                            packer.push(Datum::String("delete"));
382                            let datums = datum_vec.borrow_with(start.0);
383                            for column_order in order_by_keys {
384                                packer.push(datums[column_order.column]);
385                            }
386                            if debezium {
387                                for idx in 0..self.arity {
388                                    if !order_by_keys.iter().any(|co| co.column == idx) {
389                                        packer.push(datums[idx]);
390                                    }
391                                }
392                            }
393                            for _ in 0..value_columns {
394                                packer.push(Datum::Null);
395                            }
396                            push_row(row_buf.as_row_ref(), *start.1, Diff::ZERO)
397                        }
398                        [(old_row, _, Diff::MINUS_ONE), (row, _, Diff::ONE)] => {
399                            packer.push(Datum::String("upsert"));
400                            let datums = datum_vec.borrow_with(row);
401                            let old_datums = old_datum_vec.borrow_with(old_row);
402
403                            for column_order in order_by_keys {
404                                packer.push(datums[column_order.column]);
405                            }
406                            if debezium {
407                                for idx in 0..self.arity {
408                                    if !order_by_keys.iter().any(|co| co.column == idx) {
409                                        packer.push(old_datums[idx]);
410                                    }
411                                }
412                            }
413                            for idx in 0..self.arity {
414                                if !order_by_keys.iter().any(|co| co.column == idx) {
415                                    packer.push(datums[idx]);
416                                }
417                            }
418                            push_row(row_buf.as_row_ref(), *start.1, Diff::ZERO)
419                        }
420                        _ => {
421                            packer.push(Datum::String("key_violation"));
422                            let datums = datum_vec.borrow_with(start.0);
423                            for column_order in order_by_keys {
424                                packer.push(datums[column_order.column]);
425                            }
426                            if debezium {
427                                for _ in 0..value_columns {
428                                    packer.push(Datum::Null);
429                                }
430                            }
431                            for _ in 0..value_columns {
432                                packer.push(Datum::Null);
433                            }
434                            push_row(row_buf.as_row_ref(), *start.1, Diff::ZERO)
435                        }
436                    };
437                }
438            }
439            SubscribeOutput::Diffs => {
440                // Diffs output is sorted by time and row, so it can be pushed directly.
441                for (row, time, diff) in rows {
442                    push_row(row, *time, diff)
443                }
444            }
445        };
446
447        let rows = output_builder.build();
448        let bytes = rows.byte_len();
449        let rows = Box::new(rows.into_row_iter());
450        self.send(PeekResponseUnary::Rows(rows), bytes);
451
452        // Emit progress message if requested. Don't emit progress for the first
453        // batch if the upper is exactly `as_of` (we're guaranteed it is not
454        // less than `as_of`, but it might be exactly `as_of`) as we've already
455        // emitted that progress message in `initialize`.
456        if !batch.upper.less_equal(&self.as_of) {
457            self.send_progress_message(&batch.upper);
458        }
459
460        batch.upper.is_empty()
461    }
462
463    /// Retires the subscribe with the specified reason.
464    ///
465    /// This method must be called on every subscribe before it is dropped. It
466    /// informs the end client that the subscribe is finished for the specified
467    /// reason.
468    pub fn retire(self, reason: ActiveComputeSinkRetireReason) {
469        let message = match reason {
470            ActiveComputeSinkRetireReason::Finished => return,
471            ActiveComputeSinkRetireReason::Canceled => PeekResponseUnary::Canceled,
472            ActiveComputeSinkRetireReason::DependencyDropped(d) => {
473                PeekResponseUnary::DependencyDropped(d)
474            }
475            ActiveComputeSinkRetireReason::BufferExceeded {
476                buffered_bytes,
477                max_buffered_bytes,
478            } => PeekResponseUnary::Error(
479                AdapterError::SubscribeFellBehind {
480                    buffered_bytes,
481                    max_buffered_bytes,
482                }
483                .to_string(),
484            ),
485        };
486        self.send(message, 0);
487    }
488
489    /// Sends a message to the client if the subscribe has not already completed
490    /// and if the client has not already gone away.
491    ///
492    /// `bytes` is the message's payload size. Its footprint (payload plus a fixed
493    /// per-message overhead) is recorded in `backlog_accounting` here and
494    /// released by the receiver side when the message is drained. Overflow of
495    /// the budget is detected by the coordinator after `process_response`
496    /// returns, not here, because this method cannot retire the sink.
497    fn send(&self, response: PeekResponseUnary, bytes: usize) {
498        let footprint = bytes.saturating_add(SUBSCRIBE_MESSAGE_OVERHEAD_BYTES);
499        self.backlog_accounting
500            .lock()
501            .expect("subscribe backlog accounting poisoned")
502            .push(footprint);
503        let _ = self.channel.send(response);
504    }
505}
506
507/// A description of an active copy to sink from the coordinator's perspective.
508#[derive(Debug)]
509pub struct ActiveCopyTo {
510    /// The ID of the connection which created the subscribe.
511    pub conn_id: ConnectionId,
512    /// The result channel for the `COPY ... TO` statement that created the copy to sink.
513    pub tx: oneshot::Sender<Result<ExecuteResponse, AdapterError>>,
514    /// The ID of the cluster on which the copy to is running.
515    pub cluster_id: ClusterId,
516    /// The IDs of the objects on which the copy to depends.
517    pub depends_on: BTreeSet<GlobalId>,
518}
519
520impl ActiveCopyTo {
521    /// Retires the copy to with a response from the controller.
522    ///
523    /// Unlike subscribes, copy tos only expect a single response from the
524    /// controller, so `process_response` and `retire` are unified into a single
525    /// operation.
526    ///
527    /// Either this method or `retire` must be called on every copy to before it
528    /// is dropped.
529    pub fn retire_with_response(self, response: Result<u64, anyhow::Error>) {
530        let response = match response {
531            Ok(n) => Ok(ExecuteResponse::Copied(usize::cast_from(n))),
532            Err(error) => Err(AdapterError::Unstructured(error)),
533        };
534        let _ = self.tx.send(response);
535    }
536
537    /// Retires the copy to with the specified reason.
538    ///
539    /// Either this method or `retire_with_response` must be called on every
540    /// copy to before it is dropped.
541    pub fn retire(self, reason: ActiveComputeSinkRetireReason) {
542        let message = match reason {
543            ActiveComputeSinkRetireReason::Finished => return,
544            ActiveComputeSinkRetireReason::Canceled => Err(AdapterError::Canceled),
545            ActiveComputeSinkRetireReason::DependencyDropped(dep) => {
546                Err(dep.to_concurrent_dependency_drop())
547            }
548            ActiveComputeSinkRetireReason::BufferExceeded {
549                buffered_bytes,
550                max_buffered_bytes,
551            } => Err(AdapterError::SubscribeFellBehind {
552                buffered_bytes,
553                max_buffered_bytes,
554            }),
555        };
556        let _ = self.tx.send(message);
557    }
558}
559
560/// State we keep in the `Coordinator` to track active `COPY FROM` statements.
561#[derive(Debug)]
562pub(crate) struct ActiveCopyFrom {
563    /// ID of the ingestion running in clusterd.
564    pub ingestion_id: uuid::Uuid,
565    /// The cluster this is currently running on.
566    pub cluster_id: StorageInstanceId,
567    /// The table we're currently copying into.
568    pub table_id: CatalogItemId,
569    /// Context of the SQL session that ran the statement.
570    pub ctx: ExecuteContext,
571}
572
573#[cfg(test)]
574mod tests {
575    use crate::active_compute_sink::SubscribeBacklogAccounting;
576
577    /// The backlog excludes the message being drained, and zero-payload
578    /// messages (footprint = overhead only) still accumulate against it.
579    #[mz_ore::test]
580    fn test_subscribe_backlog_accounting() {
581        let mut acc = SubscribeBacklogAccounting::default();
582        assert_eq!(acc.backlog_size(), 0);
583
584        // A single large message is fully tolerated: nothing is queued behind it.
585        acc.push(10_000);
586        assert_eq!(acc.backlog_size(), 0);
587
588        // Near-empty messages (only per-message overhead) still build backlog, so
589        // a flood of frontier-only advances cannot grow without bound.
590        acc.push(1_024);
591        acc.push(1_024);
592        assert_eq!(acc.backlog_size(), 2_048);
593
594        // Draining the oldest message advances the tolerated front.
595        acc.pop();
596        assert_eq!(acc.backlog_size(), 1_024);
597
598        acc.pop();
599        acc.pop();
600        assert_eq!(acc.backlog_size(), 0);
601    }
602
603    /// A client that drains each message before the next is sent never
604    /// accumulates backlog, however many messages flow and however large they
605    /// are. This is the property that keeps a well-behaved subscribe from ever
606    /// being retired, so it is asserted after every step rather than at the end.
607    #[mz_ore::test]
608    fn test_subscribe_backlog_keeping_up_client() {
609        let mut acc = SubscribeBacklogAccounting::default();
610        for i in 0..1_000 {
611            acc.push(1_024 + i * 4_096);
612            assert_eq!(acc.backlog_size(), 0, "message {i} built backlog");
613            acc.pop();
614            assert_eq!(acc.backlog_size(), 0, "message {i} left backlog behind");
615        }
616    }
617}