@@ -15,12 +15,14 @@ use std::fmt::{Display, Write};
1515use std:: rc:: Rc ;
1616use std:: time:: { Duration , Instant } ;
1717
18+ use chrono:: { DateTime , Utc } ;
1819use columnar:: { Columnar , Index , Ref } ;
1920use differential_dataflow:: VecCollection ;
2021use differential_dataflow:: collection:: AsCollection ;
2122use differential_dataflow:: trace:: { BatchReader , Cursor , Navigable } ;
2223use mz_compute_types:: plan:: LirId ;
2324use mz_ore:: cast:: CastFrom ;
25+ use mz_repr:: adt:: timestamp:: CheckedTimestamp ;
2426use mz_repr:: { Datum , Diff , GlobalId , Row , RowRef , Timestamp } ;
2527use mz_timely_util:: columnar:: batcher;
2628use mz_timely_util:: columnar:: builder:: ColumnBuilder ;
@@ -163,6 +165,13 @@ pub struct Hydration {
163165 pub export_id : GlobalId ,
164166}
165167
168+ /// An export began hydrating.
169+ #[ derive( Debug , Clone , PartialOrd , PartialEq , Columnar ) ]
170+ pub struct HydrationStart {
171+ /// Identifier of the export.
172+ pub export_id : GlobalId ,
173+ }
174+
166175/// An operator's hydration status changed.
167176#[ derive( Debug , Clone , PartialOrd , PartialEq , Columnar ) ]
168177pub struct OperatorHydration {
@@ -225,6 +234,8 @@ pub enum ComputeEvent {
225234 DataflowShutdown ( DataflowShutdown ) ,
226235 /// The number of errors in a dataflow export has changed.
227236 ErrorCount ( ErrorCount ) ,
237+ /// A dataflow export began hydrating.
238+ HydrationStart ( HydrationStart ) ,
228239 /// A dataflow export was hydrated.
229240 Hydration ( Hydration ) ,
230241 /// A dataflow operator's hydration status changed.
@@ -620,11 +631,17 @@ impl DemuxState {
620631 & mut self ,
621632 export_id : GlobalId ,
622633 time_ns : Option < u64 > ,
634+ installed_at : CheckedTimestamp < DateTime < Utc > > ,
635+ started_at : Option < CheckedTimestamp < DateTime < Utc > > > ,
636+ hydrated_at : Option < CheckedTimestamp < DateTime < Utc > > > ,
623637 ) -> ( & RowRef , & RowRef ) {
624638 self . hydration_time_packer . pack_slice ( & [
625639 make_string_datum ( export_id, & mut self . scratch_string_a ) ,
626640 Datum :: UInt64 ( u64:: cast_from ( self . worker_id ) ) ,
627641 Datum :: from ( time_ns) ,
642+ Datum :: TimestampTz ( installed_at) ,
643+ started_at. map_or ( Datum :: Null , Datum :: TimestampTz ) ,
644+ hydrated_at. map_or ( Datum :: Null , Datum :: TimestampTz ) ,
628645 ] )
629646 }
630647
@@ -732,18 +749,27 @@ struct ExportState {
732749 error_count : Diff ,
733750 /// When this export was created.
734751 created_at : Instant ,
752+ /// Wall-clock time at which this export was installed.
753+ installed_at : CheckedTimestamp < DateTime < Utc > > ,
754+ /// Wall-clock time at which this export began hydrating.
755+ started_at : Option < CheckedTimestamp < DateTime < Utc > > > ,
756+ /// Wall-clock time at which this export finished hydrating.
757+ hydrated_at : Option < CheckedTimestamp < DateTime < Utc > > > ,
735758 /// Whether the exported collection is hydrated.
736759 hydration_time_ns : Option < u64 > ,
737760 /// Hydration status of operators feeding this export.
738761 operator_hydration : BTreeMap < LirId , bool > ,
739762}
740763
741764impl ExportState {
742- fn new ( dataflow_index : usize ) -> Self {
765+ fn new ( dataflow_index : usize , installed_at : CheckedTimestamp < DateTime < Utc > > ) -> Self {
743766 Self {
744767 dataflow_index,
745768 error_count : Diff :: ZERO ,
746769 created_at : Instant :: now ( ) ,
770+ installed_at,
771+ started_at : None ,
772+ hydrated_at : None ,
747773 hydration_time_ns : None ,
748774 operator_hydration : BTreeMap :: new ( ) ,
749775 }
@@ -799,6 +825,21 @@ impl DemuxHandler<'_, '_, '_> {
799825 rounded. try_into ( ) . expect ( "must fit" )
800826 }
801827
828+ /// Returns the event's wall-clock time, unrounded.
829+ ///
830+ /// Unlike `ts`, which rounds up to the logging interval to publish an update,
831+ /// this is the time the event actually happened. Reading `self.time` as a
832+ /// Unix duration is only valid because the logging clock is anchored at the
833+ /// epoch. A boot-relative anchor would silently yield 1970 timestamps in a
834+ /// durable table.
835+ fn event_timestamp ( & self ) -> CheckedTimestamp < DateTime < Utc > > {
836+ let seconds = i64:: try_from ( self . time . as_secs ( ) ) . expect ( "event timestamp must fit" ) ;
837+ DateTime :: from_timestamp ( seconds, self . time . subsec_nanos ( ) )
838+ . expect ( "event timestamp must be valid" )
839+ . try_into ( )
840+ . expect ( "event timestamp must fit" )
841+ }
842+
802843 /// Handle the given compute event.
803844 fn handle ( & mut self , event : Ref < ' _ , ComputeEvent > ) {
804845 use ComputeEventReference :: * ;
@@ -818,6 +859,7 @@ impl DemuxHandler<'_, '_, '_> {
818859 }
819860 DataflowShutdown ( shutdown) => self . handle_dataflow_shutdown ( shutdown) ,
820861 ErrorCount ( error_count) => self . handle_error_count ( error_count) ,
862+ HydrationStart ( hydration) => self . handle_hydration_start ( hydration) ,
821863 Hydration ( hydration) => self . handle_hydration ( hydration) ,
822864 OperatorHydration ( hydration) => self . handle_operator_hydration ( hydration) ,
823865 LirMapping ( mapping) => self . handle_lir_mapping ( mapping) ,
@@ -834,19 +876,36 @@ impl DemuxHandler<'_, '_, '_> {
834876 ) {
835877 let export_id = Columnar :: into_owned ( export_id) ;
836878 let ts = self . ts ( ) ;
879+ let installed_at = self . event_timestamp ( ) ;
837880 let datum = self . state . pack_export_update ( export_id, dataflow_index) ;
838881 self . output . export . give ( ( datum, ts, Diff :: ONE ) ) ;
839882
840883 let existing = self
841884 . state
842885 . exports
843- . insert ( export_id, ExportState :: new ( dataflow_index) ) ;
844- if existing . is_some ( ) {
886+ . insert ( export_id, ExportState :: new ( dataflow_index, installed_at ) ) ;
887+ if let Some ( existing ) = existing {
845888 error ! ( %export_id, "export already registered" ) ;
889+ // The stale row's value carries its own timestamps, so it is not
890+ // identical to the one we are about to insert. Retract it here or it
891+ // stays in the collection forever: the drop handler only ever sees
892+ // the state we just overwrote it with.
893+ let datum = self . state . pack_hydration_time_update (
894+ export_id,
895+ existing. hydration_time_ns ,
896+ existing. installed_at ,
897+ existing. started_at ,
898+ existing. hydrated_at ,
899+ ) ;
900+ self . output
901+ . hydration_time
902+ . give ( ( datum, ts, Diff :: MINUS_ONE ) ) ;
846903 }
847904
848905 // Insert hydration time logging for this export.
849- let datum = self . state . pack_hydration_time_update ( export_id, None ) ;
906+ let datum =
907+ self . state
908+ . pack_hydration_time_update ( export_id, None , installed_at, None , None ) ;
850909 self . output . hydration_time . give ( ( datum, ts, Diff :: ONE ) ) ;
851910 }
852911
@@ -875,9 +934,13 @@ impl DemuxHandler<'_, '_, '_> {
875934 }
876935
877936 // Remove hydration time logging for this export.
878- let datum = self
879- . state
880- . pack_hydration_time_update ( export_id, export. hydration_time_ns ) ;
937+ let datum = self . state . pack_hydration_time_update (
938+ export_id,
939+ export. hydration_time_ns ,
940+ export. installed_at ,
941+ export. started_at ,
942+ export. hydrated_at ,
943+ ) ;
881944 self . output
882945 . hydration_time
883946 . give ( ( datum, ts, Diff :: MINUS_ONE ) ) ;
@@ -961,31 +1024,105 @@ impl DemuxHandler<'_, '_, '_> {
9611024 }
9621025 }
9631026
964- fn handle_hydration ( & mut self , HydrationReference { export_id } : Ref < ' _ , Hydration > ) {
1027+ fn handle_hydration_start (
1028+ & mut self ,
1029+ HydrationStartReference { export_id } : Ref < ' _ , HydrationStart > ,
1030+ ) {
9651031 let ts = self . ts ( ) ;
1032+ let started_at = self . event_timestamp ( ) ;
9661033 let export_id = Columnar :: into_owned ( export_id) ;
9671034
968- let Some ( export) = self . state . exports . get_mut ( & export_id) else {
969- error ! ( %export_id, "hydration event for unknown export" ) ;
970- return ;
1035+ let ( old, new) = {
1036+ let Some ( export) = self . state . exports . get_mut ( & export_id) else {
1037+ error ! ( %export_id, "hydration start event for unknown export" ) ;
1038+ return ;
1039+ } ;
1040+ // A `Schedule` is delivered even for a collection that already
1041+ // hydrated, because `StartSignal` gates only imported inputs and an
1042+ // import-free dataflow is never suspended. Recording that late
1043+ // arrival as the start of hydration would invent an interval that
1044+ // was never observed, so leave `started_at` NULL instead. Repeat
1045+ // events after reconciliation are ignored for the same reason.
1046+ if export. started_at . is_some ( ) || export. hydrated_at . is_some ( ) {
1047+ return ;
1048+ }
1049+
1050+ let old = (
1051+ export. hydration_time_ns ,
1052+ export. installed_at ,
1053+ export. started_at ,
1054+ export. hydrated_at ,
1055+ ) ;
1056+ export. started_at = Some ( started_at) ;
1057+ let new = (
1058+ export. hydration_time_ns ,
1059+ export. installed_at ,
1060+ export. started_at ,
1061+ export. hydrated_at ,
1062+ ) ;
1063+ ( old, new)
9711064 } ;
972- if export. hydration_time_ns . is_some ( ) {
973- // Hydration events for already hydrated dataflows can occur when a dataflow is reused
974- // after reconciliation. We can simply ignore these.
975- return ;
976- }
9771065
978- let duration = export. created_at . elapsed ( ) ;
979- let nanos = u64:: try_from ( duration. as_nanos ( ) ) . expect ( "must fit" ) ;
980- export. hydration_time_ns = Some ( nanos) ;
1066+ let retraction = self
1067+ . state
1068+ . pack_hydration_time_update ( export_id, old. 0 , old. 1 , old. 2 , old. 3 ) ;
1069+ self . output
1070+ . hydration_time
1071+ . give ( ( retraction, ts, Diff :: MINUS_ONE ) ) ;
1072+ let insertion = self
1073+ . state
1074+ . pack_hydration_time_update ( export_id, new. 0 , new. 1 , new. 2 , new. 3 ) ;
1075+ self . output . hydration_time . give ( ( insertion, ts, Diff :: ONE ) ) ;
1076+ }
1077+
1078+ fn handle_hydration ( & mut self , HydrationReference { export_id } : Ref < ' _ , Hydration > ) {
1079+ let ts = self . ts ( ) ;
1080+ let hydrated_at = self . event_timestamp ( ) ;
1081+ let export_id = Columnar :: into_owned ( export_id) ;
1082+
1083+ let ( old, new) = {
1084+ let Some ( export) = self . state . exports . get_mut ( & export_id) else {
1085+ error ! ( %export_id, "hydration event for unknown export" ) ;
1086+ return ;
1087+ } ;
1088+ if export. hydration_time_ns . is_some ( ) {
1089+ // Hydration events for already hydrated dataflows can occur when a dataflow is
1090+ // reused after reconciliation. We can simply ignore these.
1091+ return ;
1092+ }
1093+
1094+ let old = (
1095+ export. hydration_time_ns ,
1096+ export. installed_at ,
1097+ export. started_at ,
1098+ export. hydrated_at ,
1099+ ) ;
1100+ let duration = export. created_at . elapsed ( ) ;
1101+ export. hydration_time_ns = Some ( u64:: try_from ( duration. as_nanos ( ) ) . expect ( "must fit" ) ) ;
1102+ // `started_at` stays as it is, including NULL. An import-free
1103+ // dataflow can hydrate without ever being suspended, and we do not
1104+ // invent a start we never observed. Consumers read a NULL start as
1105+ // "not observed", which keeps `installed_at <= started_at <=
1106+ // hydrated_at` true for every non-NULL pair.
1107+ export. hydrated_at = Some ( hydrated_at) ;
1108+ let new = (
1109+ export. hydration_time_ns ,
1110+ export. installed_at ,
1111+ export. started_at ,
1112+ export. hydrated_at ,
1113+ ) ;
1114+ ( old, new)
1115+ } ;
9811116
982- let retraction = self . state . pack_hydration_time_update ( export_id, None ) ;
1117+ let retraction = self
1118+ . state
1119+ . pack_hydration_time_update ( export_id, old. 0 , old. 1 , old. 2 , old. 3 ) ;
9831120 self . output
9841121 . hydration_time
9851122 . give ( ( retraction, ts, Diff :: MINUS_ONE ) ) ;
9861123 let insertion = self
9871124 . state
988- . pack_hydration_time_update ( export_id, Some ( nanos ) ) ;
1125+ . pack_hydration_time_update ( export_id, new . 0 , new . 1 , new . 2 , new . 3 ) ;
9891126 self . output . hydration_time . give ( ( insertion, ts, Diff :: ONE ) ) ;
9901127 }
9911128
@@ -1394,6 +1531,14 @@ impl CollectionLogging {
13941531 }
13951532 }
13961533
1534+ /// Records that the collection began hydrating.
1535+ pub fn set_started ( & self ) {
1536+ self . logger
1537+ . log ( & ComputeEvent :: HydrationStart ( HydrationStart {
1538+ export_id : self . export_id ,
1539+ } ) ) ;
1540+ }
1541+
13971542 /// Set the collection as hydrated.
13981543 pub fn set_hydrated ( & self ) {
13991544 self . logger . log ( & ComputeEvent :: Hydration ( Hydration {
0 commit comments