@@ -52,7 +52,7 @@ use mz_expr::row::RowCollection;
5252use mz_ore:: cast:: CastFrom ;
5353use mz_ore:: metrics:: MetricsRegistry ;
5454use mz_ore:: now:: NowFn ;
55- use mz_ore:: soft_assert_no_log ;
55+ use mz_ore:: soft_assert_or_log ;
5656use mz_ore:: tracing:: OpenTelemetryContext ;
5757use mz_persist_types:: PersistLocation ;
5858use mz_repr:: { GlobalId , RelationDesc , Row , Timestamp } ;
@@ -813,25 +813,43 @@ impl ComputeController {
813813 return Err ( EmptyAsOfForCopyTo ) ;
814814 }
815815
816- // Validation: every import is read
816+ // Validation: the dataflow exports something
817817 //
818- // The import list is meant to be exactly the imports the exports read. `optimize_dataflow`
819- // prunes it to that, and the read holds, the time dependence, and the persist sources the
820- // replicas build are all derived from it below, so each of them describes a dataflow other
821- // than the one that will run once the list is loose. This catches a producer that stops
822- // pruning.
818+ // An export-less description has nothing to render and no answer to "what do the exports
819+ // read", which the checks below are phrased in terms of. `optimize_dataflow` leaves such a
820+ // description's imports alone for that reason, so one arriving here would fail the import
821+ // check for the wrong reason.
822+ soft_assert_or_log ! (
823+ !dataflow. index_exports. is_empty( ) || !dataflow. sink_exports. is_empty( ) ,
824+ "dataflow {} has no exports" ,
825+ dataflow. debug_name,
826+ ) ;
827+
828+ // The imports the exports actually read. `optimize_dataflow` prunes the import list to
829+ // exactly this set, so the two agree unless a producer stopped pruning.
823830 //
824- // `soft_assert_no_log!` rather than a logging variant because the check walks the plan and
825- // this runs per peek, and the walk is worth paying for only where it can fail a test.
826- soft_assert_no_log ! (
827- {
828- let used = dataflow. used_import_ids( ) ;
829- dataflow. import_ids( ) . all( |id| used. contains( & id) )
830- } ,
831+ // Computed once and used twice: the check below reports a loose list, and
832+ // `determine_time_dependence` counts through it rather than over the raw list. That
833+ // consumer is the one whose wrong answer hangs an environment: an import no export reads
834+ // would report wall-clock dependence for a dataflow whose exports are constant, earning it
835+ // a dataflow expiration that pins the output frontier days short of the empty antichain,
836+ // and nothing downstream would learn the collection is final. Deriving it from this set
837+ // makes that correct by construction, leaving the prune to reclaim the read hold and the
838+ // persist source.
839+ let used_imports = dataflow. used_import_ids ( ) ;
840+
841+ // Validation: every import is read
842+ //
843+ // The read holds and the persist sources the replicas build are still derived from the raw
844+ // list below, so a loose one describes a dataflow other than the one that will run. A
845+ // logging variant rather than `soft_assert_no_log!`: the walk is paid for above either way,
846+ // so reporting it in production costs only the comparison.
847+ soft_assert_or_log ! (
848+ dataflow. import_ids( ) . all( |id| used_imports. contains( & id) ) ,
831849 "dataflow {} imports collections no export reads: imports {:?}, read {:?}" ,
832850 dataflow. debug_name,
833851 dataflow. import_ids( ) . collect:: <Vec <_>>( ) ,
834- dataflow . used_import_ids ( ) ,
852+ used_imports ,
835853 ) ;
836854
837855 // Validation: input collections
@@ -854,7 +872,7 @@ impl ComputeController {
854872 }
855873 }
856874 let time_dependence = self
857- . determine_time_dependence ( instance_id, & dataflow)
875+ . determine_time_dependence ( instance_id, & dataflow, & used_imports )
858876 . expect ( "must exist" ) ;
859877
860878 let instance = self . instance_mut ( instance_id) . expect ( "validated" ) ;
@@ -1036,31 +1054,43 @@ impl ComputeController {
10361054 }
10371055
10381056 /// Determine the time dependence for a dataflow.
1057+ ///
1058+ /// `used_imports` are the imports the exports read, as
1059+ /// [`DataflowDescription::used_import_ids`] reports them. Only those count: an import no export
1060+ /// reads would report wall-clock dependence for a dataflow whose exports are constant, and that
1061+ /// earns it a dataflow expiration, which pins its output frontier at the expiration time. A
1062+ /// constant export's frontier is the empty antichain, so the pin would hold it days short of
1063+ /// the truth and whoever reads that frontier would never learn the collection can no longer
1064+ /// change.
1065+ ///
1066+ /// `optimize_dataflow` prunes the import list to this set, so the two agree and the filtering
1067+ /// is a no-op. It is here because this is the consumer whose wrong answer hangs an environment,
1068+ /// and deriving the answer from the read set makes it independent of the list staying tight.
10391069 fn determine_time_dependence (
10401070 & self ,
10411071 instance_id : ComputeInstanceId ,
10421072 dataflow : & DataflowDescription < mz_compute_types:: plan:: LirRelationExpr , ( ) > ,
1073+ used_imports : & BTreeSet < GlobalId > ,
10431074 ) -> Result < Option < TimeDependence > , TimeDependenceError > {
10441075 let instance = self
10451076 . instance ( instance_id)
10461077 . map_err ( |err| TimeDependenceError :: InstanceMissing ( err. 0 ) ) ?;
10471078 let mut time_dependencies = Vec :: new ( ) ;
10481079
1049- // Every import counts, which is only the right answer because the import list is the set of
1050- // imports the exports read. An import no export reads would report wall-clock dependence
1051- // for a dataflow whose exports are constant, and that earns it a dataflow expiration, which
1052- // pins its output frontier at the expiration time. A constant export's frontier is the
1053- // empty antichain, so the pin would hold it days short of the truth and whoever reads that
1054- // frontier would never learn the collection can no longer change. `create_dataflow` asserts
1055- // the list is tight before we get here.
1056- for id in dataflow. imported_index_ids ( ) {
1080+ for id in dataflow
1081+ . imported_index_ids ( )
1082+ . filter ( |id| used_imports. contains ( id) )
1083+ {
10571084 let dependence = instance
10581085 . get_time_dependence ( id)
10591086 . map_err ( |err| TimeDependenceError :: CollectionMissing ( err. 0 ) ) ?;
10601087 time_dependencies. push ( dependence) ;
10611088 }
10621089
1063- ' source: for id in dataflow. imported_source_ids ( ) {
1090+ ' source: for id in dataflow
1091+ . imported_source_ids ( )
1092+ . filter ( |id| used_imports. contains ( id) )
1093+ {
10641094 // We first check whether the id is backed by a compute object, in which case we use
10651095 // the time dependence we know. This is true for storage sinks.
10661096 for instance in self . instances . values ( ) {
0 commit comments