1#![allow(missing_docs)]
11
12use std::collections::{BTreeMap, BTreeSet};
15use std::fmt::Debug;
16use std::iter;
17
18use differential_dataflow::difference::Monoid;
19use differential_dataflow::lattice::Lattice;
20use mz_cluster_client::ReplicaId;
21use mz_cluster_client::client::TryIntoProtocolNonce;
22use mz_ore::assert_none;
23use mz_persist_client::batch::{BatchBuilder, ProtoBatch};
24use mz_persist_client::write::WriteHandle;
25use mz_persist_types::{Codec, Codec64, StepForward};
26use mz_repr::{Diff, GlobalId, Row, Timestamp};
27use mz_service::client::{Partitionable, PartitionedState};
28use mz_storage_types::controller::CollectionMetadata;
29use mz_storage_types::oneshot_sources::OneshotIngestionRequest;
30use mz_storage_types::parameters::StorageParameters;
31use mz_storage_types::sinks::StorageSinkDesc;
32use mz_storage_types::sources::IngestionDescription;
33use serde::{Deserialize, Serialize};
34use smallvec::SmallVec;
35use timely::PartialOrder;
36use timely::progress::frontier::{Antichain, MutableAntichain};
37use uuid::Uuid;
38
39use crate::statistics::{SinkStatisticsUpdate, SourceStatisticsUpdate};
40
41#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
43pub enum StorageCommand {
44 Hello {
46 nonce: Uuid,
47 },
48 InitializationComplete,
51 AllowWrites,
58 UpdateConfiguration(Box<StorageParameters>),
60 RunIngestion(Box<RunIngestionCommand>),
62 AllowCompaction(GlobalId, Antichain<Timestamp>),
66 RunSink(Box<RunSinkCommand>),
67 RunOneshotIngestion(Box<RunOneshotIngestion>),
73 CancelOneshotIngestion(Uuid),
82}
83
84impl StorageCommand {
85 pub fn installs_objects(&self) -> bool {
87 use StorageCommand::*;
88 match self {
89 Hello { .. }
90 | InitializationComplete
91 | AllowWrites
92 | UpdateConfiguration(_)
93 | AllowCompaction(_, _)
94 | CancelOneshotIngestion { .. } => false,
95 RunIngestion(_) | RunSink(_) | RunOneshotIngestion(_) => true,
99 }
100 }
101}
102
103#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
105pub struct RunIngestionCommand {
106 pub id: GlobalId,
108 pub description: IngestionDescription<CollectionMetadata>,
111}
112
113#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
115pub struct RunOneshotIngestion {
116 pub ingestion_id: uuid::Uuid,
118 pub collection_id: GlobalId,
120 pub collection_meta: CollectionMetadata,
122 pub request: OneshotIngestionRequest,
124}
125
126#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
128pub struct RunSinkCommand {
129 pub id: GlobalId,
130 pub description: StorageSinkDesc<CollectionMetadata>,
131}
132
133#[derive(
135 Copy,
136 Clone,
137 Debug,
138 Serialize,
139 Deserialize,
140 PartialEq,
141 Eq,
142 PartialOrd,
143 Ord
144)]
145pub enum Status {
146 Starting,
147 Running,
148 Paused,
149 Stalled,
150 Ceased,
153 Dropped,
154}
155
156impl std::str::FromStr for Status {
157 type Err = anyhow::Error;
158 fn from_str(s: &str) -> Result<Self, Self::Err> {
160 Ok(match s {
161 "starting" => Status::Starting,
162 "running" => Status::Running,
163 "paused" => Status::Paused,
164 "stalled" => Status::Stalled,
165 "ceased" => Status::Ceased,
166 "dropped" => Status::Dropped,
167 s => return Err(anyhow::anyhow!("{} is not a valid status", s)),
168 })
169 }
170}
171
172impl Status {
173 pub fn to_str(&self) -> &'static str {
175 match self {
176 Status::Starting => "starting",
177 Status::Running => "running",
178 Status::Paused => "paused",
179 Status::Stalled => "stalled",
180 Status::Ceased => "ceased",
181 Status::Dropped => "dropped",
182 }
183 }
184
185 pub fn superseded_by(self, new: Status) -> bool {
188 match (self, new) {
189 (_, Status::Dropped) => true,
190 (Status::Dropped, _) => false,
191 (Status::Paused, Status::Paused) => false,
193 _ => true,
196 }
197 }
198}
199
200#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
206pub struct StatusUpdate {
207 pub id: GlobalId,
208 pub status: Status,
209 pub timestamp: chrono::DateTime<chrono::Utc>,
210 pub error: Option<String>,
211 pub hints: BTreeSet<String>,
212 pub namespaced_errors: BTreeMap<String, String>,
213 pub replica_id: Option<ReplicaId>,
214}
215
216impl StatusUpdate {
217 pub fn new(
218 id: GlobalId,
219 timestamp: chrono::DateTime<chrono::Utc>,
220 status: Status,
221 ) -> StatusUpdate {
222 StatusUpdate {
223 id,
224 timestamp,
225 status,
226 error: None,
227 hints: Default::default(),
228 namespaced_errors: Default::default(),
229 replica_id: None,
230 }
231 }
232}
233
234impl From<StatusUpdate> for Row {
235 fn from(update: StatusUpdate) -> Self {
236 use mz_repr::Datum;
237
238 let timestamp = Datum::TimestampTz(update.timestamp.try_into().expect("must fit"));
239 let id = update.id.to_string();
240 let id = Datum::String(&id);
241 let status = Datum::String(update.status.to_str());
242 let error = update.error.as_deref().into();
243
244 let mut row = Row::default();
245 let mut packer = row.packer();
246 packer.extend([timestamp, id, status, error]);
247
248 if !update.hints.is_empty() || !update.namespaced_errors.is_empty() {
249 packer.push_dict_with(|dict_packer| {
250 if !update.hints.is_empty() {
253 dict_packer.push(Datum::String("hints"));
254 dict_packer.push_list(update.hints.iter().map(|s| Datum::String(s)));
255 }
256 if !update.namespaced_errors.is_empty() {
257 dict_packer.push(Datum::String("namespaced"));
258 dict_packer.push_dict(
259 update
260 .namespaced_errors
261 .iter()
262 .map(|(k, v)| (k.as_str(), Datum::String(v))),
263 );
264 }
265 });
266 } else {
267 packer.push(Datum::Null);
268 }
269
270 match update.replica_id {
271 Some(id) => packer.push(Datum::String(&id.to_string())),
272 None => packer.push(Datum::Null),
273 }
274
275 row
276 }
277}
278
279pub enum AppendOnlyUpdate {
281 Row((Row, Diff)),
282 Status(StatusUpdate),
283}
284
285impl AppendOnlyUpdate {
286 pub fn into_row(self) -> (Row, Diff) {
287 match self {
288 AppendOnlyUpdate::Row((row, diff)) => (row, diff),
289 AppendOnlyUpdate::Status(status) => (Row::from(status), Diff::ONE),
290 }
291 }
292}
293
294impl From<(Row, Diff)> for AppendOnlyUpdate {
295 fn from((row, diff): (Row, Diff)) -> Self {
296 Self::Row((row, diff))
297 }
298}
299
300impl From<StatusUpdate> for AppendOnlyUpdate {
301 fn from(update: StatusUpdate) -> Self {
302 Self::Status(update)
303 }
304}
305
306#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
308pub enum StorageResponse {
309 FrontierUpper(GlobalId, Antichain<Timestamp>),
311 DroppedId(GlobalId),
313 StagedBatches(BTreeMap<uuid::Uuid, Vec<Result<ProtoBatch, String>>>),
315 StatisticsUpdates(Vec<SourceStatisticsUpdate>, Vec<SinkStatisticsUpdate>),
317 StatusUpdate(StatusUpdate),
320}
321
322#[derive(Debug)]
327pub struct PartitionedStorageState {
328 parts: usize,
330 uppers: BTreeMap<
333 GlobalId,
334 (
335 MutableAntichain<Timestamp>,
336 Vec<Option<Antichain<Timestamp>>>,
337 ),
338 >,
339 oneshot_source_responses:
341 BTreeMap<uuid::Uuid, BTreeMap<usize, Vec<Result<ProtoBatch, String>>>>,
342}
343
344impl Partitionable<StorageCommand, StorageResponse> for (StorageCommand, StorageResponse) {
345 type PartitionedState = PartitionedStorageState;
346
347 fn new(parts: usize) -> PartitionedStorageState {
348 PartitionedStorageState {
349 parts,
350 uppers: BTreeMap::new(),
351 oneshot_source_responses: BTreeMap::new(),
352 }
353 }
354}
355
356impl PartitionedStorageState {
357 fn observe_command(&mut self, command: &StorageCommand) {
358 match command {
365 StorageCommand::Hello { .. } => {}
366 StorageCommand::RunIngestion(ingestion) => {
367 self.insert_new_uppers(ingestion.description.collection_ids());
368 }
369 StorageCommand::RunSink(export) => {
370 self.insert_new_uppers([export.id]);
371 }
372 StorageCommand::InitializationComplete
373 | StorageCommand::AllowWrites
374 | StorageCommand::UpdateConfiguration(_)
375 | StorageCommand::AllowCompaction(_, _)
376 | StorageCommand::RunOneshotIngestion(_)
377 | StorageCommand::CancelOneshotIngestion { .. } => {}
378 }
379 }
380
381 fn insert_new_uppers<I: IntoIterator<Item = GlobalId>>(&mut self, ids: I) {
387 for id in ids {
388 self.uppers.entry(id).or_insert_with(|| {
389 let mut frontier = MutableAntichain::new();
390 #[allow(clippy::as_conversions)]
393 frontier.update_iter(iter::once((Timestamp::MIN, self.parts as i64)));
394 let part_frontiers = vec![Some(Antichain::from_elem(Timestamp::MIN)); self.parts];
395
396 (frontier, part_frontiers)
397 });
398 }
399 }
400}
401
402impl PartitionedState<StorageCommand, StorageResponse> for PartitionedStorageState {
403 fn split_command(&mut self, command: StorageCommand) -> Vec<Option<StorageCommand>> {
404 self.observe_command(&command);
405
406 vec![Some(command); self.parts]
409 }
410
411 fn absorb_response(
412 &mut self,
413 shard_id: usize,
414 response: StorageResponse,
415 ) -> Option<Result<StorageResponse, anyhow::Error>> {
416 match response {
417 StorageResponse::FrontierUpper(id, new_shard_upper) => {
419 let (frontier, shard_frontiers) = match self.uppers.get_mut(&id) {
420 Some(value) => value,
421 None => panic!("Reference to absent collection: {id}"),
422 };
423 let old_upper = frontier.frontier().to_owned();
424 let shard_upper = match &mut shard_frontiers[shard_id] {
425 Some(shard_upper) => shard_upper,
426 None => panic!("Reference to absent shard {shard_id} for collection {id}"),
427 };
428 frontier.update_iter(shard_upper.iter().map(|t| (*t, -1)));
429 frontier.update_iter(new_shard_upper.iter().map(|t| (*t, 1)));
430 shard_upper.join_assign(&new_shard_upper);
431
432 let new_upper = frontier.frontier();
433 if PartialOrder::less_than(&old_upper.borrow(), &new_upper) {
434 Some(Ok(StorageResponse::FrontierUpper(id, new_upper.to_owned())))
435 } else {
436 None
437 }
438 }
439 StorageResponse::DroppedId(id) => {
440 let (_, shard_frontiers) = match self.uppers.get_mut(&id) {
441 Some(value) => value,
442 None => panic!("Reference to absent collection: {id}"),
443 };
444 let prev = shard_frontiers[shard_id].take();
445 assert!(
446 prev.is_some(),
447 "got double drop for {id} from shard {shard_id}"
448 );
449
450 if shard_frontiers.iter().all(Option::is_none) {
451 self.uppers.remove(&id);
452 Some(Ok(StorageResponse::DroppedId(id)))
453 } else {
454 None
455 }
456 }
457 StorageResponse::StatisticsUpdates(source_stats, sink_stats) => {
458 Some(Ok(StorageResponse::StatisticsUpdates(
462 source_stats,
463 sink_stats,
464 )))
465 }
466 StorageResponse::StatusUpdate(updates) => {
467 Some(Ok(StorageResponse::StatusUpdate(updates)))
468 }
469 StorageResponse::StagedBatches(batches) => {
470 let mut finished_batches = BTreeMap::new();
471
472 for (collection_id, batches) in batches {
473 tracing::info!(%shard_id, %collection_id, "got batch");
474
475 let entry = self
476 .oneshot_source_responses
477 .entry(collection_id)
478 .or_default();
479 let novel = entry.insert(shard_id, batches);
480 assert_none!(novel, "Duplicate oneshot source response");
481
482 if entry.len() == self.parts {
484 let entry = self
485 .oneshot_source_responses
486 .remove(&collection_id)
487 .expect("checked above");
488 let all_batches: Vec<_> = entry.into_values().flatten().collect();
489
490 finished_batches.insert(collection_id, all_batches);
491 }
492 }
493
494 if !finished_batches.is_empty() {
495 Some(Ok(StorageResponse::StagedBatches(finished_batches)))
496 } else {
497 None
498 }
499 }
500 }
501 }
502}
503
504#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
505pub struct Update {
507 pub row: Row,
508 pub timestamp: Timestamp,
509 pub diff: Diff,
510}
511
512#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
513pub struct TimestamplessUpdate {
518 pub row: Row,
519 pub diff: Diff,
520}
521
522#[derive(Debug, Clone, PartialEq)]
523pub enum TableData {
524 Rows(Vec<(Row, Diff)>),
528 Batches(SmallVec<[ProtoBatch; 1]>),
530}
531
532impl TableData {
533 pub fn is_empty(&self) -> bool {
534 match self {
535 TableData::Rows(rows) => rows.is_empty(),
536 TableData::Batches(batches) => batches.is_empty(),
537 }
538 }
539}
540
541pub struct TimestamplessUpdateBuilder<K, V, D>
544where
545 K: Codec,
546 V: Codec,
547 D: Codec64,
548{
549 builder: BatchBuilder<K, V, Timestamp, D>,
550 initial_ts: Timestamp,
551}
552
553impl<K, V, D> TimestamplessUpdateBuilder<K, V, D>
554where
555 K: Debug + Codec,
556 V: Debug + Codec,
557 D: Monoid + Ord + Codec64 + Send + Sync,
558{
559 pub fn new(handle: &WriteHandle<K, V, Timestamp, D>) -> Self {
562 let initial_ts = Timestamp::MIN;
563 let builder = handle.builder(Antichain::from_elem(initial_ts));
564 TimestamplessUpdateBuilder {
565 builder,
566 initial_ts,
567 }
568 }
569
570 pub async fn add(&mut self, k: &K, v: &V, d: &D) {
572 self.builder
573 .add(k, v, &self.initial_ts, d)
574 .await
575 .expect("invalid Persist usage");
576 }
577
578 pub async fn finish(self) -> ProtoBatch {
583 let finish_ts = StepForward::step_forward(&self.initial_ts);
584 let batch = self
585 .builder
586 .finish(Antichain::from_elem(finish_ts))
587 .await
588 .expect("invalid Persist usage");
589
590 batch.into_transmittable_batch()
591 }
592}
593
594impl TryIntoProtocolNonce for StorageCommand {
595 fn try_into_protocol_nonce(self) -> Result<Uuid, Self> {
596 match self {
597 StorageCommand::Hello { nonce } => Ok(nonce),
598 cmd => Err(cmd),
599 }
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606
607 #[mz_ore::test]
609 fn test_storage_command_size() {
610 assert_eq!(std::mem::size_of::<StorageCommand>(), 40);
611 }
612
613 #[mz_ore::test]
615 fn test_storage_response_size() {
616 assert_eq!(std::mem::size_of::<StorageResponse>(), 120);
617 }
618}