Skip to main content

mz_compute_client/protocol/
response.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//! Compute protocol responses.
11
12use std::fmt;
13
14use bytesize::ByteSize;
15use mz_expr::EvalError;
16use mz_expr::row::RowCollection;
17use mz_ore::cast::CastFrom;
18use mz_ore::tracing::OpenTelemetryContext;
19use mz_persist_client::batch::ProtoBatch;
20use mz_persist_types::ShardId;
21use mz_repr::{GlobalId, RelationDesc, Timestamp, UpdateCollection};
22use mz_storage_types::errors::DataflowError;
23use serde::{Deserialize, Serialize};
24use timely::progress::frontier::Antichain;
25use uuid::Uuid;
26
27/// Compute protocol responses, sent by replicas to the compute controller.
28///
29/// Replicas send `ComputeResponse`s in response to [`ComputeCommand`]s they previously received
30/// from the compute controller.
31///
32/// [`ComputeCommand`]: super::command::ComputeCommand
33#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
34pub enum ComputeResponse {
35    /// `Frontiers` announces the advancement of the various frontiers of the specified compute
36    /// collection.
37    ///
38    /// Replicas must send `Frontiers` responses for compute collections that are indexes or
39    /// storage sinks. Replicas must not send `Frontiers` responses for subscribes and copy-tos
40    /// ([#16274]).
41    ///
42    /// Replicas must never report regressing frontiers. Specifically:
43    ///
44    ///   * The first frontier of any kind reported for a collection must not be less than that
45    ///     collection's initial `as_of` frontier.
46    ///   * Subsequent reported frontiers for a collection must not be less than any frontier of
47    ///     the same kind reported previously for the same collection.
48    ///
49    /// Replicas must send `Frontiers` responses that report each frontier kind to have advanced to
50    /// the empty frontier in response to an [`AllowCompaction` command] that allows compaction of
51    /// the collection to to the empty frontier, unless the frontier has previously advanced to the
52    /// empty frontier as part of the regular dataflow computation. ([#16271])
53    ///
54    /// Once a frontier was reported to have been advanced to the empty frontier, the replica must
55    /// not send further `Frontiers` responses with non-`None` values for that frontier kind.
56    ///
57    /// The replica must not send `Frontiers` responses for collections that have not
58    /// been created previously by a [`CreateDataflow` command] or by a [`CreateInstance`
59    /// command].
60    ///
61    /// [`AllowCompaction` command]: super::command::ComputeCommand::AllowCompaction
62    /// [`CreateDataflow` command]: super::command::ComputeCommand::CreateDataflow
63    /// [`CreateInstance` command]: super::command::ComputeCommand::CreateInstance
64    /// [#16271]: https://github.com/MaterializeInc/database-issues/issues/4699
65    /// [#16274]: https://github.com/MaterializeInc/database-issues/issues/4701
66    Frontiers(GlobalId, FrontiersResponse),
67
68    /// `PeekResponse` reports the result of a previous [`Peek` command]. The peek is identified by
69    /// a `Uuid` that matches the command's [`Peek::uuid`].
70    ///
71    /// The replica must send exactly one `PeekResponse` for every [`Peek` command] it received.
72    ///
73    /// If the replica did not receive a [`CancelPeek` command] for a peek, it must not send a
74    /// [`Canceled`] response for that peek. If the replica did receive a [`CancelPeek` command]
75    /// for a peek, it may send any of the three [`PeekResponse`] variants.
76    ///
77    /// The replica must not send `PeekResponse`s for peek IDs that were not previously specified
78    /// in a [`Peek` command].
79    ///
80    /// [`Peek` command]: super::command::ComputeCommand::Peek
81    /// [`CancelPeek` command]: super::command::ComputeCommand::CancelPeek
82    /// [`Peek::uuid`]: super::command::Peek::uuid
83    /// [`Canceled`]: PeekResponse::Canceled
84    PeekResponse(Uuid, PeekResponse, OpenTelemetryContext),
85
86    /// `SubscribeResponse` reports the results emitted by an active subscribe over some time
87    /// interval.
88    ///
89    /// For each subscribe that was installed by a previous [`CreateDataflow` command], the
90    /// replica must emit [`Batch`] responses that cover the entire time interval from the
91    /// minimum time until the subscribe advances to the empty frontier or is
92    /// dropped. The time intervals of consecutive [`Batch`]es must be increasing, contiguous,
93    /// non-overlapping, and non-empty. All updates transmitted in a batch must be consolidated and
94    /// have times within that batch’s time interval. All updates' times must be greater than or
95    /// equal to `as_of`. The `upper` of the first [`Batch`] of a subscribe must not be less than
96    /// that subscribe's initial `as_of` frontier.
97    ///
98    /// The replica must send [`DroppedAt`] responses if the subscribe was dropped in response to
99    /// an [`AllowCompaction` command] that advanced its read frontier to the empty frontier. The
100    /// [`DroppedAt`] frontier must be the upper frontier of the last emitted batch.
101    ///
102    /// The replica must not send a [`DroppedAt`] response if the subscribe’s upper frontier
103    /// (reported by [`Batch`] responses) has advanced to the empty frontier (e.g. because its
104    /// inputs advanced to the empty frontier).
105    ///
106    /// Once a subscribe was reported to have advanced to the empty frontier, or has been dropped:
107    ///
108    ///   * It must no longer read from its inputs.
109    ///   * The replica must not send further `SubscribeResponse`s for that subscribe.
110    ///
111    /// The replica must not send `SubscribeResponse`s for subscribes that have not been
112    /// created previously by a [`CreateDataflow` command].
113    ///
114    /// [`Batch`]: SubscribeResponse::Batch
115    /// [`DroppedAt`]: SubscribeResponse::DroppedAt
116    /// [`CreateDataflow` command]: super::command::ComputeCommand::CreateDataflow
117    /// [`AllowCompaction` command]: super::command::ComputeCommand::AllowCompaction
118    SubscribeResponse(GlobalId, SubscribeResponse),
119
120    /// `CopyToResponse` reports the completion of an S3-oneshot sink.
121    ///
122    /// The replica must send exactly one `CopyToResponse` for every S3-oneshot sink previously
123    /// created by a [`CreateDataflow` command].
124    ///
125    /// The replica must not send `CopyToResponse`s for S3-oneshot sinks that were not previously
126    /// created by a [`CreateDataflow` command].
127    ///
128    /// [`CreateDataflow` command]: super::command::ComputeCommand::CreateDataflow
129    CopyToResponse(GlobalId, CopyToResponse),
130
131    /// `Status` reports status updates from replicas to the controller.
132    ///
133    /// `Status` responses are a way for replicas to stream back introspection data that the
134    /// controller can then announce to its clients. They have no effect on the lifecycles of
135    /// compute collections. Correct operation of the Compute layer must not rely on `Status`
136    /// responses being sent or received.
137    ///
138    /// `Status` responses that are specific to collections must only be sent for collections that
139    /// (a) have previously been created by a [`CreateDataflow` command] and (b) have not yet
140    /// been reported to have advanced to the empty frontier.
141    ///
142    /// [`CreateDataflow` command]: super::command::ComputeCommand::CreateDataflow
143    Status(StatusResponse),
144}
145
146/// A response reporting advancement of frontiers of a compute collection.
147///
148/// All contained frontier fields are optional. `None` values imply that the respective frontier
149/// has not advanced and the previously reported value is still current.
150#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
151pub struct FrontiersResponse {
152    /// The collection's new write frontier, if any.
153    ///
154    /// Upon receiving an updated `write_frontier`, the controller may assume that the contents of the
155    /// collection are sealed for all times less than that frontier. Once it has reported the
156    /// `write_frontier` as the empty frontier, the replica must no longer change the contents of the
157    /// collection.
158    pub write_frontier: Option<Antichain<Timestamp>>,
159    /// The collection's new input frontier, if any.
160    ///
161    /// Upon receiving an updated `input_frontier`, the controller may assume that the replica has
162    /// finished reading from the collection’s inputs up to that frontier. Once it has reported the
163    /// `input_frontier` as the empty frontier, the replica must no longer read from the
164    /// collection's inputs.
165    pub input_frontier: Option<Antichain<Timestamp>>,
166    /// The collection's new output frontier, if any.
167    ///
168    /// Upon receiving an updated `output_frontier`, the controller may assume that the replica
169    /// has finished processing the collection's input up to that frontier.
170    ///
171    /// The `output_frontier` is often equal to the `write_frontier`, but not always. Some
172    /// collections can jump their write frontiers ahead of the times they have finished
173    /// processing, causing the `output_frontier` to lag behind the `write_frontier`. Collections
174    /// writing materialized views do so in two cases:
175    ///
176    ///  * `REFRESH` MVs jump their write frontier ahead to the next refresh time.
177    ///  * In a multi-replica cluster, slower replicas observe and report the write frontier of the
178    ///    fastest replica, by witnessing advancements of the target persist shard's `upper`.
179    pub output_frontier: Option<Antichain<Timestamp>>,
180}
181
182impl FrontiersResponse {
183    /// Returns whether there are any contained updates.
184    pub fn has_updates(&self) -> bool {
185        self.write_frontier.is_some()
186            || self.input_frontier.is_some()
187            || self.output_frontier.is_some()
188    }
189}
190
191/// The response from a `Peek`.
192///
193/// Note that each `Peek` expects to generate exactly one `PeekResponse`, i.e.
194/// we expect a 1:1 contract between `Peek` and `PeekResponse`.
195///
196/// Encoded with bincode, which identifies a variant by its position, so both ends of a connection
197/// have to agree on this declaration. They do: the CTP handshake refuses a peer whose version
198/// differs from its own (`mz_service::transport`), so a replica never speaks to a controller that
199/// declares a different shape, and the encoding is free to change with the type.
200#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
201pub enum PeekResponse {
202    /// Returned rows of a successful peek.
203    Rows(Vec<RowCollection>),
204    /// Results of the peek were stashed in persist batches.
205    Stashed(Box<StashedPeekResponse>),
206    /// Error of an unsuccessful peek.
207    Error(PeekError),
208    /// The peek was canceled.
209    Canceled,
210}
211
212impl PeekResponse {
213    /// Return the size of row bytes stored inline in this response.
214    pub fn inline_byte_len(&self) -> usize {
215        match self {
216            Self::Rows(rows) => rows.iter().map(|r| r.byte_len()).sum(),
217            Self::Stashed(stashed) => stashed.inline_rows.iter().map(|r| r.byte_len()).sum(),
218            Self::Error(_) | Self::Canceled => 0,
219        }
220    }
221}
222
223/// The error of an unsuccessful peek.
224///
225/// The variant decides what the user sees: a `Dataflow` error keeps the structure the dataflow
226/// produced and so gets the same SQLSTATE constant folding would have assigned, a
227/// `RowIterationLimitExceeded` names a limit the user can raise, and an `Unstructured` error is
228/// reported as an internal error. Prefer the structured variants whenever the source has one.
229#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
230pub enum PeekError {
231    /// An error produced while executing the dataflow, for example evaluating an expression over
232    /// a collection.
233    Dataflow(Box<DataflowError>),
234    /// An error from the peek machinery itself, with no structured form.
235    Unstructured(String),
236    /// A worker examined more rows than `compute_peek_row_iteration_limit` allows.
237    RowIterationLimitExceeded {
238        /// The limit that was in effect, in rows.
239        limit: usize,
240    },
241    /// A worker accumulated more answer bytes than `max_result_size` allows.
242    ResultExceedsMaxSize {
243        /// The ceiling that was in effect, in bytes.
244        max_result_size: usize,
245    },
246}
247
248impl PeekError {
249    /// Constructs an unstructured peek error.
250    pub fn unstructured(message: impl Into<String>) -> Self {
251        Self::Unstructured(message.into())
252    }
253}
254
255impl fmt::Display for PeekError {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        match self {
258            Self::Dataflow(error) => error.fmt(f),
259            Self::Unstructured(error) => f.write_str(error),
260            Self::RowIterationLimitExceeded { limit } => write!(
261                f,
262                "query exceeded the configured row iteration limit of {limit} rows"
263            ),
264            Self::ResultExceedsMaxSize { max_result_size } => write!(
265                f,
266                "result exceeds max size of {}",
267                ByteSize::b(u64::cast_from(*max_result_size))
268            ),
269        }
270    }
271}
272
273impl From<DataflowError> for PeekError {
274    fn from(error: DataflowError) -> Self {
275        Self::Dataflow(Box::new(error))
276    }
277}
278
279impl From<EvalError> for PeekError {
280    fn from(error: EvalError) -> Self {
281        Self::Dataflow(Box::new(error.into()))
282    }
283}
284
285/// Response from a peek whose results have been stashed into persist.
286#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
287pub struct StashedPeekResponse {
288    /// The number of rows stored in response batches. This is the sum of the
289    /// diff values of the contained rows.
290    ///
291    /// This does _NOT_ include rows in `inline_rows`.
292    pub num_rows_batches: u64,
293    /// The sum of the encoded sizes of all batches in this response.
294    pub encoded_size_bytes: usize,
295    /// [RelationDesc] for the rows in these stashed batches of results.
296    pub relation_desc: RelationDesc,
297    /// The [ShardId] under which result batches have been stashed.
298    pub shard_id: ShardId,
299    /// Batches of Rows, must be combined with responses from other workers and
300    /// consolidated before sending back via a client.
301    pub batches: Vec<ProtoBatch>,
302    /// Rows that have not been uploaded to the stash, because their total size
303    /// did not go above the threshold for using the peek stash.
304    ///
305    /// We will have a mix of stashed responses and inline responses because the
306    /// result sizes across different workers can and will vary.
307    pub inline_rows: Vec<RowCollection>,
308}
309
310impl StashedPeekResponse {
311    /// Total count of [mz_repr::Row]s represented by this collection, considering a
312    /// possible `OFFSET` and `LIMIT`.
313    pub fn num_rows(&self, offset: usize, limit: Option<usize>) -> usize {
314        let num_stashed_rows: usize = usize::cast_from(self.num_rows_batches);
315        let num_inline_rows: usize = self.inline_rows.iter().map(|r| r.count()).sum();
316        RowCollection::offset_limit(num_stashed_rows + num_inline_rows, offset, limit)
317    }
318
319    /// The size in bytes of the encoded rows in this result.
320    pub fn size_bytes(&self) -> usize {
321        let inline_size: usize = self.inline_rows.iter().map(|r| r.byte_len()).sum();
322
323        self.encoded_size_bytes + inline_size
324    }
325}
326
327/// Various responses that can be communicated after a COPY TO command.
328#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
329pub enum CopyToResponse {
330    /// Returned number of rows for a successful COPY TO.
331    RowCount(u64),
332    /// Error of an unsuccessful COPY TO.
333    Error(String),
334    /// The COPY TO sink dataflow was dropped.
335    Dropped,
336}
337
338/// Various responses that can be communicated about the progress of a SUBSCRIBE command.
339#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
340pub enum SubscribeResponse {
341    /// A batch of updates over a non-empty interval of time.
342    Batch(SubscribeBatch),
343    /// The SUBSCRIBE dataflow was dropped, leaving updates from this frontier onward unspecified.
344    DroppedAt(Antichain<Timestamp>),
345}
346
347impl SubscribeResponse {
348    /// Converts `self` to an error if a maximum size is exceeded.
349    pub fn to_error_if_exceeds(&mut self, max_result_size: usize) {
350        if let SubscribeResponse::Batch(batch) = self {
351            batch.to_error_if_exceeds(max_result_size);
352        }
353    }
354}
355
356/// A batch of updates for the interval `[lower, upper)`.
357#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
358pub struct SubscribeBatch {
359    /// The lower frontier of the batch of updates.
360    pub lower: Antichain<Timestamp>,
361    /// The upper frontier of the batch of updates.
362    pub upper: Antichain<Timestamp>,
363    /// All updates greater than `lower` and not greater than `upper`.
364    ///
365    /// Each of the update collections is sorted first by time, and then by the ordering specified
366    /// by the order-by of the subscribe. There is no implied ordering between different collections
367    /// of updates in the [Vec].
368    ///
369    /// It's typical to include just a single collection as part of the clusterd's response, but we
370    /// may aggregate different batches together later as we combine results from different workers.
371    ///
372    /// An `Err` variant can be used to indicate e.g. that the size of the updates exceeds internal limits.
373    pub updates: Result<Vec<UpdateCollection>, String>,
374}
375
376impl SubscribeBatch {
377    /// Converts `self` to an error if a maximum size is exceeded.
378    fn to_error_if_exceeds(&mut self, max_result_size: usize) {
379        if let Ok(updates) = &self.updates {
380            let total_size: usize = updates.iter().map(|updates| updates.byte_len()).sum();
381            if total_size > max_result_size {
382                self.updates = Err(format!(
383                    "result exceeds max size of {}",
384                    ByteSize::b(u64::cast_from(max_result_size))
385                ));
386            }
387        }
388    }
389}
390
391/// Status updates replicas can report to the controller.
392#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
393pub enum StatusResponse {
394    /// No status responses are implemented currently, but we're leaving the infrastructure around
395    /// in anticipation of materialize#31246.
396    Placeholder,
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    /// Test to ensure the size of the `ComputeResponse` enum doesn't regress.
404    #[mz_ore::test]
405    fn test_compute_response_size() {
406        assert_eq!(std::mem::size_of::<ComputeResponse>(), 112);
407    }
408}