mz_compute_client/protocol/command.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 commands.
11
12use std::time::Duration;
13
14use mz_cluster_client::client::TryIntoProtocolNonce;
15use mz_cluster_client::params::GrpcClientParameters;
16use mz_compute_types::dataflows::RenderDataflowDescription;
17use mz_dyncfg::ConfigUpdates;
18use mz_expr::RowSetFinishing;
19use mz_ore::tracing::OpenTelemetryContext;
20use mz_persist_types::PersistLocation;
21use mz_repr::{GlobalId, RelationDesc, Row, Timestamp};
22use mz_storage_types::controller::CollectionMetadata;
23use mz_tracing::params::TracingParameters;
24use serde::{Deserialize, Serialize};
25use timely::progress::frontier::Antichain;
26use uuid::Uuid;
27
28use crate::logging::LoggingConfig;
29
30/// Compute protocol commands, sent by the compute controller to replicas.
31///
32/// Command sequences sent by the compute controller must be valid according to the [Protocol
33/// Stages].
34///
35/// [Protocol Stages]: super#protocol-stages
36#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
37pub enum ComputeCommand {
38 /// `Hello` is the first command sent to a replica after a connection was established. It
39 /// provides the replica with meta information about the connection.
40 ///
41 /// This command is special in that it is broadcast to all workers of a multi-worker replica.
42 /// All subsequent commands, except `UpdateConfiguration`, are only sent to the first worker,
43 /// which then distributes them to the other workers using a dataflow.
44 Hello {
45 /// A nonce unique to the current iteration of the compute protocol.
46 ///
47 /// The nonce allows identifying different iterations of the compute protocol. When the
48 /// compute controller connects to a replica, it must send a nonce that is different from
49 /// all nonces it sent to the same replica on previous connections. Multi-worker replicas
50 /// should use the nonce to ensure that their individual workers agree on which protocol
51 /// iteration they are in.
52 nonce: Uuid,
53 },
54
55 /// `CreateInstance` must be sent after `Hello` to complete the [Creation Stage] of the compute
56 /// protocol. Unlike `Hello`, it is only sent to the first worker of the replica, and then
57 /// distributed through the timely runtime. `CreateInstance` instructs the replica to
58 /// initialize its state to a point where it is ready to start maintaining dataflows.
59 ///
60 /// Upon receiving a `CreateInstance` command, the replica must further initialize logging
61 /// dataflows according to the given [`LoggingConfig`].
62 ///
63 /// [Creation Stage]: super#creation-stage
64 CreateInstance(Box<InstanceConfig>),
65
66 /// `InitializationComplete` informs the replica about the end of the [Initialization Stage].
67 /// Upon receiving this command, the replica should perform a reconciliation process, to ensure
68 /// its dataflow state matches the state requested by the computation commands it received
69 /// previously. The replica must now start sending responses to commands received previously,
70 /// if it opted to defer them during the [Initialization Stage].
71 ///
72 /// [Initialization Stage]: super#initialization-stage
73 InitializationComplete,
74
75 /// `UpdateConfiguration` instructs the replica to update its configuration, according to the
76 /// given [`ComputeParameters`].
77 ///
78 /// This command is special in that, like `Hello`, it is broadcast to all workers of the
79 /// replica. However, unlike `Hello`, it is ignored by all workers except the first one, which
80 /// distributes the command to the other workers through the timely runtime.
81 /// `UpdateConfiguration` commands are broadcast only to allow the intermediary parts of the
82 /// networking fabric to observe them and learn of configuration updates.
83 ///
84 /// Parameter updates transmitted through this command must be applied by the replica as soon
85 /// as it receives the command, and they must be applied globally to all replica state, even
86 /// dataflows and pending peeks that were created before the parameter update. This property
87 /// allows the replica to hoist `UpdateConfiguration` commands during reconciliation.
88 ///
89 /// Configuration parameters that should not be applied globally, but only to specific
90 /// dataflows or peeks, should be added to the [`RenderDataflowDescription`] or [`Peek`] types,
91 /// rather than as [`ComputeParameters`].
92 UpdateConfiguration(Box<ComputeParameters>),
93
94 /// `CreateDataflow` instructs the replica to create a dataflow according to the given
95 /// [`RenderDataflowDescription`].
96 ///
97 /// The [`RenderDataflowDescription`] must have the following properties:
98 ///
99 /// * Dataflow imports are valid:
100 /// * Imported storage collections specified in [`source_imports`] exist and are readable by
101 /// the compute replica.
102 /// * Imported indexes specified in [`index_imports`] have been created on the replica
103 /// previously, by previous `CreateDataflow` commands.
104 /// * Dataflow imports are readable at the specified [`as_of`]. In other words: The `since`s of
105 /// imported collections are not beyond the dataflow [`as_of`].
106 /// * Dataflow exports have unique IDs, i.e., the IDs of exports from dataflows a replica is
107 /// instructed to create do not repeat (within a single protocol iteration).
108 /// * The dataflow objects defined in [`objects_to_build`] are topologically ordered according
109 /// to the dependency relation.
110 ///
111 /// A dataflow description that violates any of the above properties can cause the replica to
112 /// exhibit undefined behavior, such as panicking or production of incorrect results. A replica
113 /// should prefer panicking over producing incorrect results.
114 ///
115 /// After receiving a `CreateDataflow` command, if the created dataflow exports indexes or
116 /// storage sinks, the replica must produce [`Frontiers`] responses that report the
117 /// advancement of the frontiers of these compute collections.
118 ///
119 /// After receiving a `CreateDataflow` command, if the created dataflow exports subscribes, the
120 /// replica must produce [`SubscribeResponse`]s that report the progress and results of the
121 /// subscribes.
122 ///
123 /// After receiving a `CreateDataflow` command, if the created dataflow exports copy-to sinks,
124 /// the replica must produce [`CopyToResponse`]s that report the results and completion of the
125 /// copy-to sinks.
126 ///
127 /// After receiving a `CreateDataflow` command, if the created dataflow exports a persist sink
128 /// (materialized view), the dataflow must not write any data to the sink until the replica
129 /// receives an `AllowWrites` command for the corresponding collection.
130 ///
131 /// The replica may create the dataflow in a suspended state and defer starting the computation
132 /// until it receives a corresponding `Schedule` command. Thus, to ensure dataflow execution,
133 /// the compute controller should eventually send a `Schedule` command for each sent
134 /// `CreateDataflow` command.
135 ///
136 /// [`objects_to_build`]: RenderDataflowDescription::objects_to_build
137 /// [`source_imports`]: RenderDataflowDescription::source_imports
138 /// [`index_imports`]: RenderDataflowDescription::index_imports
139 /// [`as_of`]: RenderDataflowDescription::as_of
140 /// [`Frontiers`]: super::response::ComputeResponse::Frontiers
141 /// [`SubscribeResponse`]: super::response::ComputeResponse::SubscribeResponse
142 /// [`CopyToResponse`]: super::response::ComputeResponse::CopyToResponse
143 CreateDataflow(Box<RenderDataflowDescription<CollectionMetadata>>),
144
145 /// `Schedule` allows the replica to start computation for a compute collection.
146 ///
147 /// It is invalid to send a `Schedule` command that references a collection that was not
148 /// created by a corresponding `CreateDataflow` command before. Doing so may cause the replica
149 /// to exhibit undefined behavior.
150 ///
151 /// It is also invalid to send a `Schedule` command that references a collection that has,
152 /// through an `AllowCompaction` command, been allowed to compact to the empty frontier before.
153 Schedule(GlobalId),
154
155 /// `AllowWrites` allows the replica to start writing external state for a compute collection.
156 ///
157 /// It is invalid to send an `AllowWrites` command that references a compute collection
158 /// that was not created by a corresponding `CreateDataflow` command before. Doing so may cause
159 /// the replica to exhibit undefined behavior.
160 ///
161 /// It is also invalid to send an `AllowWrites` command for a compute collection that has
162 /// been allowed to compact to the empty frontier through an `AllowCompaction` command before.
163 ///
164 /// NOTE: This command only allows writes to persist. Other external state, such as copy-to
165 /// sinks, do not need an explicit permission to write.
166 ///
167 /// NOTE: We don't have a protocol in place that allows writes only after a
168 /// certain, controller-determined, timestamp. Such a protocol would allow
169 /// tighter control and could allow the instance to avoid work. However, it
170 /// is more work to put in place the logic for that so we leave it as future
171 /// work for now.
172 ///
173 /// Note: The `AllowWrites` command is per collection, but a dataflow could host multiple
174 /// collections. We're accepting this impedance mismatch for now.
175 AllowWrites(GlobalId),
176
177 /// `AllowCompaction` informs the replica about the relaxation of external read capabilities on
178 /// a compute collection exported by one of the replica's dataflows.
179 ///
180 /// The command names a collection and provides a frontier after which accumulations must be
181 /// correct. The replica gains the liberty of compacting the corresponding maintained trace up
182 /// through that frontier.
183 ///
184 /// It is invalid to send an `AllowCompaction` command that references a compute collection
185 /// that was not created by a corresponding `CreateDataflow` command before. Doing so may cause
186 /// the replica to exhibit undefined behavior.
187 ///
188 /// The `AllowCompaction` command only informs about external read requirements, not internal
189 /// ones. The replica is responsible for ensuring that internal requirements are fulfilled at
190 /// all times, so local dataflow inputs are not compacted beyond times at which they are still
191 /// being read from.
192 ///
193 /// The read frontiers transmitted through `AllowCompaction`s may be beyond the corresponding
194 /// collections' current `upper` frontiers. This signals that external readers are not
195 /// interested in times up to the specified new read frontiers. Consequently, an empty read
196 /// frontier signals that external readers are not interested in updates from the corresponding
197 /// collection ever again, so the collection is not required anymore.
198 ///
199 /// Sending an `AllowCompaction` command with the empty frontier is the canonical way to drop
200 /// compute collections.
201 ///
202 /// A replica that receives an `AllowCompaction` command with the empty frontier must
203 /// eventually respond with [`Frontiers`] responses reporting empty frontiers for the
204 /// same collection. ([#16271])
205 ///
206 /// [`Frontiers`]: super::response::ComputeResponse::Frontiers
207 /// [#16271]: https://github.com/MaterializeInc/database-issues/issues/4699
208 AllowCompaction {
209 /// TODO(database-issues#7533): Add documentation.
210 id: GlobalId,
211 /// TODO(database-issues#7533): Add documentation.
212 frontier: Antichain<Timestamp>,
213 },
214
215 /// `Peek` instructs the replica to perform a peek on a collection: either an index or a
216 /// Persist-backed collection.
217 ///
218 /// The [`Peek`] description must have the following properties:
219 ///
220 /// * If targeting an index, it has previously been created by a corresponding `CreateDataflow`
221 /// command. (If targeting a persist collection, that collection should exist.)
222 /// * The [`Peek::uuid`] is unique, i.e., the UUIDs of peeks a replica gets instructed to
223 /// perform do not repeat (within a single protocol iteration).
224 ///
225 /// A [`Peek`] description that violates any of the above properties can cause the replica to
226 /// exhibit undefined behavior.
227 ///
228 /// Specifying a [`Peek::timestamp`] that is less than the target index's `since` frontier does
229 /// not provoke undefined behavior. Instead, the replica must produce a [`PeekResponse::Error`]
230 /// in response.
231 ///
232 /// After receiving a `Peek` command, the replica must eventually produce a single
233 /// [`PeekResponse`]:
234 ///
235 /// * For peeks that were not cancelled: [`Rows`], [`Stashed`], or [`Error`].
236 /// * For peeks that were cancelled: any response above, or [`Canceled`].
237 ///
238 /// [`PeekResponse`]: super::response::PeekResponse
239 /// [`PeekResponse::Error`]: super::response::PeekResponse::Error
240 /// [`Rows`]: super::response::PeekResponse::Rows
241 /// [`Stashed`]: super::response::PeekResponse::Stashed
242 /// [`Error`]: super::response::PeekResponse::Error
243 /// [`Canceled`]: super::response::PeekResponse::Canceled
244 Peek(Box<Peek>),
245
246 /// `CancelPeek` instructs the replica to cancel the identified pending peek.
247 ///
248 /// It is invalid to send a `CancelPeek` command that references a peek that was not created
249 /// by a corresponding `Peek` command before. Doing so may cause the replica to exhibit
250 /// undefined behavior.
251 ///
252 /// If a replica cancels a peek in response to a `CancelPeek` command, it must respond with a
253 /// [`PeekResponse::Canceled`]. The replica may also decide to fulfill the peek instead and
254 /// return a different [`PeekResponse`], or it may already have returned a response to the
255 /// specified peek. In these cases it must *not* return another [`PeekResponse`].
256 ///
257 /// [`PeekResponse`]: super::response::PeekResponse
258 /// [`PeekResponse::Canceled`]: super::response::PeekResponse::Canceled
259 CancelPeek {
260 /// The identifier of the peek request to cancel.
261 ///
262 /// This Value must match a [`Peek::uuid`] value transmitted in a previous `Peek` command.
263 uuid: Uuid,
264 },
265}
266
267/// Configuration for a replica, passed with the `CreateInstance`. Replicas should halt
268/// if the controller attempt to reconcile them with different values
269/// for anything in this struct.
270#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
271pub struct InstanceConfig {
272 /// Specification of introspection logging.
273 pub logging: LoggingConfig,
274 /// The offset relative to the replica startup at which it should expire. None disables feature.
275 pub expiration_offset: Option<Duration>,
276 /// The persist location where we can stash large peek results.
277 pub peek_stash_persist_location: PersistLocation,
278 /// Whether arrangements created by this replica use dictionary compression.
279 ///
280 /// Captured from `enable_arrangement_dictionary_compression_alpha` when the replica is created and
281 /// held fixed for the replica's lifetime, so flipping the flag only affects replicas created
282 /// afterwards rather than retroactively changing arrangements across the environment.
283 pub arrangement_dictionary_compression: bool,
284 /// A snapshot of the controller's dynamic configuration at replica creation, including any
285 /// replica-scoped overrides.
286 ///
287 /// The replica applies this to its worker configuration before running create-time setup, so
288 /// that work performed while handling `CreateInstance` (for example rendering introspection
289 /// dataflows) observes controller-synced values rather than dyncfg defaults. The values are
290 /// also delivered through the subsequent `UpdateConfiguration`, which applies globally and
291 /// keeps the configuration current for the replica's lifetime. An empty snapshot leaves the
292 /// worker at its defaults until that first `UpdateConfiguration`.
293 pub initial_config: ConfigUpdates,
294}
295
296impl InstanceConfig {
297 /// Check if the configuration is compatible with another configuration. This is true iff the
298 /// logging configuration is equivalent, and the other configuration (non-strictly) strengthens
299 /// the expiration offset.
300 ///
301 /// We consider a stricter offset compatible, which allows us to strengthen the value without
302 /// forcing replica restarts. However, it also means that replicas will only pick up the new
303 /// value after a restart.
304 ///
305 /// Dictionary compression is intentionally excluded from this check: it is captured at replica
306 /// creation, so a change is always compatible and a running replica keeps the value it was
307 /// created with (a new value is only picked up by replicas created afterwards).
308 ///
309 /// The initial config snapshot is likewise excluded: it carries dyncfg values that apply
310 /// globally and are kept current through `UpdateConfiguration`, so a difference across
311 /// reconnects is expected and does not require a restart.
312 pub fn compatible_with(&self, other: &InstanceConfig) -> bool {
313 // Destructure to protect against adding fields in the future.
314 let InstanceConfig {
315 logging: self_logging,
316 expiration_offset: self_offset,
317 peek_stash_persist_location: self_peek_stash_persist_location,
318 // Captured at replica creation; intentionally not part of compatibility (see above).
319 arrangement_dictionary_compression: _,
320 // Globally-applied dyncfg snapshot, intentionally not part of compatibility (see above).
321 initial_config: _,
322 } = self;
323 let InstanceConfig {
324 logging: other_logging,
325 expiration_offset: other_offset,
326 peek_stash_persist_location: other_peek_stash_persist_location,
327 arrangement_dictionary_compression: _,
328 initial_config: _,
329 } = other;
330
331 // Logging is compatible if exactly the same.
332 let logging_compatible = self_logging == other_logging;
333
334 // The offsets are compatible of other_offset is less than or equal to self_offset, i.e., it
335 // is a smaller offset and strengthens the offset.
336 let self_offset = Antichain::from_iter(*self_offset);
337 let other_offset = Antichain::from_iter(*other_offset);
338 let offset_compatible = timely::PartialOrder::less_equal(&other_offset, &self_offset);
339
340 let persist_location_compatible =
341 self_peek_stash_persist_location == other_peek_stash_persist_location;
342
343 logging_compatible && offset_compatible && persist_location_compatible
344 }
345}
346
347/// Compute instance configuration parameters.
348///
349/// Parameters can be set (`Some`) or unset (`None`).
350/// Unset parameters should be interpreted to mean "use the previous value".
351#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
352pub struct ComputeParameters {
353 /// An optional arbitrary string that describes the class of the workload
354 /// this compute instance is running (e.g., `production` or `staging`).
355 ///
356 /// When `Some(x)`, a `workload_class=x` label is applied to all metrics
357 /// exported by the metrics registry associated with the compute instance.
358 pub workload_class: Option<Option<String>>,
359 /// The maximum allowed size in bytes for results of peeks and subscribes.
360 ///
361 /// Peeks and subscribes that would return results larger than this maximum return the
362 /// respective error responses instead:
363 /// * [`PeekResponse::Rows`] is replaced by [`PeekResponse::Error`].
364 /// * The [`SubscribeBatch::updates`] field is populated with an [`Err`] value.
365 ///
366 /// [`PeekResponse::Rows`]: super::response::PeekResponse::Rows
367 /// [`PeekResponse::Error`]: super::response::PeekResponse::Error
368 /// [`SubscribeBatch::updates`]: super::response::SubscribeBatch::updates
369 pub max_result_size: Option<u64>,
370 /// Tracing configuration.
371 pub tracing: TracingParameters,
372 /// gRPC client configuration.
373 pub grpc_client: GrpcClientParameters,
374
375 /// Config updates for components migrated to `mz_dyncfg`.
376 pub dyncfg_updates: ConfigUpdates,
377}
378
379impl ComputeParameters {
380 /// Update the parameter values with the set ones from `other`.
381 pub fn update(&mut self, other: ComputeParameters) {
382 let ComputeParameters {
383 workload_class,
384 max_result_size,
385 tracing,
386 grpc_client,
387 dyncfg_updates,
388 } = other;
389
390 if workload_class.is_some() {
391 self.workload_class = workload_class;
392 }
393 if max_result_size.is_some() {
394 self.max_result_size = max_result_size;
395 }
396
397 self.tracing.update(tracing);
398 self.grpc_client.update(grpc_client);
399
400 self.dyncfg_updates.extend(dyncfg_updates);
401 }
402
403 /// Return whether all parameters are unset.
404 pub fn all_unset(&self) -> bool {
405 *self == Self::default()
406 }
407}
408
409/// Metadata specific to the peek variant.
410#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
411pub enum PeekTarget {
412 /// This peek is against an index. Since this should be held in memory on
413 /// the target cluster, no additional coordinates are necessary.
414 Index {
415 /// The id of the (possibly transient) index.
416 id: GlobalId,
417 },
418 /// This peek is against a Persist collection.
419 Persist {
420 /// The id of the backing Persist collection.
421 id: GlobalId,
422 /// The identifying metadata of the Persist shard.
423 metadata: CollectionMetadata,
424 },
425}
426
427impl PeekTarget {
428 /// Returns the ID of the peeked collection.
429 pub fn id(&self) -> GlobalId {
430 match self {
431 Self::Index { id } => *id,
432 Self::Persist { id, .. } => *id,
433 }
434 }
435}
436
437/// Peek a collection, either in an arrangement or Persist.
438///
439/// This request elicits data from the worker, by naming the
440/// collection and some actions to apply to the results before
441/// returning them.
442///
443/// The `timestamp` member must be valid for the arrangement that
444/// is referenced by `id`. This means that `AllowCompaction` for
445/// this arrangement should not pass `timestamp` before this command.
446/// Subsequent commands may arbitrarily compact the arrangements;
447/// the dataflow runners are responsible for ensuring that they can
448/// correctly answer the `Peek`.
449#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
450pub struct Peek {
451 /// Target-specific metadata.
452 pub target: PeekTarget,
453 /// The relation description for the rows returned by this peek, before
454 /// applying the [RowSetFinishing] but _after_ applying the given
455 /// `map_filter_project`.
456 pub result_desc: RelationDesc,
457 /// If `Some`, then look up only the given keys from the collection (instead of a full scan).
458 /// The vector is never empty.
459 pub literal_constraints: Option<Vec<Row>>,
460 /// The identifier of this peek request.
461 ///
462 /// Used in responses and cancellation requests.
463 pub uuid: Uuid,
464 /// The logical timestamp at which the collection is queried.
465 pub timestamp: Timestamp,
466 /// Actions to apply to the result set before returning them.
467 pub finishing: RowSetFinishing,
468 /// Linear operation to apply in-line on each result.
469 pub map_filter_project: mz_expr::SafeMfpPlan,
470 /// An `OpenTelemetryContext` to forward trace information along
471 /// to the compute worker to allow associating traces between
472 /// the compute controller and the compute worker.
473 pub otel_ctx: OpenTelemetryContext,
474}
475
476impl TryIntoProtocolNonce for ComputeCommand {
477 fn try_into_protocol_nonce(self) -> Result<Uuid, Self> {
478 match self {
479 ComputeCommand::Hello { nonce } => Ok(nonce),
480 cmd => Err(cmd),
481 }
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 /// Test to ensure the size of the `ComputeCommand` enum doesn't regress.
490 #[mz_ore::test]
491 fn test_compute_command_size() {
492 assert_eq!(std::mem::size_of::<ComputeCommand>(), 40);
493 }
494}