1use std::cell::RefCell;
80use std::collections::{BTreeMap, BTreeSet};
81use std::fmt;
82use std::rc::Rc;
83
84use mz_compute_types::dataflows::LirDataflowDescription;
85use mz_ore::collections::CollectionExt;
86use mz_ore::soft_panic_or_log;
87use mz_repr::{GlobalId, Timestamp};
88use mz_storage_client::storage_collections::StorageCollections;
89use mz_storage_types::read_holds::ReadHold;
90use mz_storage_types::read_policy::ReadPolicy;
91use timely::PartialOrder;
92use timely::progress::Antichain;
93use tracing::{info, warn};
94
95pub fn run(
104 dataflows: &mut [LirDataflowDescription],
105 read_policies: &BTreeMap<GlobalId, ReadPolicy>,
106 storage_collections: &dyn StorageCollections,
107 current_time: Timestamp,
108 read_only_mode: bool,
109 pending_replacements: &BTreeSet<GlobalId>,
110) -> BTreeMap<GlobalId, ReadHold> {
111 let mut storage_read_holds = BTreeMap::new();
114 for dataflow in &*dataflows {
115 for id in dataflow.source_imports.keys() {
116 if !storage_read_holds.contains_key(id) {
117 let read_hold = storage_collections
118 .acquire_read_holds(vec![*id])
119 .expect("storage collection exists")
120 .into_element();
121 storage_read_holds.insert(*id, read_hold);
122 }
123 }
124 }
125
126 let mut ctx = Context::new(dataflows, storage_collections, read_policies, current_time);
127
128 ctx.prune_sealed_persist_sinks();
132
133 if read_only_mode {
138 ctx.prune_dropped_collections();
139 }
140
141 ctx.apply_upstream_storage_constraints(&storage_read_holds);
143 ctx.apply_downstream_storage_constraints(pending_replacements);
144
145 ctx.apply_warmup_constraints();
161
162 ctx.apply_index_read_policy_constraints();
164
165 ctx.apply_index_current_time_constraints();
168
169 for dataflow in dataflows {
171 let first_export = dataflow.export_ids().next();
174 let as_of = first_export.map_or_else(Antichain::new, |id| ctx.best_as_of(id));
175 dataflow.as_of = Some(as_of);
176 }
177
178 storage_read_holds
179}
180
181#[derive(Debug)]
183struct AsOfBounds {
184 lower: Antichain<Timestamp>,
185 upper: Antichain<Timestamp>,
186 sealed: bool,
188}
189
190impl AsOfBounds {
191 fn single(frontier: Antichain<Timestamp>) -> Self {
193 Self {
194 lower: frontier.clone(),
195 upper: frontier,
196 sealed: false,
197 }
198 }
199
200 fn get(&self, type_: BoundType) -> &Antichain<Timestamp> {
202 match type_ {
203 BoundType::Lower => &self.lower,
204 BoundType::Upper => &self.upper,
205 }
206 }
207}
208
209impl Default for AsOfBounds {
210 fn default() -> Self {
211 Self {
212 lower: Antichain::from_elem(Timestamp::MIN),
213 upper: Antichain::new(),
214 sealed: false,
215 }
216 }
217}
218
219impl fmt::Display for AsOfBounds {
220 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221 write!(
222 f,
223 "[{:?} .. {:?}]",
224 self.lower.elements(),
225 self.upper.elements()
226 )
227 }
228}
229
230#[derive(Clone, Copy, Debug, PartialEq, Eq)]
232enum BoundType {
233 Lower,
234 Upper,
235}
236
237impl fmt::Display for BoundType {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 match self {
240 Self::Lower => f.write_str("lower"),
241 Self::Upper => f.write_str("upper"),
242 }
243 }
244}
245
246#[derive(Clone, Copy, Debug, PartialEq, Eq)]
248enum ConstraintType {
249 Hard,
252 Soft,
255}
256
257#[derive(Debug)]
259struct Constraint<'a> {
260 type_: ConstraintType,
261 bound_type: BoundType,
263 frontier: &'a Antichain<Timestamp>,
265 reason: &'a str,
269}
270
271impl Constraint<'_> {
272 fn apply(&self, bounds: &mut AsOfBounds) -> Result<bool, bool> {
282 if bounds.sealed {
283 return Ok(false);
284 }
285
286 match self.bound_type {
287 BoundType::Lower => {
288 if PartialOrder::less_than(&bounds.upper, self.frontier) {
289 bounds.sealed = true;
290 if PartialOrder::less_than(&bounds.lower, &bounds.upper) {
291 bounds.lower.clone_from(&bounds.upper);
292 Err(true)
293 } else {
294 Err(false)
295 }
296 } else if PartialOrder::less_equal(self.frontier, &bounds.lower) {
297 Ok(false)
298 } else {
299 bounds.lower.clone_from(self.frontier);
300 Ok(true)
301 }
302 }
303 BoundType::Upper => {
304 if PartialOrder::less_than(self.frontier, &bounds.lower) {
305 bounds.sealed = true;
306 if PartialOrder::less_than(&bounds.lower, &bounds.upper) {
307 bounds.upper.clone_from(&bounds.lower);
308 Err(true)
309 } else {
310 Err(false)
311 }
312 } else if PartialOrder::less_equal(&bounds.upper, self.frontier) {
313 Ok(false)
314 } else {
315 bounds.upper.clone_from(self.frontier);
316 Ok(true)
317 }
318 }
319 }
320 }
321}
322
323struct Collection<'a> {
325 storage_inputs: Vec<GlobalId>,
326 compute_inputs: Vec<GlobalId>,
327 read_policy: Option<&'a ReadPolicy>,
328 bounds: Rc<RefCell<AsOfBounds>>,
332 is_index: bool,
334}
335
336struct Context<'a> {
338 collections: BTreeMap<GlobalId, Collection<'a>>,
339 storage_collections: &'a dyn StorageCollections,
340 current_time: Timestamp,
341}
342
343impl<'a> Context<'a> {
344 fn new(
346 dataflows: &[LirDataflowDescription],
347 storage_collections: &'a dyn StorageCollections,
348 read_policies: &'a BTreeMap<GlobalId, ReadPolicy>,
349 current_time: Timestamp,
350 ) -> Self {
351 let mut collections = BTreeMap::new();
354 for dataflow in dataflows {
355 let storage_inputs: Vec<_> = dataflow.source_imports.keys().copied().collect();
356 let compute_inputs: Vec<_> = dataflow.index_imports.keys().copied().collect();
357
358 let bounds = match dataflow.as_of.clone() {
359 Some(frontier) => AsOfBounds::single(frontier),
360 None => AsOfBounds::default(),
361 };
362 let bounds = Rc::new(RefCell::new(bounds));
363
364 for id in dataflow.export_ids() {
365 let collection = Collection {
366 storage_inputs: storage_inputs.clone(),
367 compute_inputs: compute_inputs.clone(),
368 read_policy: read_policies.get(&id),
369 bounds: Rc::clone(&bounds),
370 is_index: dataflow.index_exports.contains_key(&id),
371 };
372 collections.insert(id, collection);
373 }
374 }
375
376 Self {
377 collections,
378 storage_collections,
379 current_time,
380 }
381 }
382
383 fn expect_collection(&self, id: GlobalId) -> &Collection<'_> {
389 self.collections
390 .get(&id)
391 .unwrap_or_else(|| panic!("collection missing: {id}"))
392 }
393
394 fn apply_constraint(&self, id: GlobalId, constraint: Constraint) -> bool {
398 let collection = self.expect_collection(id);
399 let mut bounds = collection.bounds.borrow_mut();
400 match constraint.apply(&mut bounds) {
401 Ok(changed) => {
402 if changed {
403 info!(%id, %bounds, reason = %constraint.reason, "applied as-of constraint");
404 }
405 changed
406 }
407 Err(changed) => {
408 match constraint.type_ {
409 ConstraintType::Hard => {
410 soft_panic_or_log!(
411 "failed to apply hard as-of constraint \
412 (id={id}, bounds={bounds}, constraint={constraint:?})"
413 );
414 }
415 ConstraintType::Soft => {
416 info!(%id, %bounds, ?constraint, "failed to apply soft as-of constraint");
417 }
418 }
419 changed
420 }
421 }
422 }
423
424 fn apply_upstream_storage_constraints(
432 &self,
433 storage_read_holds: &BTreeMap<GlobalId, ReadHold>,
434 ) {
435 for (id, collection) in &self.collections {
437 for input_id in &collection.storage_inputs {
438 let read_hold = &storage_read_holds[input_id];
439 let constraint = Constraint {
440 type_: ConstraintType::Hard,
441 bound_type: BoundType::Lower,
442 frontier: read_hold.since(),
443 reason: &format!("storage input {input_id} read frontier"),
444 };
445 self.apply_constraint(*id, constraint);
446 }
447 }
448
449 self.propagate_bounds_downstream(BoundType::Lower);
451 }
452
453 fn apply_downstream_storage_constraints(&self, pending_replacements: &BTreeSet<GlobalId>) {
480 for id in self.collections.keys() {
482 let Ok(frontiers) = self.storage_collections.collection_frontiers(*id) else {
483 continue;
484 };
485
486 let collection_empty =
487 PartialOrder::less_equal(&frontiers.write_frontier, &frontiers.read_capabilities);
488 let upper = if collection_empty {
489 frontiers.read_capabilities
490 } else {
491 step_back_frontier(&frontiers.write_frontier)
492 };
493
494 let type_ = if pending_replacements.contains(id) {
500 ConstraintType::Soft
501 } else {
502 ConstraintType::Hard
503 };
504 let constraint = Constraint {
505 type_,
506 bound_type: BoundType::Upper,
507 frontier: &upper,
508 reason: &format!("storage export {id} write frontier"),
509 };
510 self.apply_constraint(*id, constraint);
511 }
512
513 self.propagate_bounds_upstream(BoundType::Upper);
515 }
516
517 fn apply_warmup_constraints(&self) {
526 let mut write_frontiers = BTreeMap::new();
528 for (id, collection) in &self.collections {
529 let storage_frontiers = self
530 .storage_collections
531 .collections_frontiers(collection.storage_inputs.clone())
532 .expect("storage collections exist");
533
534 let mut write_frontier = Antichain::new();
535 for frontiers in storage_frontiers {
536 write_frontier.extend(frontiers.write_frontier);
537 }
538
539 write_frontiers.insert(*id, write_frontier);
540 }
541
542 fixpoint(|changed| {
544 for (id, collection) in &self.collections {
545 let mut write_frontier = write_frontiers.remove(id).expect("inserted above");
546 for input_id in &collection.compute_inputs {
547 let frontier = &write_frontiers[input_id];
548 *changed |= write_frontier.extend(frontier.iter().cloned());
549 }
550 write_frontiers.insert(*id, write_frontier);
551 }
552 });
553
554 for (id, write_frontier) in write_frontiers {
556 let upper = step_back_frontier(&write_frontier);
557 let constraint = Constraint {
558 type_: ConstraintType::Soft,
559 bound_type: BoundType::Upper,
560 frontier: &upper,
561 reason: &format!(
562 "warmup frontier derived from storage write frontier {:?}",
563 write_frontier.elements()
564 ),
565 };
566 self.apply_constraint(id, constraint);
567 }
568
569 self.propagate_bounds_upstream(BoundType::Upper);
571 }
572
573 fn apply_index_read_policy_constraints(&self) {
583 let mut write_frontiers = BTreeMap::new();
590 for (id, collection) in &self.collections {
591 let storage_frontiers = self
592 .storage_collections
593 .collections_frontiers(collection.storage_inputs.clone())
594 .expect("storage collections exist");
595
596 let mut write_frontier = Antichain::new();
597 for frontiers in storage_frontiers {
598 write_frontier.extend(frontiers.write_frontier);
599 }
600
601 write_frontiers.insert(*id, write_frontier);
602 }
603
604 fixpoint(|changed| {
606 for (id, collection) in &self.collections {
607 let mut write_frontier = write_frontiers.remove(id).expect("inserted above");
608 for input_id in &collection.compute_inputs {
609 let frontier = &write_frontiers[input_id];
610 *changed |= write_frontier.extend(frontier.iter().cloned());
611 }
612 write_frontiers.insert(*id, write_frontier);
613 }
614 });
615
616 for (id, collection) in &self.collections {
618 if let (true, Some(read_policy)) = (collection.is_index, &collection.read_policy) {
619 let mut write_frontier = write_frontiers.remove(id).expect("inserted above");
620 if write_frontier.is_empty() {
621 write_frontier = Antichain::from_elem(self.current_time.clone());
622 }
623 let upper = read_policy.frontier(write_frontier.borrow());
624 let constraint = Constraint {
625 type_: ConstraintType::Soft,
626 bound_type: BoundType::Upper,
627 frontier: &upper,
628 reason: &format!(
629 "read policy applied to write frontier {:?}",
630 write_frontier.elements()
631 ),
632 };
633 self.apply_constraint(*id, constraint);
634 }
635 }
636
637 self.propagate_bounds_upstream(BoundType::Upper);
639 }
640
641 fn apply_index_current_time_constraints(&self) {
648 let upper = Antichain::from_elem(self.current_time.clone());
650 for (id, collection) in &self.collections {
651 if collection.is_index {
652 let constraint = Constraint {
653 type_: ConstraintType::Soft,
654 bound_type: BoundType::Upper,
655 frontier: &upper,
656 reason: "index current time",
657 };
658 self.apply_constraint(*id, constraint);
659 }
660 }
661
662 self.propagate_bounds_upstream(BoundType::Upper);
664 }
665
666 fn propagate_bounds_downstream(&self, bound_type: BoundType) {
668 let constraint_type = match bound_type {
671 BoundType::Lower => ConstraintType::Hard,
672 BoundType::Upper => ConstraintType::Soft,
673 };
674
675 fixpoint(|changed| {
678 self.propagate_bounds_downstream_inner(bound_type, constraint_type, changed);
679
680 if bound_type == BoundType::Upper {
683 self.propagate_bounds_upstream_inner(
684 BoundType::Upper,
685 ConstraintType::Hard,
686 changed,
687 );
688 }
689 });
690 }
691
692 fn propagate_bounds_downstream_inner(
693 &self,
694 bound_type: BoundType,
695 constraint_type: ConstraintType,
696 changed: &mut bool,
697 ) {
698 for (id, collection) in &self.collections {
699 for input_id in &collection.compute_inputs {
700 let input_collection = self.expect_collection(*input_id);
701 let bounds = input_collection.bounds.borrow();
702 let constraint = Constraint {
703 type_: constraint_type,
704 bound_type,
705 frontier: bounds.get(bound_type),
706 reason: &format!("upstream {input_id} {bound_type} as-of bound"),
707 };
708 *changed |= self.apply_constraint(*id, constraint);
709 }
710 }
711 }
712
713 fn propagate_bounds_upstream(&self, bound_type: BoundType) {
715 let constraint_type = match bound_type {
718 BoundType::Lower => ConstraintType::Soft,
719 BoundType::Upper => ConstraintType::Hard,
720 };
721
722 fixpoint(|changed| {
725 self.propagate_bounds_upstream_inner(bound_type, constraint_type, changed);
726
727 if bound_type == BoundType::Lower {
730 self.propagate_bounds_downstream_inner(
731 BoundType::Lower,
732 ConstraintType::Hard,
733 changed,
734 );
735 }
736 });
737 }
738
739 fn propagate_bounds_upstream_inner(
740 &self,
741 bound_type: BoundType,
742 constraint_type: ConstraintType,
743 changed: &mut bool,
744 ) {
745 for (id, collection) in self.collections.iter().rev() {
746 let bounds = collection.bounds.borrow();
747 for input_id in &collection.compute_inputs {
748 let constraint = Constraint {
749 type_: constraint_type,
750 bound_type,
751 frontier: bounds.get(bound_type),
752 reason: &format!("downstream {id} {bound_type} as-of bound"),
753 };
754 *changed |= self.apply_constraint(*input_id, constraint);
755 }
756 }
757 }
758
759 fn best_as_of(&self, id: GlobalId) -> Antichain<Timestamp> {
766 if let Some(collection) = self.collections.get(&id) {
767 let bounds = collection.bounds.borrow();
768 bounds.upper.clone()
769 } else {
770 Antichain::new()
771 }
772 }
773
774 fn prune_sealed_persist_sinks(&mut self) {
782 self.collections.retain(|id, _| {
783 self.storage_collections
784 .collection_frontiers(*id)
785 .map_or(true, |f| !f.write_frontier.is_empty())
786 });
787 }
788
789 fn prune_dropped_collections(&mut self) {
796 let mut pruned = BTreeSet::new();
798 self.collections.retain(|id, c| {
799 let input_dropped = c.storage_inputs.iter().any(|id| {
800 let frontiers = self
801 .storage_collections
802 .collection_frontiers(*id)
803 .expect("storage collection exists");
804 frontiers.read_capabilities.is_empty()
805 });
806
807 if input_dropped {
808 pruned.insert(*id);
809 false
810 } else {
811 true
812 }
813 });
814
815 warn!(?pruned, "pruned dependants of dropped storage collections");
816
817 while !pruned.is_empty() {
819 let pruned_inputs = std::mem::take(&mut pruned);
820
821 self.collections.retain(|id, c| {
822 if c.compute_inputs.iter().any(|id| pruned_inputs.contains(id)) {
823 pruned.insert(*id);
824 false
825 } else {
826 true
827 }
828 });
829
830 warn!(?pruned, "pruned collections with pruned inputs");
831 }
832 }
833}
834
835fn fixpoint(mut step: impl FnMut(&mut bool)) {
837 loop {
838 let mut changed = false;
839 step(&mut changed);
840 if !changed {
841 break;
842 }
843 }
844}
845
846fn step_back_frontier(frontier: &Antichain<Timestamp>) -> Antichain<Timestamp> {
851 frontier
852 .iter()
853 .map(|t| t.step_back().unwrap_or(Timestamp::MIN))
854 .collect()
855}
856
857#[cfg(test)]
858mod tests {
859 use std::collections::BTreeSet;
860
861 use async_trait::async_trait;
862 use futures::future::BoxFuture;
863 use futures::stream::BoxStream;
864 use mz_compute_types::dataflows::{DataflowDescription, IndexDesc, IndexImport};
865 use mz_compute_types::sinks::ComputeSinkConnection;
866 use mz_compute_types::sinks::ComputeSinkDesc;
867 use mz_compute_types::sinks::MaterializedViewSinkConnection;
868 use mz_compute_types::sources::SourceInstanceArguments;
869 use mz_compute_types::sources::SourceInstanceDesc;
870 use mz_persist_client::stats::{SnapshotPartsStats, SnapshotStats};
871 use mz_persist_types::ShardId;
872 use mz_repr::{RelationDesc, RelationVersion, Row, SqlRelationType};
873 use mz_repr::{ReprRelationType, Timestamp};
874 use mz_storage_client::client::TimestamplessUpdateBuilder;
875 use mz_storage_client::controller::{CollectionDescription, StorageMetadata, StorageTxn};
876 use mz_storage_client::storage_collections::{CollectionFrontiers, SnapshotCursor};
877 use mz_storage_types::StorageDiff;
878 use mz_storage_types::controller::{CollectionMetadata, StorageError};
879 use mz_storage_types::errors::CollectionMissing;
880 use mz_storage_types::parameters::StorageParameters;
881 use mz_storage_types::sources::SourceData;
882 use mz_storage_types::time_dependence::{TimeDependence, TimeDependenceError};
883
884 use super::*;
885
886 const SEALED: u64 = 0x5ea1ed;
887
888 fn ts_to_frontier(ts: u64) -> Antichain<Timestamp> {
889 if ts == SEALED {
890 Antichain::new()
891 } else {
892 Antichain::from_elem(ts.into())
893 }
894 }
895
896 #[derive(Debug)]
897 struct StorageFrontiers(BTreeMap<GlobalId, (Antichain<Timestamp>, Antichain<Timestamp>)>);
898
899 #[async_trait]
900 impl StorageCollections for StorageFrontiers {
901 async fn initialize_state(
902 &self,
903 _txn: &mut (dyn StorageTxn + Send),
904 _init_ids: BTreeSet<GlobalId>,
905 ) -> Result<(), StorageError> {
906 unimplemented!()
907 }
908
909 fn update_parameters(&self, _config_params: StorageParameters) {
910 unimplemented!()
911 }
912
913 fn collection_metadata(
914 &self,
915 _id: GlobalId,
916 ) -> Result<CollectionMetadata, CollectionMissing> {
917 unimplemented!()
918 }
919
920 fn active_collection_metadatas(&self) -> Vec<(GlobalId, CollectionMetadata)> {
921 unimplemented!()
922 }
923
924 fn collections_frontiers(
925 &self,
926 ids: Vec<GlobalId>,
927 ) -> Result<Vec<CollectionFrontiers>, CollectionMissing> {
928 let mut frontiers = Vec::with_capacity(ids.len());
929 for id in ids {
930 let (read, write) = self.0.get(&id).ok_or(CollectionMissing(id))?;
931 frontiers.push(CollectionFrontiers {
932 id,
933 write_frontier: write.clone(),
934 implied_capability: read.clone(),
935 read_capabilities: read.clone(),
936 })
937 }
938 Ok(frontiers)
939 }
940
941 fn active_collection_frontiers(&self) -> Vec<CollectionFrontiers> {
942 unimplemented!()
943 }
944
945 fn check_exists(&self, _id: GlobalId) -> Result<(), StorageError> {
946 unimplemented!()
947 }
948
949 async fn snapshot_stats(
950 &self,
951 _id: GlobalId,
952 _as_of: Antichain<Timestamp>,
953 ) -> Result<SnapshotStats, StorageError> {
954 unimplemented!()
955 }
956
957 async fn snapshot_parts_stats(
958 &self,
959 _id: GlobalId,
960 _as_of: Antichain<Timestamp>,
961 ) -> BoxFuture<'static, Result<SnapshotPartsStats, StorageError>> {
962 unimplemented!()
963 }
964
965 fn snapshot(
966 &self,
967 _id: GlobalId,
968 _as_of: Timestamp,
969 ) -> BoxFuture<'static, Result<Vec<(Row, StorageDiff)>, StorageError>> {
970 unimplemented!()
971 }
972
973 async fn snapshot_latest(&self, _id: GlobalId) -> Result<Vec<Row>, StorageError> {
974 unimplemented!()
975 }
976
977 fn snapshot_cursor(
978 &self,
979 _id: GlobalId,
980 _as_of: Timestamp,
981 ) -> BoxFuture<'static, Result<SnapshotCursor, StorageError>> {
982 unimplemented!()
983 }
984
985 fn snapshot_and_stream(
986 &self,
987 _id: GlobalId,
988 _as_of: Timestamp,
989 ) -> BoxFuture<
990 'static,
991 Result<BoxStream<'static, (SourceData, Timestamp, StorageDiff)>, StorageError>,
992 > {
993 unimplemented!()
994 }
995
996 fn create_update_builder(
997 &self,
998 _id: GlobalId,
999 ) -> BoxFuture<
1000 'static,
1001 Result<TimestamplessUpdateBuilder<SourceData, (), StorageDiff>, StorageError>,
1002 > {
1003 unimplemented!()
1004 }
1005
1006 async fn prepare_state(
1007 &self,
1008 _txn: &mut (dyn StorageTxn + Send),
1009 _ids_to_add: BTreeSet<GlobalId>,
1010 _ids_to_drop: BTreeSet<GlobalId>,
1011 _ids_to_register: BTreeMap<GlobalId, ShardId>,
1012 ) -> Result<(), StorageError> {
1013 unimplemented!()
1014 }
1015
1016 async fn create_collections_for_bootstrap(
1017 &self,
1018 _storage_metadata: &StorageMetadata,
1019 _register_ts: Option<Timestamp>,
1020 _collections: Vec<(GlobalId, CollectionDescription)>,
1021 _migrated_storage_collections: &BTreeSet<GlobalId>,
1022 ) -> Result<(), StorageError> {
1023 unimplemented!()
1024 }
1025
1026 async fn alter_table_desc(
1027 &self,
1028 _existing_collection: GlobalId,
1029 _new_collection: GlobalId,
1030 _new_desc: RelationDesc,
1031 _expected_version: RelationVersion,
1032 ) -> Result<(), StorageError> {
1033 unimplemented!()
1034 }
1035
1036 fn drop_collections_unvalidated(
1037 &self,
1038 _storage_metadata: &StorageMetadata,
1039 _identifiers: Vec<GlobalId>,
1040 ) {
1041 unimplemented!()
1042 }
1043
1044 fn set_read_policies(&self, _policies: Vec<(GlobalId, ReadPolicy)>) {
1045 unimplemented!()
1046 }
1047
1048 fn acquire_read_holds(
1049 &self,
1050 desired_holds: Vec<GlobalId>,
1051 ) -> Result<Vec<ReadHold>, CollectionMissing> {
1052 let mut holds = Vec::with_capacity(desired_holds.len());
1053 for id in desired_holds {
1054 let (read, _write) = self.0.get(&id).ok_or(CollectionMissing(id))?;
1055 let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
1056 holds.push(ReadHold::with_channel(id, read.clone(), tx));
1057 }
1058 Ok(holds)
1059 }
1060
1061 fn determine_time_dependence(
1062 &self,
1063 _id: GlobalId,
1064 ) -> Result<Option<TimeDependence>, TimeDependenceError> {
1065 unimplemented!()
1066 }
1067
1068 fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
1069 unimplemented!()
1070 }
1071 }
1072
1073 fn dataflow(
1074 export_id: &str,
1075 input_ids: &[&str],
1076 storage_ids: &BTreeSet<&str>,
1077 ) -> LirDataflowDescription {
1078 let source_imports = input_ids
1079 .iter()
1080 .filter(|s| storage_ids.contains(*s))
1081 .map(|s| {
1082 let id = s.parse().unwrap();
1083 let desc = SourceInstanceDesc {
1084 arguments: SourceInstanceArguments {
1085 operators: Default::default(),
1086 },
1087 storage_metadata: Default::default(),
1088 typ: SqlRelationType::empty(),
1089 };
1090 (
1091 id,
1092 mz_compute_types::dataflows::SourceImport {
1093 desc,
1094 monotonic: Default::default(),
1095 with_snapshot: true,
1096 upper: Default::default(),
1097 },
1098 )
1099 })
1100 .collect();
1101 let index_imports = input_ids
1102 .iter()
1103 .filter(|s| !storage_ids.contains(*s))
1104 .map(|s| {
1105 let id = s.parse().unwrap();
1106 let import = IndexImport {
1107 desc: IndexDesc {
1108 on_id: GlobalId::Transient(0),
1109 key: Default::default(),
1110 },
1111 typ: ReprRelationType::empty(),
1112 monotonic: Default::default(),
1113 with_snapshot: true,
1114 };
1115 (id, import)
1116 })
1117 .collect();
1118 let index_exports = std::iter::once(export_id)
1119 .filter(|s| !storage_ids.contains(*s))
1120 .map(|sid| {
1121 let id = sid.parse().unwrap();
1122 let desc = IndexDesc {
1123 on_id: GlobalId::Transient(0),
1124 key: Default::default(),
1125 };
1126 let typ = ReprRelationType::empty();
1127 (id, (desc, typ))
1128 })
1129 .collect();
1130 let sink_exports = std::iter::once(export_id)
1131 .filter(|s| storage_ids.contains(*s))
1132 .map(|sid| {
1133 let id = sid.parse().unwrap();
1134 let desc = ComputeSinkDesc {
1135 from: GlobalId::Transient(0),
1136 from_desc: RelationDesc::empty(),
1137 connection: ComputeSinkConnection::MaterializedView(
1138 MaterializedViewSinkConnection {
1139 value_desc: RelationDesc::empty(),
1140 storage_metadata: Default::default(),
1141 },
1142 ),
1143 with_snapshot: Default::default(),
1144 up_to: Default::default(),
1145 non_null_assertions: Default::default(),
1146 refresh_schedule: Default::default(),
1147 from_arrangement: None,
1148 };
1149 (id, desc)
1150 })
1151 .collect();
1152
1153 DataflowDescription {
1154 source_imports,
1155 index_imports,
1156 objects_to_build: Default::default(),
1157 index_exports,
1158 sink_exports,
1159 as_of: None,
1160 until: Default::default(),
1161 initial_storage_as_of: Default::default(),
1162 refresh_schedule: Default::default(),
1163 debug_name: Default::default(),
1164 time_dependence: None,
1165 }
1166 }
1167
1168 macro_rules! testcase {
1169 ($name:ident, {
1170 storage: { $( $storage_id:literal: ($read:expr, $write:expr), )* },
1171 dataflows: [ $( $export_id:literal <- $inputs:expr => $as_of:expr, )* ],
1172 current_time: $current_time:literal,
1173 $( read_policies: { $( $policy_id:literal: $policy:expr, )* }, )?
1174 $( read_only: $read_only:expr, )?
1175 $( pending_replacements: [ $( $replacement_id:literal, )* ], )?
1176 }) => {
1177 #[mz_ore::test]
1178 fn $name() {
1179 let storage_ids = [$( $storage_id, )*].into();
1180
1181 let storage_frontiers = StorageFrontiers(BTreeMap::from([
1182 $(
1183 (
1184 $storage_id.parse().unwrap(),
1185 (ts_to_frontier($read), ts_to_frontier($write)),
1186 ),
1187 )*
1188 ]));
1189
1190 let mut dataflows = [
1191 $(
1192 dataflow($export_id, &$inputs, &storage_ids),
1193 )*
1194 ];
1195
1196 let read_policies = BTreeMap::from([
1197 $($( ($policy_id.parse().unwrap(), $policy), )*)?
1198 ]);
1199
1200 #[allow(unused_variables)]
1201 let read_only = false;
1202 $( let read_only = $read_only; )?
1203
1204 let pending_replacements = BTreeSet::from([
1205 $($( $replacement_id.parse().unwrap(), )*)?
1206 ]);
1207
1208 super::run(
1209 &mut dataflows,
1210 &read_policies,
1211 &storage_frontiers,
1212 $current_time.into(),
1213 read_only,
1214 &pending_replacements,
1215 );
1216
1217 let actual_as_ofs: Vec<_> = dataflows
1218 .into_iter()
1219 .map(|d| d.as_of.unwrap())
1220 .collect();
1221 let expected_as_ofs = [ $( ts_to_frontier($as_of), )* ];
1222
1223 assert_eq!(actual_as_ofs, expected_as_ofs);
1224 }
1225 };
1226 }
1227
1228 testcase!(upstream_storage_constraints, {
1229 storage: {
1230 "s1": (10, 20),
1231 "s2": (20, 30),
1232 },
1233 dataflows: [
1234 "u1" <- ["s1"] => 10,
1235 "u2" <- ["s2"] => 20,
1236 "u3" <- ["s1", "s2"] => 20,
1237 "u4" <- ["u1", "u2"] => 20,
1238 ],
1239 current_time: 0,
1240 });
1241
1242 testcase!(downstream_storage_constraints, {
1243 storage: {
1244 "s1": (10, 20),
1245 "u3": (10, 15),
1246 "u4": (10, 13),
1247 },
1248 dataflows: [
1249 "u1" <- ["s1"] => 19,
1250 "u2" <- ["s1"] => 12,
1251 "u3" <- ["u2"] => 14,
1252 "u4" <- ["u2"] => 12,
1253 ],
1254 current_time: 100,
1255 });
1256
1257 testcase!(warmup_constraints, {
1258 storage: {
1259 "s1": (10, 20),
1260 "s2": (10, 30),
1261 "s3": (10, 40),
1262 "s4": (10, 50),
1263 },
1264 dataflows: [
1265 "u1" <- ["s1"] => 19,
1266 "u2" <- ["s2"] => 19,
1267 "u3" <- ["s3"] => 39,
1268 "u4" <- ["s4"] => 39,
1269 "u5" <- ["u1", "u2"] => 19,
1270 "u6" <- ["u3", "u4"] => 39,
1271 ],
1272 current_time: 100,
1273 });
1274
1275 testcase!(index_read_policy_constraints, {
1276 storage: {
1277 "s1": (10, 20),
1278 "u6": (10, 18),
1279 },
1280 dataflows: [
1281 "u1" <- ["s1"] => 15,
1282 "u2" <- ["s1"] => 10,
1283 "u3" <- ["s1"] => 13,
1284 "u4" <- ["s1"] => 10,
1285 "u5" <- [] => 95,
1286 "u6" <- ["s1"] => 17,
1287 ],
1288 current_time: 100,
1289 read_policies: {
1290 "u1": ReadPolicy::lag_writes_by(5.into(), 1.into()),
1291 "u2": ReadPolicy::lag_writes_by(15.into(), 1.into()),
1292 "u3": ReadPolicy::ValidFrom(Antichain::from_elem(13.into())),
1293 "u4": ReadPolicy::ValidFrom(Antichain::from_elem(5.into())),
1294 "u5": ReadPolicy::lag_writes_by(5.into(), 1.into()),
1295 "u6": ReadPolicy::ValidFrom(Antichain::from_elem(13.into())),
1296 },
1297 });
1298
1299 testcase!(index_current_time_constraints, {
1300 storage: {
1301 "s1": (10, 20),
1302 "s2": (20, 30),
1303 "u4": (10, 12),
1304 "u5": (10, 18),
1305 },
1306 dataflows: [
1307 "u1" <- ["s1"] => 15,
1308 "u2" <- ["s2"] => 20,
1309 "u3" <- ["s1"] => 11,
1310 "u4" <- ["u3"] => 11,
1311 "u5" <- ["s1"] => 17,
1312 "u6" <- [] => 15,
1313 ],
1314 current_time: 15,
1315 });
1316
1317 testcase!(sealed_storage_sink, {
1318 storage: {
1319 "s1": (10, 20),
1320 "u1": (10, SEALED),
1321 },
1322 dataflows: [
1323 "u1" <- ["s1"] => SEALED,
1324 ],
1325 current_time: 100,
1326 });
1327
1328 testcase!(read_only_dropped_storage_inputs, {
1329 storage: {
1330 "s1": (10, 20),
1331 "s2": (SEALED, SEALED),
1332 "u4": (10, 20),
1333 },
1334 dataflows: [
1335 "u1" <- ["s1"] => 15,
1336 "u2" <- ["s2"] => SEALED,
1337 "u3" <- ["s1", "s2"] => SEALED,
1338 "u4" <- ["u2"] => SEALED,
1339 ],
1340 current_time: 15,
1341 read_only: true,
1342 });
1343
1344 testcase!(pending_replacements, {
1348 storage: {
1349 "s1": (10, 30),
1350 "s2": (20, 30),
1351 "u2": (10, 15),
1352 "u3": (10, 15),
1353 },
1354 dataflows: [
1355 "u2" <- ["s2"] => 20,
1356 "u3" <- ["s1"] => 14,
1357 ],
1358 current_time: 100,
1359 pending_replacements: ["u2", "u3",],
1360 });
1361
1362 testcase!(github_9273, {
1364 storage: {
1365 "s1": (10, 20),
1366 "u3": (14, 15),
1367 },
1368 dataflows: [
1369 "u1" <- ["s1"] => 14,
1370 "u2" <- ["u1"] => 19,
1371 "u3" <- ["u1"] => 14,
1372 ],
1373 current_time: 100,
1374 read_policies: {
1375 "u1": ReadPolicy::lag_writes_by(1.into(), 1.into()),
1376 "u2": ReadPolicy::lag_writes_by(1.into(), 1.into()),
1377 },
1378 });
1379}