@@ -746,6 +746,7 @@ impl Cluster for ClusterImpl {
746746 fn job_result_listener ( & self ) -> JobResultListener {
747747 JobResultListener {
748748 receiver : self . meta_store_sender . subscribe ( ) ,
749+ meta_store : self . meta_store . clone ( ) ,
749750 }
750751 }
751752
@@ -1018,6 +1019,7 @@ pub trait MessageStream: Send + Sync {
10181019
10191020pub struct JobResultListener {
10201021 receiver : Receiver < MetaStoreEvent > ,
1022+ meta_store : Arc < dyn MetaStore > ,
10211023}
10221024
10231025impl JobResultListener {
@@ -1038,53 +1040,161 @@ impl JobResultListener {
10381040 . unwrap ( ) )
10391041 }
10401042
1043+ /// Resolve the current outcome of each still-pending job directly from the metastore.
1044+ /// Used when broadcast events can't be relied on (channel lag, or the periodic safety poll).
1045+ /// A job that is gone from the metastore finished and was deleted, but after the fact we can't
1046+ /// tell success from orphan/error — report Orphaned so the caller retries rather than treating
1047+ /// a possibly-incomplete result as success.
1048+ async fn resolve_pending_from_metastore (
1049+ & self ,
1050+ results : Vec < ( RowKey , JobType ) > ,
1051+ res : & mut Vec < JobEvent > ,
1052+ ) -> Result < Vec < ( RowKey , JobType ) > , CubeError > {
1053+ let mut still_pending = Vec :: new ( ) ;
1054+ for ( row_key, job_type) in results. into_iter ( ) {
1055+ match self
1056+ . meta_store
1057+ . get_job_by_ref ( row_key. clone ( ) , job_type. clone ( ) )
1058+ . await ?
1059+ {
1060+ Some ( job) => match job. get_row ( ) . status ( ) {
1061+ JobStatus :: Completed => res. push ( JobEvent :: Success ( row_key, job_type) ) ,
1062+ JobStatus :: Error ( e) => {
1063+ res. push ( JobEvent :: Error ( row_key, job_type, e. to_string ( ) ) )
1064+ }
1065+ JobStatus :: Timeout => res. push ( JobEvent :: Error (
1066+ row_key,
1067+ job_type,
1068+ "Job timed out" . to_string ( ) ,
1069+ ) ) ,
1070+ JobStatus :: Orphaned => res. push ( JobEvent :: Orphaned ( row_key, job_type) ) ,
1071+ JobStatus :: Scheduled ( _) | JobStatus :: ProcessingBy ( _) => {
1072+ still_pending. push ( ( row_key, job_type) )
1073+ }
1074+ } ,
1075+ None => res. push ( JobEvent :: Orphaned ( row_key, job_type) ) ,
1076+ }
1077+ }
1078+ Ok ( still_pending)
1079+ }
1080+
10411081 pub async fn wait_for_job_results (
1082+ self ,
1083+ results : Vec < ( RowKey , JobType ) > ,
1084+ ) -> Result < Vec < JobEvent > , CubeError > {
1085+ self . wait_for_job_results_with_poll ( results, None ) . await
1086+ }
1087+
1088+ /// Like `wait_for_job_results`, but when `poll_interval` is set it also actively re-checks
1089+ /// pending job statuses in the metastore whenever no event arrives within the interval. This
1090+ /// keeps the wait live even if completion events are lost or dropped by the broadcast channel
1091+ /// (used by the import finalize path, where a missed event would otherwise leave the table
1092+ /// stuck "processing" forever).
1093+ pub async fn wait_for_job_results_with_poll (
10421094 mut self ,
10431095 mut results : Vec < ( RowKey , JobType ) > ,
1096+ poll_interval : Option < Duration > ,
10441097 ) -> Result < Vec < JobEvent > , CubeError > {
10451098 let mut res = Vec :: new ( ) ;
1099+ // Independent wall-clock timer for the safety re-check. It must NOT be a per-iteration
1100+ // recv timeout: the receiver sees every metastore event, so on a busy router unrelated
1101+ // events would reset such a timeout and it would never fire — exactly where a missed
1102+ // completion event is most likely. An `interval` only advances its deadline when a tick
1103+ // completes, so repeatedly cancelling tick() while recv wins keeps the cadence; with a
1104+ // `biased` select the due tick wins even when the channel is saturated with ready events.
1105+ let mut poll_timer = poll_interval. map ( |interval| {
1106+ let mut timer =
1107+ tokio:: time:: interval_at ( tokio:: time:: Instant :: now ( ) + interval, interval) ;
1108+ timer. set_missed_tick_behavior ( tokio:: time:: MissedTickBehavior :: Delay ) ;
1109+ timer
1110+ } ) ;
10461111 loop {
1047- if results. len ( ) == 0 {
1112+ if results. is_empty ( ) {
10481113 return Ok ( res) ;
10491114 }
1050- let event = self . receiver . recv ( ) . await ?;
1051- if let MetaStoreEvent :: UpdateJob ( old, new) = & event {
1052- if old. get_row ( ) . status ( ) != new. get_row ( ) . status ( ) {
1053- let job_event = match new. get_row ( ) . status ( ) {
1054- JobStatus :: Scheduled ( _) => None ,
1055- JobStatus :: ProcessingBy ( _) => None ,
1056- JobStatus :: Completed => Some ( JobEvent :: Success (
1057- new. get_row ( ) . row_reference ( ) . clone ( ) ,
1058- new. get_row ( ) . job_type ( ) . clone ( ) ,
1059- ) ) ,
1060- JobStatus :: Timeout => Some ( JobEvent :: Error (
1061- new. get_row ( ) . row_reference ( ) . clone ( ) ,
1062- new. get_row ( ) . job_type ( ) . clone ( ) ,
1063- "Job timed out" . to_string ( ) ,
1064- ) ) ,
1065- JobStatus :: Orphaned => Some ( JobEvent :: Orphaned (
1066- new. get_row ( ) . row_reference ( ) . clone ( ) ,
1067- new. get_row ( ) . job_type ( ) . clone ( ) ,
1068- ) ) ,
1069- JobStatus :: Error ( e) => Some ( JobEvent :: Error (
1070- new. get_row ( ) . row_reference ( ) . clone ( ) ,
1071- new. get_row ( ) . job_type ( ) . clone ( ) ,
1072- e. to_string ( ) ,
1073- ) ) ,
1074- } ;
1075- if let Some ( event) = job_event {
1076- if let JobEvent :: Success ( k, t) | JobEvent :: Error ( k, t, _) = & event {
1077- if let Some ( ( index, _) ) = results
1078- . iter ( )
1079- . find_position ( |( row_key, job_type) | k == row_key && t == job_type)
1080- {
1081- res. push ( event) ;
1082- results. remove ( index) ;
1115+ let mut needs_recheck = false ;
1116+ tokio:: select! {
1117+ biased;
1118+ // Pends forever when no poll_interval, so this arm is effectively disabled then.
1119+ _ = async {
1120+ match poll_timer. as_mut( ) {
1121+ Some ( timer) => {
1122+ timer. tick( ) . await ;
1123+ }
1124+ None => std:: future:: pending:: <( ) >( ) . await ,
1125+ }
1126+ } => {
1127+ needs_recheck = true ;
1128+ }
1129+ recv_result = self . receiver. recv( ) => {
1130+ match recv_result {
1131+ Ok ( event) => {
1132+ if let MetaStoreEvent :: UpdateJob ( old, new) = & event {
1133+ if old. get_row( ) . status( ) != new. get_row( ) . status( ) {
1134+ let job_event = match new. get_row( ) . status( ) {
1135+ JobStatus :: Scheduled ( _) => None ,
1136+ JobStatus :: ProcessingBy ( _) => None ,
1137+ JobStatus :: Completed => Some ( JobEvent :: Success (
1138+ new. get_row( ) . row_reference( ) . clone( ) ,
1139+ new. get_row( ) . job_type( ) . clone( ) ,
1140+ ) ) ,
1141+ JobStatus :: Timeout => Some ( JobEvent :: Error (
1142+ new. get_row( ) . row_reference( ) . clone( ) ,
1143+ new. get_row( ) . job_type( ) . clone( ) ,
1144+ "Job timed out" . to_string( ) ,
1145+ ) ) ,
1146+ JobStatus :: Orphaned => Some ( JobEvent :: Orphaned (
1147+ new. get_row( ) . row_reference( ) . clone( ) ,
1148+ new. get_row( ) . job_type( ) . clone( ) ,
1149+ ) ) ,
1150+ JobStatus :: Error ( e) => Some ( JobEvent :: Error (
1151+ new. get_row( ) . row_reference( ) . clone( ) ,
1152+ new. get_row( ) . job_type( ) . clone( ) ,
1153+ e. to_string( ) ,
1154+ ) ) ,
1155+ } ;
1156+ if let Some ( event) = job_event {
1157+ if let JobEvent :: Success ( k, t)
1158+ | JobEvent :: Orphaned ( k, t)
1159+ | JobEvent :: Error ( k, t, _) = & event
1160+ {
1161+ if let Some ( ( index, _) ) =
1162+ results. iter( ) . find_position( |( row_key, job_type) | {
1163+ k == row_key && t == job_type
1164+ } )
1165+ {
1166+ res. push( event) ;
1167+ results. remove( index) ;
1168+ }
1169+ }
1170+ }
1171+ }
10831172 }
10841173 }
1174+ Err ( tokio:: sync:: broadcast:: error:: RecvError :: Lagged ( n) ) => {
1175+ // The channel overflowed and dropped messages — the completion events
1176+ // we were waiting for may have been among them. Re-derive pending jobs
1177+ // authoritatively instead of failing finalize.
1178+ log:: warn!(
1179+ "JobResultListener lagged by {} events; re-checking {} pending job(s) from metastore" ,
1180+ n,
1181+ results. len( )
1182+ ) ;
1183+ needs_recheck = true ;
1184+ }
1185+ Err ( tokio:: sync:: broadcast:: error:: RecvError :: Closed ) => {
1186+ return Err ( CubeError :: internal(
1187+ "JobResultListener event channel closed" . to_string( ) ,
1188+ ) ) ;
1189+ }
10851190 }
10861191 }
10871192 }
1193+ if needs_recheck {
1194+ results = self
1195+ . resolve_pending_from_metastore ( results, & mut res)
1196+ . await ?;
1197+ }
10881198 }
10891199 }
10901200}
0 commit comments