mz_storage_client/controller.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//! A controller that provides an interface to the storage layer.
11//!
12//! The storage controller curates the creation of sources, the progress of readers through these collections,
13//! and their eventual dropping and resource reclamation.
14//!
15//! The storage controller can be viewed as a partial map from `GlobalId` to collection. It is an error to
16//! use an identifier before it has been "created" with `create_source()`. Once created, the controller holds
17//! a read capability for each source, which is manipulated with `update_read_capabilities()`.
18//! Eventually, the source is dropped with either `drop_sources()` or by allowing compaction to the
19//! empty frontier.
20
21use std::collections::{BTreeMap, BTreeSet};
22use std::fmt::Debug;
23use std::future::Future;
24use std::num::NonZeroI64;
25use std::pin::Pin;
26use std::sync::Arc;
27use std::time::Duration;
28
29use async_trait::async_trait;
30use chrono::{DateTime, Utc};
31use differential_dataflow::lattice::Lattice;
32use mz_cluster_client::ReplicaId;
33use mz_cluster_client::client::ClusterReplicaLocation;
34use mz_controller_types::dyncfgs::WALLCLOCK_LAG_HISTOGRAM_PERIOD_INTERVAL;
35use mz_dyncfg::ConfigSet;
36use mz_ore::soft_panic_or_log;
37use mz_persist_client::batch::ProtoBatch;
38use mz_persist_types::{Codec64, ShardId};
39use mz_repr::adt::interval::Interval;
40use mz_repr::adt::timestamp::CheckedTimestamp;
41use mz_repr::{Datum, Diff, GlobalId, RelationDesc, RelationVersion, Row, Timestamp};
42use mz_storage_types::configuration::StorageConfiguration;
43use mz_storage_types::connections::inline::InlinedConnection;
44use mz_storage_types::controller::{CollectionMetadata, StorageError};
45use mz_storage_types::errors::CollectionMissing;
46use mz_storage_types::instances::StorageInstanceId;
47use mz_storage_types::oneshot_sources::{OneshotIngestionRequest, OneshotResultCallback};
48use mz_storage_types::parameters::StorageParameters;
49use mz_storage_types::read_holds::ReadHold;
50use mz_storage_types::read_policy::ReadPolicy;
51use mz_storage_types::sinks::{StorageSinkConnection, StorageSinkDesc};
52use mz_storage_types::sources::{
53 GenericSourceConnection, IngestionDescription, SourceDesc, SourceExportDataConfig,
54 SourceExportDetails, Timeline,
55};
56use serde::{Deserialize, Serialize};
57use timely::progress::Antichain;
58use timely::progress::frontier::MutableAntichain;
59use tokio::sync::{mpsc, oneshot};
60
61use crate::client::{AppendOnlyUpdate, StatusUpdate, TableData};
62use crate::statistics::WebhookStatistics;
63
64#[derive(
65 Clone,
66 Copy,
67 Debug,
68 Serialize,
69 Deserialize,
70 Eq,
71 PartialEq,
72 Hash,
73 PartialOrd,
74 Ord
75)]
76pub enum IntrospectionType {
77 /// We're not responsible for appending to this collection automatically, but we should
78 /// automatically bump the write frontier from time to time.
79 SinkStatusHistory,
80 SourceStatusHistory,
81 ShardMapping,
82
83 Frontiers,
84 ReplicaFrontiers,
85
86 ReplicaStatusHistory,
87 ReplicaMetricsHistory,
88 WallclockLagHistory,
89 WallclockLagHistogram,
90
91 // Note that this single-shard introspection source will be changed to per-replica,
92 // once we allow multiplexing multiple sources/sinks on a single cluster.
93 StorageSourceStatistics,
94 StorageSinkStatistics,
95
96 // The below are for statement logging.
97 StatementExecutionHistory,
98 SessionHistory,
99 PreparedStatementHistory,
100 SqlText,
101 // For statement lifecycle logging, which is closely related
102 // to statement logging
103 StatementLifecycleHistory,
104
105 // Collections written by the compute controller.
106 ComputeDependencies,
107 ComputeOperatorHydrationStatus,
108 ComputeMaterializedViewRefreshes,
109 ComputeErrorCounts,
110 ComputeHydrationTimes,
111 ComputeObjectArrangementSizes,
112
113 // Written by the Adapter for tracking AWS PrivateLink Connection Status History
114 PrivatelinkConnectionStatusHistory,
115}
116
117/// Describes how data is written to the collection.
118#[derive(Clone, Debug, Eq, PartialEq)]
119pub enum DataSource {
120 /// Ingest data from some external source.
121 Ingestion(IngestionDescription),
122 /// This source receives its data from the identified ingestion,
123 /// from an external object identified using `SourceExportDetails`.
124 ///
125 /// The referenced ingestion must be created before all of its exports.
126 IngestionExport {
127 ingestion_id: GlobalId,
128 details: SourceExportDetails,
129 data_config: SourceExportDataConfig,
130 },
131 /// Data comes from introspection sources, which the controller itself is
132 /// responsible for generating.
133 Introspection(IntrospectionType),
134 /// Data comes from the source's remapping/reclock operator.
135 Progress,
136 /// Data comes from external HTTP requests pushed to Materialize.
137 Webhook,
138 /// The adapter layer appends timestamped data, i.e. it is a `TABLE`.
139 Table,
140 /// This source's data does not need to be managed by the storage
141 /// controller, e.g. it's a materialized view or the catalog collection.
142 Other,
143 /// This collection is the output collection of a sink.
144 Sink { desc: ExportDescription },
145}
146
147/// Describes a request to create a source.
148#[derive(Clone, Debug, Eq, PartialEq)]
149pub struct CollectionDescription {
150 /// The schema of this collection
151 pub desc: RelationDesc,
152 /// The source of this collection's data.
153 pub data_source: DataSource,
154 /// An optional frontier to which the collection's `since` should be advanced.
155 pub since: Option<Antichain<Timestamp>>,
156 /// The timeline of the source. Absent for materialized views, etc.
157 pub timeline: Option<Timeline>,
158 /// The primary of this collections.
159 ///
160 /// Multiple storage collections can point to the same persist shard,
161 /// possibly with different schemas. In such a configuration, we select one
162 /// of the involved collections as the primary, who "owns" the persist
163 /// shard. All other involved collections have a dependency on the primary.
164 pub primary: Option<GlobalId>,
165}
166
167impl CollectionDescription {
168 /// Create a CollectionDescription for [`DataSource::Other`].
169 pub fn for_other(desc: RelationDesc, since: Option<Antichain<Timestamp>>) -> Self {
170 Self {
171 desc,
172 data_source: DataSource::Other,
173 since,
174 timeline: None,
175 primary: None,
176 }
177 }
178
179 /// Create a CollectionDescription for a table.
180 pub fn for_table(desc: RelationDesc) -> Self {
181 Self {
182 desc,
183 data_source: DataSource::Table,
184 since: None,
185 timeline: Some(Timeline::EpochMilliseconds),
186 primary: None,
187 }
188 }
189}
190
191#[derive(Clone, Debug, Eq, PartialEq)]
192pub struct ExportDescription<T = mz_repr::Timestamp> {
193 pub sink: StorageSinkDesc<(), T>,
194 /// The ID of the instance in which to install the export.
195 pub instance_id: StorageInstanceId,
196}
197
198#[derive(Debug)]
199pub enum Response {
200 FrontierUpdates(Vec<(GlobalId, Antichain<Timestamp>)>),
201}
202
203/// Metadata that the storage controller must know to properly handle the life
204/// cycle of creating and dropping collections.
205///
206/// This data should be kept consistent with the state modified using
207/// [`StorageTxn`].
208///
209/// n.b. the "txn WAL shard" is also metadata that's persisted, but if we
210/// included it in this struct it would never be read.
211#[derive(Debug, Clone, Serialize, Default)]
212pub struct StorageMetadata {
213 #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
214 pub collection_metadata: BTreeMap<GlobalId, ShardId>,
215 pub unfinalized_shards: BTreeSet<ShardId>,
216}
217
218impl StorageMetadata {
219 pub fn get_collection_shard(&self, id: GlobalId) -> Result<ShardId, StorageError> {
220 let shard_id = self
221 .collection_metadata
222 .get(&id)
223 .ok_or(StorageError::IdentifierMissing(id))?;
224
225 Ok(*shard_id)
226 }
227}
228
229/// Provides an interface for the storage controller to read and write data that
230/// is recorded elsewhere.
231///
232/// Data written to the implementor of this trait should make a consistent view
233/// of the data available through [`StorageMetadata`].
234#[async_trait]
235pub trait StorageTxn {
236 /// Retrieve all of the visible storage metadata.
237 ///
238 /// The value of this map should be treated as opaque.
239 fn get_collection_metadata(&self) -> BTreeMap<GlobalId, ShardId>;
240
241 /// Add new storage metadata for a collection.
242 ///
243 /// Subsequent calls to [`StorageTxn::get_collection_metadata`] must include
244 /// this data.
245 fn insert_collection_metadata(
246 &mut self,
247 s: BTreeMap<GlobalId, ShardId>,
248 ) -> Result<(), StorageError>;
249
250 /// Remove the metadata associated with the identified collections.
251 ///
252 /// Subsequent calls to [`StorageTxn::get_collection_metadata`] must not
253 /// include these keys.
254 fn delete_collection_metadata(&mut self, ids: BTreeSet<GlobalId>) -> Vec<(GlobalId, ShardId)>;
255
256 /// Retrieve the durable set of shards recorded as unfinalized.
257 ///
258 /// Entries must be reconciled with active collection metadata before
259 /// finalization because stale entries can refer to active collections.
260 fn get_unfinalized_shards(&self) -> BTreeSet<ShardId>;
261
262 /// Insert the specified values as unfinalized shards.
263 fn insert_unfinalized_shards(&mut self, s: BTreeSet<ShardId>) -> Result<(), StorageError>;
264
265 /// Removes the specified shards from the unfinalized shard collection.
266 ///
267 /// Missing shards are ignored. This only updates transaction metadata and
268 /// does not finalize the underlying Persist shards.
269 fn remove_unfinalized_shards(&mut self, shards: BTreeSet<ShardId>);
270
271 /// Get the txn WAL shard for this environment if it exists.
272 fn get_txn_wal_shard(&self) -> Option<ShardId>;
273
274 /// Store the specified shard as the environment's txn WAL shard.
275 ///
276 /// The implementor should error if the shard is already specified.
277 fn write_txn_wal_shard(&mut self, shard: ShardId) -> Result<(), StorageError>;
278}
279
280pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
281
282/// A predicate for a `Row` filter.
283pub type RowPredicate = Box<dyn Fn(&Row) -> bool + Send + Sync>;
284
285/// High-level write operations applicable to storage collections.
286pub enum StorageWriteOp {
287 /// Append a set of rows with specified multiplicities.
288 ///
289 /// The multiplicities may be negative, so an `Append` operation can perform
290 /// both insertions and retractions.
291 Append { updates: Vec<(Row, Diff)> },
292 /// Delete all rows matching the given predicate.
293 Delete { filter: RowPredicate },
294}
295
296impl StorageWriteOp {
297 /// Returns whether this operation appends an empty set of updates.
298 pub fn is_empty_append(&self) -> bool {
299 match self {
300 Self::Append { updates } => updates.is_empty(),
301 Self::Delete { .. } => false,
302 }
303 }
304}
305
306/// Metadata required to register a table with the txns shard.
307#[derive(Debug, Clone)]
308pub struct TableRegistration {
309 pub id: GlobalId,
310 pub data_shard: ShardId,
311 pub relation_desc: RelationDesc,
312}
313
314/// Queues txns-shard operations on the storage table worker.
315///
316/// The adapter's group committer is the sole runtime caller, preserving FIFO order across appends,
317/// registrations, and forgets. On [`StorageError::InvalidUppers`], the writable implementation
318/// restores its bookkeeping and the caller must retry at a fresh timestamp.
319pub trait TableWriteHandle: Debug + Send + Sync {
320 /// Appends `commands` at `write_ts` and advances all registered tables to `advance_to`.
321 fn append(
322 &self,
323 write_ts: Timestamp,
324 advance_to: Timestamp,
325 commands: Vec<(GlobalId, Vec<TableData>)>,
326 ) -> oneshot::Receiver<Result<(), StorageError>>;
327
328 /// Registers `tables` at `register_ts`.
329 fn register(
330 &self,
331 register_ts: Timestamp,
332 tables: Vec<TableRegistration>,
333 ) -> oneshot::Receiver<Result<(), StorageError>>;
334
335 /// Forgets registered `ids` at `forget_ts`, ignoring unknown IDs.
336 fn forget(
337 &self,
338 forget_ts: Timestamp,
339 ids: Vec<GlobalId>,
340 ) -> oneshot::Receiver<Result<(), StorageError>>;
341}
342
343#[async_trait(?Send)]
344pub trait StorageController: Debug {
345 /// Marks the end of any initialization commands.
346 ///
347 /// The implementor may wait for this method to be called before implementing prior commands,
348 /// and so it is important for a user to invoke this method as soon as it is comfortable.
349 /// This method can be invoked immediately, at the potential expense of performance.
350 fn initialization_complete(&mut self);
351
352 /// Update storage configuration with new parameters.
353 fn update_parameters(&mut self, config_params: StorageParameters);
354
355 /// Get the current configuration, including parameters updated with `update_parameters`.
356 fn config(&self) -> &StorageConfiguration;
357
358 /// Returns the [CollectionMetadata] of the collection identified by `id`.
359 fn collection_metadata(&self, id: GlobalId) -> Result<CollectionMetadata, CollectionMissing>;
360
361 /// Returns `true` iff the given collection/ingestion has been hydrated.
362 ///
363 /// For this check, zero-replica clusters are always considered hydrated.
364 /// Their collections would never normally be considered hydrated but it's
365 /// clearly intentional that they have no replicas.
366 fn collection_hydrated(&self, collection_id: GlobalId) -> Result<bool, StorageError>;
367
368 /// Returns `true` if each non-transient, non-excluded collection is
369 /// hydrated on at least one of the provided replicas.
370 ///
371 /// Collections that are not scheduled on any of the provided replicas do
372 /// not count against hydration: a single-replica source keeps running on
373 /// its current replica and can never hydrate on a replica it is not
374 /// scheduled on.
375 ///
376 /// If no replicas are provided, this checks for hydration on _any_ replica.
377 ///
378 /// This also returns `true` in case this cluster does not have any
379 /// replicas.
380 fn collections_hydrated_on_replicas(
381 &self,
382 target_replica_ids: Option<Vec<ReplicaId>>,
383 target_cluster_ids: &StorageInstanceId,
384 exclude_collections: &BTreeSet<GlobalId>,
385 ) -> Result<bool, StorageError>;
386
387 /// Returns the since/upper frontiers of the identified collection.
388 fn collection_frontiers(
389 &self,
390 id: GlobalId,
391 ) -> Result<(Antichain<Timestamp>, Antichain<Timestamp>), CollectionMissing>;
392
393 /// Returns the since/upper frontiers of the identified collections.
394 ///
395 /// Having a method that returns both frontiers at the same time, for all
396 /// requested collections, ensures that we can get a consistent "snapshot"
397 /// of collection state. If we had separate methods instead, and/or would
398 /// allow getting frontiers for collections one at a time, it could happen
399 /// that collection state changes concurrently, while information is
400 /// gathered.
401 fn collections_frontiers(
402 &self,
403 id: Vec<GlobalId>,
404 ) -> Result<Vec<(GlobalId, Antichain<Timestamp>, Antichain<Timestamp>)>, CollectionMissing>;
405
406 /// Acquire an iterator over [CollectionMetadata] for all active
407 /// collections.
408 ///
409 /// A collection is "active" when it has a non-empty frontier of read
410 /// capabilities.
411 fn active_collection_metadatas(&self) -> Vec<(GlobalId, CollectionMetadata)>;
412
413 /// Returns the IDs of ingestion exports running on the given instance. This
414 /// includes the ingestion itself, if any, and running source tables (aka.
415 /// subsources).
416 fn active_ingestion_exports(
417 &self,
418 instance_id: StorageInstanceId,
419 ) -> Box<dyn Iterator<Item = &GlobalId> + '_>;
420
421 /// Checks whether a collection exists under the given `GlobalId`. Returns
422 /// an error if the collection does not exist.
423 fn check_exists(&self, id: GlobalId) -> Result<(), StorageError>;
424
425 /// Creates a storage instance with the specified ID.
426 ///
427 /// A storage instance can have zero or one replicas. The instance is
428 /// created with zero replicas.
429 ///
430 /// Panics if a storage instance with the given ID already exists.
431 fn create_instance(&mut self, id: StorageInstanceId, workload_class: Option<String>);
432
433 /// Drops the storage instance with the given ID.
434 ///
435 /// If you call this method while the storage instance has a replica
436 /// attached, that replica will be leaked. Call `drop_replica` first.
437 ///
438 /// Panics if a storage instance with the given ID does not exist.
439 fn drop_instance(&mut self, id: StorageInstanceId);
440
441 /// Updates a storage instance's workload class.
442 fn update_instance_workload_class(
443 &mut self,
444 id: StorageInstanceId,
445 workload_class: Option<String>,
446 );
447
448 /// Connects the storage instance to the specified replica.
449 ///
450 /// If the storage instance is already attached to a replica, communication
451 /// with that replica is severed in favor of the new replica.
452 ///
453 /// In the future, this API will be adjusted to support active replication
454 /// of storage instances (i.e., multiple replicas attached to a given
455 /// storage instance).
456 fn connect_replica(
457 &mut self,
458 instance_id: StorageInstanceId,
459 replica_id: ReplicaId,
460 location: ClusterReplicaLocation,
461 );
462
463 /// Disconnects the storage instance from the specified replica.
464 fn drop_replica(&mut self, instance_id: StorageInstanceId, replica_id: ReplicaId);
465
466 /// Across versions of Materialize the nullability of columns for some objects can change based
467 /// on updates to our optimizer.
468 ///
469 /// During bootstrap we will register these new schemas with Persist.
470 ///
471 /// See: <https://github.com/MaterializeInc/database-issues/issues/2488>
472 async fn evolve_nullability_for_bootstrap(
473 &mut self,
474 storage_metadata: &StorageMetadata,
475 collections: Vec<(GlobalId, RelationDesc)>,
476 ) -> Result<(), StorageError>;
477
478 /// Create the sources described in the individual RunIngestionCommand commands.
479 ///
480 /// Each command carries the source id, the source description, and any associated metadata
481 /// needed to ingest the particular source.
482 ///
483 /// This command installs collection state for the indicated sources, and they are
484 /// now valid to use in queries at times beyond the initial `since` frontiers. Each
485 /// collection also acquires a read capability at this frontier, which will need to
486 /// be repeatedly downgraded with `allow_compaction()` to permit compaction.
487 ///
488 /// This method is NOT idempotent; It can fail between processing of different
489 /// collections and leave the controller in an inconsistent state. It is almost
490 /// always wrong to do anything but abort the process on `Err`.
491 ///
492 /// The `register_ts` is the initial timestamp at which tables become available for reads. (We
493 /// might later give non-tables the same treatment, but hold off on that initially.) Callers
494 /// must provide a Some if any of the collections is a table. A None may be given if none of the
495 /// collections are a table (i.e. all materialized views, sources, etc).
496 ///
497 /// This sets up storage but does not register tables in the txns shard. Runtime registration
498 /// must go through the adapter's group committer. Bootstrap uses
499 /// [`Self::register_table_collections`].
500 async fn create_collections(
501 &mut self,
502 storage_metadata: &StorageMetadata,
503 register_ts: Option<Timestamp>,
504 collections: Vec<(GlobalId, CollectionDescription)>,
505 ) -> Result<(), StorageError> {
506 self.create_collections_for_bootstrap(
507 storage_metadata,
508 register_ts,
509 collections,
510 &BTreeSet::new(),
511 )
512 .await
513 }
514
515 /// Like [`Self::create_collections`], except used specifically for bootstrap.
516 ///
517 /// `migrated_storage_collections` is a set of migrated storage collections to be excluded
518 /// from the txn-wal sub-system.
519 async fn create_collections_for_bootstrap(
520 &mut self,
521 storage_metadata: &StorageMetadata,
522 register_ts: Option<Timestamp>,
523 collections: Vec<(GlobalId, CollectionDescription)>,
524 migrated_storage_collections: &BTreeSet<GlobalId>,
525 ) -> Result<(), StorageError>;
526
527 /// Check that the ingestion associated with `id` can use the provided
528 /// [`SourceDesc`].
529 ///
530 /// Note that this check is optimistic and its return of `Ok(())` does not
531 /// guarantee that subsequent calls to `alter_ingestion_source_desc` are
532 /// guaranteed to succeed.
533 fn check_alter_ingestion_source_desc(
534 &mut self,
535 ingestion_id: GlobalId,
536 source_desc: &SourceDesc,
537 ) -> Result<(), StorageError>;
538
539 /// Alters each identified ingestion to use the correlated [`SourceDesc`].
540 async fn alter_ingestion_source_desc(
541 &mut self,
542 ingestion_ids: BTreeMap<GlobalId, SourceDesc>,
543 ) -> Result<(), StorageError>;
544
545 /// Alters each identified collection to use the correlated [`GenericSourceConnection`].
546 async fn alter_ingestion_connections(
547 &mut self,
548 source_connections: BTreeMap<GlobalId, GenericSourceConnection<InlinedConnection>>,
549 ) -> Result<(), StorageError>;
550
551 /// Alters the data config for the specified source exports of the specified ingestions.
552 async fn alter_ingestion_export_data_configs(
553 &mut self,
554 source_exports: BTreeMap<GlobalId, SourceExportDataConfig>,
555 ) -> Result<(), StorageError>;
556
557 /// Evolves a table's schema without registering the new collection in the txns shard.
558 ///
559 /// Runtime registration must go through the adapter's group committer.
560 async fn alter_table_desc(
561 &mut self,
562 existing_collection: GlobalId,
563 new_collection: GlobalId,
564 new_desc: RelationDesc,
565 expected_version: RelationVersion,
566 ) -> Result<(), StorageError>;
567
568 /// Registers the `DataSource::Table` collections among `ids` during bootstrap.
569 ///
570 /// Runtime registration must go through the adapter's group committer. In read-only mode, only
571 /// migrated tables are registered.
572 async fn register_table_collections(
573 &mut self,
574 register_ts: Timestamp,
575 ids: Vec<GlobalId>,
576 ) -> Result<(), StorageError>;
577
578 /// Returns registration metadata for the `DataSource::Table` collections among `ids`.
579 ///
580 /// Other data sources are ignored.
581 fn table_registrations(
582 &self,
583 ids: Vec<GlobalId>,
584 ) -> Result<Vec<TableRegistration>, StorageError>;
585
586 /// Returns the `DataSource::Table` IDs among `ids`.
587 fn txns_table_ids(&self, ids: Vec<GlobalId>) -> Result<Vec<GlobalId>, StorageError>;
588
589 /// Acquire an immutable reference to the export state, should it exist.
590 fn export(&self, id: GlobalId) -> Result<&ExportState, StorageError>;
591
592 /// Acquire a mutable reference to the export state, should it exist.
593 fn export_mut(&mut self, id: GlobalId) -> Result<&mut ExportState, StorageError>;
594
595 /// Create a oneshot ingestion.
596 async fn create_oneshot_ingestion(
597 &mut self,
598 ingestion_id: uuid::Uuid,
599 collection_id: GlobalId,
600 instance_id: StorageInstanceId,
601 request: OneshotIngestionRequest,
602 result_tx: OneshotResultCallback<ProtoBatch>,
603 ) -> Result<(), StorageError>;
604
605 /// Cancel a oneshot ingestion.
606 fn cancel_oneshot_ingestion(&mut self, ingestion_id: uuid::Uuid) -> Result<(), StorageError>;
607
608 /// Alter the sink identified by the given id to match the provided `ExportDescription`.
609 async fn alter_export(
610 &mut self,
611 id: GlobalId,
612 export: ExportDescription,
613 ) -> Result<(), StorageError>;
614
615 /// For each identified export, alter its [`StorageSinkConnection`].
616 async fn alter_export_connections(
617 &mut self,
618 exports: BTreeMap<GlobalId, StorageSinkConnection>,
619 ) -> Result<(), StorageError>;
620
621 /// Schedules controller cleanup for tables.
622 ///
623 /// The txn-wal tables among `identifiers` must first be forgotten through the adapter's group
624 /// committer.
625 fn drop_tables(
626 &mut self,
627 storage_metadata: &StorageMetadata,
628 identifiers: Vec<GlobalId>,
629 ) -> Result<(), StorageError>;
630
631 /// Drops the read capability for the sources and allows their resources to be reclaimed.
632 fn drop_sources(
633 &mut self,
634 storage_metadata: &StorageMetadata,
635 identifiers: Vec<GlobalId>,
636 ) -> Result<(), StorageError>;
637
638 /// Drops the read capability for the sinks and allows their resources to be reclaimed.
639 fn drop_sinks(
640 &mut self,
641 storage_metadata: &StorageMetadata,
642 identifiers: Vec<GlobalId>,
643 ) -> Result<(), StorageError>;
644
645 /// Drops the read capability for the sinks and allows their resources to be reclaimed.
646 ///
647 /// TODO(jkosh44): This method does not validate the provided identifiers. Currently when the
648 /// controller starts/restarts it has no durable state. That means that it has no way of
649 /// remembering any past commands sent. In the future we plan on persisting state for the
650 /// controller so that it is aware of past commands.
651 /// Therefore this method is for dropping sinks that we know to have been previously
652 /// created, but have been forgotten by the controller due to a restart.
653 /// Once command history becomes durable we can remove this method and use the normal
654 /// `drop_sinks`.
655 fn drop_sinks_unvalidated(
656 &mut self,
657 storage_metadata: &StorageMetadata,
658 identifiers: Vec<GlobalId>,
659 );
660
661 /// Drops the read capability for the sources and allows their resources to be reclaimed.
662 ///
663 /// TODO(jkosh44): This method does not validate the provided identifiers. Currently when the
664 /// controller starts/restarts it has no durable state. That means that it has no way of
665 /// remembering any past commands sent. In the future we plan on persisting state for the
666 /// controller so that it is aware of past commands.
667 /// Therefore this method is for dropping sources that we know to have been previously
668 /// created, but have been forgotten by the controller due to a restart.
669 /// Once command history becomes durable we can remove this method and use the normal
670 /// `drop_sources`.
671 fn drop_sources_unvalidated(
672 &mut self,
673 storage_metadata: &StorageMetadata,
674 identifiers: Vec<GlobalId>,
675 ) -> Result<(), StorageError>;
676
677 /// Appends to tables during bootstrap.
678 ///
679 /// Runtime writes must go through the adapter's group committer. The returned receiver resolves
680 /// when the atomic write completes.
681 fn append_table(
682 &mut self,
683 write_ts: Timestamp,
684 advance_to: Timestamp,
685 commands: Vec<(GlobalId, Vec<TableData>)>,
686 ) -> Result<tokio::sync::oneshot::Receiver<Result<(), StorageError>>, StorageError>;
687
688 /// Returns the process-lifetime storage mechanism used by the adapter's group committer.
689 fn table_write_handle(&self) -> Arc<dyn TableWriteHandle>;
690
691 /// Returns a [`MonotonicAppender`] which is a channel that can be used to monotonically
692 /// append to the specified [`GlobalId`].
693 fn monotonic_appender(&self, id: GlobalId) -> Result<MonotonicAppender, StorageError>;
694
695 /// Returns a shared [`WebhookStatistics`] which can be used to report user-facing
696 /// statistics for this given webhhook, specified by the [`GlobalId`].
697 ///
698 // This is used to support a fairly special case, where a source needs to report statistics
699 // from outside the ordinary controller-clusterd path. Its possible to merge this with
700 // `monotonic_appender`, whose only current user is webhooks, but given that they will
701 // likely be moved to clusterd, we just leave this a special case.
702 fn webhook_statistics(&self, id: GlobalId) -> Result<Arc<WebhookStatistics>, StorageError>;
703
704 /// Waits until the controller is ready to process a response.
705 ///
706 /// This method may block for an arbitrarily long time.
707 ///
708 /// When the method returns, the owner should call
709 /// [`StorageController::process`] to process the ready message.
710 ///
711 /// This method is cancellation safe.
712 async fn ready(&mut self);
713
714 /// Processes the work queued by [`StorageController::ready`].
715 fn process(
716 &mut self,
717 storage_metadata: &StorageMetadata,
718 ) -> Result<Option<Response>, anyhow::Error>;
719
720 /// Exposes the internal state of the data shard for debugging and QA.
721 ///
722 /// We'll be thoughtful about making unnecessary changes, but the **output
723 /// of this method needs to be gated from users**, so that it's not subject
724 /// to our backward compatibility guarantees.
725 ///
726 /// TODO: Ideally this would return `impl Serialize` so the caller can do
727 /// with it what they like, but that doesn't work in traits yet. The
728 /// workaround (an associated type) doesn't work because persist doesn't
729 /// want to make the type public. In the meantime, move the `serde_json`
730 /// call from the single user into this method.
731 async fn inspect_persist_state(&self, id: GlobalId)
732 -> Result<serde_json::Value, anyhow::Error>;
733
734 /// Records append-only updates for the given introspection type.
735 ///
736 /// Rows passed in `updates` MUST have the correct schema for the given
737 /// introspection type, as readers rely on this and might panic otherwise.
738 fn append_introspection_updates(&mut self, type_: IntrospectionType, updates: Vec<(Row, Diff)>);
739
740 /// Records append-only status updates for the given introspection type.
741 fn append_status_introspection_updates(
742 &mut self,
743 type_: IntrospectionType,
744 updates: Vec<StatusUpdate>,
745 );
746
747 /// Updates the desired state of the given introspection type.
748 ///
749 /// Rows passed in `op` MUST have the correct schema for the given
750 /// introspection type, as readers rely on this and might panic otherwise.
751 fn update_introspection_collection(&mut self, type_: IntrospectionType, op: StorageWriteOp);
752
753 /// Returns a sender for updates to the specified append-only introspection collection.
754 ///
755 /// # Panics
756 ///
757 /// Panics if the given introspection type is not associated with an append-only collection.
758 fn append_only_introspection_tx(
759 &self,
760 type_: IntrospectionType,
761 ) -> mpsc::UnboundedSender<(
762 Vec<AppendOnlyUpdate>,
763 oneshot::Sender<Result<(), StorageError>>,
764 )>;
765
766 /// Returns a sender for updates to the specified differential introspection collection.
767 ///
768 /// # Panics
769 ///
770 /// Panics if the given introspection type is not associated with a differential collection.
771 fn differential_introspection_tx(
772 &self,
773 type_: IntrospectionType,
774 ) -> mpsc::UnboundedSender<(StorageWriteOp, oneshot::Sender<Result<(), StorageError>>)>;
775
776 async fn real_time_recent_timestamp(
777 &self,
778 source_ids: BTreeSet<GlobalId>,
779 timeout: Duration,
780 ) -> Result<BoxFuture<Result<Timestamp, StorageError>>, StorageError>;
781
782 /// Returns the state of the [`StorageController`] formatted as JSON.
783 fn dump(&self) -> Result<serde_json::Value, anyhow::Error>;
784}
785
786impl DataSource {
787 /// Returns true if the storage controller manages the data shard for this
788 /// source using txn-wal.
789 pub fn in_txns(&self) -> bool {
790 match self {
791 DataSource::Table => true,
792 DataSource::Other
793 | DataSource::Ingestion(_)
794 | DataSource::IngestionExport { .. }
795 | DataSource::Introspection(_)
796 | DataSource::Progress
797 | DataSource::Webhook => false,
798 DataSource::Sink { .. } => false,
799 }
800 }
801}
802
803/// A wrapper struct that presents the adapter token to a format that is understandable by persist
804/// and also allows us to differentiate between a token being present versus being set for the
805/// first time.
806#[derive(PartialEq, Clone, Debug, Default)]
807pub struct PersistEpoch(pub Option<NonZeroI64>);
808
809impl Codec64 for PersistEpoch {
810 fn codec_name() -> String {
811 "PersistEpoch".to_owned()
812 }
813
814 fn encode(&self) -> [u8; 8] {
815 self.0.map(NonZeroI64::get).unwrap_or(0).to_le_bytes()
816 }
817
818 fn decode(buf: [u8; 8]) -> Self {
819 Self(NonZeroI64::new(i64::from_le_bytes(buf)))
820 }
821}
822
823impl From<NonZeroI64> for PersistEpoch {
824 fn from(epoch: NonZeroI64) -> Self {
825 Self(Some(epoch))
826 }
827}
828
829/// State maintained about individual exports.
830#[derive(Debug)]
831pub struct ExportState {
832 /// Really only for keeping track of changes to the `derived_since`.
833 pub read_capabilities: MutableAntichain<Timestamp>,
834
835 /// The cluster this export is associated with.
836 pub cluster_id: StorageInstanceId,
837
838 /// The current since frontier, derived from `write_frontier` using
839 /// `hold_policy`.
840 pub derived_since: Antichain<Timestamp>,
841
842 /// The read holds that this export has on its dependencies (its input and itself). When
843 /// the upper of the export changes, we downgrade this, which in turn
844 /// downgrades holds we have on our dependencies' sinces.
845 pub read_holds: [ReadHold; 2],
846
847 /// The policy to use to downgrade `self.read_capability`.
848 pub read_policy: ReadPolicy,
849
850 /// Reported write frontier.
851 pub write_frontier: Antichain<Timestamp>,
852}
853
854impl ExportState {
855 pub fn new(
856 cluster_id: StorageInstanceId,
857 read_hold: ReadHold,
858 self_hold: ReadHold,
859 write_frontier: Antichain<Timestamp>,
860 read_policy: ReadPolicy,
861 ) -> Self {
862 let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
863 for read_hold in [&read_hold, &self_hold] {
864 dependency_since.join_assign(read_hold.since());
865 }
866 Self {
867 read_capabilities: MutableAntichain::from(dependency_since.borrow()),
868 cluster_id,
869 derived_since: dependency_since,
870 read_holds: [read_hold, self_hold],
871 read_policy,
872 write_frontier,
873 }
874 }
875
876 /// Returns the cluster to which the export is bound.
877 pub fn cluster_id(&self) -> StorageInstanceId {
878 self.cluster_id
879 }
880
881 /// Returns the cluster to which the export is bound.
882 pub fn input_hold(&self) -> &ReadHold {
883 &self.read_holds[0]
884 }
885
886 /// Returns whether the export was dropped.
887 pub fn is_dropped(&self) -> bool {
888 self.read_holds.iter().all(|h| h.since().is_empty())
889 }
890}
891/// A channel that allows you to append a set of updates to a pre-defined [`GlobalId`].
892///
893/// See `CollectionManager::monotonic_appender` to acquire a [`MonotonicAppender`].
894#[derive(Clone, Debug)]
895pub struct MonotonicAppender {
896 /// Channel that sends to a [`tokio::task`] which pushes updates to Persist.
897 tx: mpsc::UnboundedSender<(
898 Vec<AppendOnlyUpdate>,
899 oneshot::Sender<Result<(), StorageError>>,
900 )>,
901}
902
903impl MonotonicAppender {
904 pub fn new(
905 tx: mpsc::UnboundedSender<(
906 Vec<AppendOnlyUpdate>,
907 oneshot::Sender<Result<(), StorageError>>,
908 )>,
909 ) -> Self {
910 MonotonicAppender { tx }
911 }
912
913 pub async fn append(&self, updates: Vec<AppendOnlyUpdate>) -> Result<(), StorageError> {
914 let (tx, rx) = oneshot::channel();
915
916 // Send our update to the CollectionManager.
917 self.tx
918 .send((updates, tx))
919 .map_err(|_| StorageError::ShuttingDown("collection manager"))?;
920
921 // Wait for a response, if we fail to receive then the CollectionManager has gone away.
922 let result = rx
923 .await
924 .map_err(|_| StorageError::ShuttingDown("collection manager"))?;
925
926 result
927 }
928}
929
930/// A wallclock lag measurement.
931///
932/// The enum representation reflects the fact that wallclock lag is undefined for unreadable
933/// collections, i.e. collections that contain no readable times.
934#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
935pub enum WallclockLag {
936 /// Lag value in seconds, for readable collections.
937 Seconds(u64),
938 /// Undefined lag, for unreadable collections.
939 Undefined,
940}
941
942impl WallclockLag {
943 /// The smallest possible wallclock lag measurement.
944 pub const MIN: Self = Self::Seconds(0);
945
946 /// Return the maximum of two lag values.
947 ///
948 /// We treat `Undefined` lags as greater than `Seconds`, to ensure we never report low lag
949 /// values when a collection was actually unreadable for some amount of time.
950 pub fn max(self, other: Self) -> Self {
951 match (self, other) {
952 (Self::Seconds(a), Self::Seconds(b)) => Self::Seconds(a.max(b)),
953 (Self::Undefined, _) | (_, Self::Undefined) => Self::Undefined,
954 }
955 }
956
957 /// Return the wrapped seconds value, or a default if the lag is `Undefined`.
958 pub fn unwrap_seconds_or(self, default: u64) -> u64 {
959 match self {
960 Self::Seconds(s) => s,
961 Self::Undefined => default,
962 }
963 }
964
965 /// Create a new `WallclockLag` by transforming the wrapped seconds value.
966 pub fn map_seconds(self, f: impl FnOnce(u64) -> u64) -> Self {
967 match self {
968 Self::Seconds(s) => Self::Seconds(f(s)),
969 Self::Undefined => Self::Undefined,
970 }
971 }
972
973 /// Convert this lag value into a [`Datum::Interval`] or [`Datum::Null`].
974 pub fn into_interval_datum(self) -> Datum<'static> {
975 match self {
976 Self::Seconds(secs) => {
977 let micros = i64::try_from(secs * 1_000_000).expect("must fit");
978 Datum::Interval(Interval::new(0, 0, micros))
979 }
980 Self::Undefined => Datum::Null,
981 }
982 }
983
984 /// Convert this lag value into a [`Datum::UInt64`] or [`Datum::Null`].
985 pub fn into_uint64_datum(self) -> Datum<'static> {
986 match self {
987 Self::Seconds(secs) => Datum::UInt64(secs),
988 Self::Undefined => Datum::Null,
989 }
990 }
991}
992
993/// The period covered by a wallclock lag histogram, represented as a `[start, end)` range.
994#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
995pub struct WallclockLagHistogramPeriod {
996 pub start: CheckedTimestamp<DateTime<Utc>>,
997 pub end: CheckedTimestamp<DateTime<Utc>>,
998}
999
1000impl WallclockLagHistogramPeriod {
1001 /// Construct a `WallclockLagHistogramPeriod` from the given epoch timestamp and dyncfg.
1002 pub fn from_epoch_millis(epoch_ms: u64, dyncfg: &ConfigSet) -> Self {
1003 let interval = WALLCLOCK_LAG_HISTOGRAM_PERIOD_INTERVAL.get(dyncfg);
1004 let interval_ms = u64::try_from(interval.as_millis()).unwrap_or_else(|_| {
1005 soft_panic_or_log!("excessive wallclock lag histogram period interval: {interval:?}");
1006 let default = WALLCLOCK_LAG_HISTOGRAM_PERIOD_INTERVAL.default();
1007 u64::try_from(default.as_millis()).unwrap()
1008 });
1009 let interval_ms = std::cmp::max(interval_ms, 1);
1010
1011 let start_ms = epoch_ms - (epoch_ms % interval_ms);
1012 let start_dt = mz_ore::now::to_datetime(start_ms);
1013 let start = start_dt.try_into().expect("must fit");
1014
1015 let end_ms = start_ms + interval_ms;
1016 let end_dt = mz_ore::now::to_datetime(end_ms);
1017 let end = end_dt.try_into().expect("must fit");
1018
1019 Self { start, end }
1020 }
1021}
1022
1023#[cfg(test)]
1024mod tests {
1025 use super::*;
1026
1027 #[mz_ore::test]
1028 fn lag_writes_by_zero() {
1029 let policy =
1030 ReadPolicy::lag_writes_by(mz_repr::Timestamp::default(), mz_repr::Timestamp::default());
1031 let write_frontier = Antichain::from_elem(mz_repr::Timestamp::from(5));
1032 assert_eq!(policy.frontier(write_frontier.borrow()), write_frontier);
1033 }
1034}