1use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17
18use futures::StreamExt;
19use futures::stream::FuturesUnordered;
20use risingwave_common::catalog::{DatabaseId, FragmentTypeFlag, TableId};
21use risingwave_common::id::{JobId, PartialGraphId, SinkId};
22use risingwave_hummock_sdk::change_log::TableChangeLogs;
23use risingwave_meta_model::ActorId;
24use risingwave_meta_model::streaming_job::BackfillOrders;
25use risingwave_pb::common::WorkerNode;
26use risingwave_pb::hummock::HummockVersionStats;
27use risingwave_pb::meta::PbTableRefillRuntimeConfig;
28use risingwave_pb::meta::subscribe_response::{Info, Operation};
29use risingwave_pb::stream_service::barrier_complete_response::{
30 PbListFinishedSource, PbLoadFinishedSource, PbRefreshFinishedActor,
31};
32use risingwave_rpc_client::StreamingControlHandle;
33use thiserror_ext::AsReport;
34
35use crate::barrier::cdc_progress::CdcTableBackfillTracker;
36use crate::barrier::checkpoint::independent_job::BatchRefreshJobTriggerContext;
37use crate::barrier::command::{
38 PostCollectCommand, ResumeBackfillTarget, SinceTimestampResolvedEpoch,
39};
40use crate::barrier::context::{GlobalBarrierWorkerContext, GlobalBarrierWorkerContextImpl};
41use crate::barrier::progress::TrackingJob;
42use crate::barrier::schedule::MarkReadyOptions;
43use crate::barrier::{
44 BarrierManagerStatus, BarrierWorkerRuntimeInfoSnapshot, CreateStreamingJobCommandInfo,
45 CreateStreamingJobType, DatabaseRuntimeInfoSnapshot, RecoveryReason, ReplaceStreamJobPlan,
46 Scheduled,
47};
48use crate::hummock::CommitEpochInfo;
49use crate::manager::iceberg_pk_index_sink::{
50 IcebergPkIndexPreCommitMetadata, build_iceberg_config, group_pre_commit_metadata,
51 is_iceberg_pk_index_sink,
52};
53use crate::manager::sink_coordination::{RecoveryStart, RecoverySucceeded};
54use crate::manager::{LocalNotification, StreamingJob};
55use crate::model::FragmentDownstreamRelation;
56use crate::serving::{fetch_serving_infos, sync_serving_table_vnode_mappings_to_hummock};
57use crate::stream::{RefreshStage, SourceChange, cleanup_dropped_streaming_jobs};
58use crate::{MetaError, MetaResult};
59
60fn resolve_since_timestamp_log_store_epoch(
61 table_id: TableId,
62 since_epoch: u64,
63 upstream_committed_epoch: u64,
64 table_change_log: &TableChangeLogs,
65) -> MetaResult<SinceTimestampResolvedEpoch> {
66 let change_log = table_change_log.get(&table_id).ok_or_else(|| {
67 anyhow::anyhow!(
68 "no table changelog found for upstream table {} when resolving since_timestamp",
69 table_id
70 )
71 })?;
72 let Some(first_log) = change_log.first() else {
73 return Err(anyhow::anyhow!(
74 "empty table changelog found for upstream table {} when resolving since_timestamp",
75 table_id
76 )
77 .into());
78 };
79 let first_checkpoint_epoch = first_log.checkpoint_epoch;
80 if since_epoch < first_checkpoint_epoch {
81 return Err(anyhow::anyhow!(
82 "since_timestamp is earlier than the retained changelog of upstream table {}: requested epoch {}, first retained checkpoint epoch {}",
83 table_id,
84 since_epoch,
85 first_checkpoint_epoch,
86 )
87 .into());
88 }
89 if since_epoch >= upstream_committed_epoch {
90 return Err(anyhow::anyhow!(
91 "since_timestamp is not before the committed epoch of upstream table {}: requested epoch {}, committed epoch {}",
92 table_id,
93 since_epoch,
94 upstream_committed_epoch,
95 )
96 .into());
97 }
98 let latest_log = change_log.last().expect("checked non-empty");
99 if upstream_committed_epoch != latest_log.checkpoint_epoch {
100 return Err(anyhow::anyhow!(
101 "upstream committed epoch {} does not match latest changelog epoch {} for upstream table {}",
102 upstream_committed_epoch,
103 latest_log.checkpoint_epoch,
104 table_id,
105 )
106 .into());
107 }
108
109 let snapshot_epoch_index = match change_log.binary_search_by_checkpoint_epoch(since_epoch) {
117 Ok(index) => index,
118 Err(index) => index,
119 };
120 let snapshot_epoch = change_log
121 .get(snapshot_epoch_index)
122 .ok_or_else(|| {
123 anyhow::anyhow!(
124 "since_timestamp is later than the latest changelog of upstream table {}: requested epoch {}, latest changelog epoch {}",
125 table_id,
126 since_epoch,
127 upstream_committed_epoch,
128 )
129 })?
130 .checkpoint_epoch;
131 if snapshot_epoch >= upstream_committed_epoch {
135 return Err(anyhow::anyhow!(
136 "since_timestamp is too new for upstream table {}: requested epoch {}, resolved snapshot epoch {}, latest changelog epoch {}",
137 table_id,
138 since_epoch,
139 snapshot_epoch,
140 upstream_committed_epoch,
141 )
142 .into());
143 }
144 let epochs = change_log
145 .range((snapshot_epoch_index + 1)..)
146 .map(|epoch_log| {
147 (
148 epoch_log.non_checkpoint_epochs.clone(),
149 epoch_log.checkpoint_epoch,
150 )
151 })
152 .collect::<Vec<_>>();
153
154 Ok((snapshot_epoch, epochs))
155}
156
157impl GlobalBarrierWorkerContext for GlobalBarrierWorkerContextImpl {
158 #[await_tree::instrument]
159 async fn commit_epoch(&self, commit_info: CommitEpochInfo) -> MetaResult<HummockVersionStats> {
160 self.hummock_manager.commit_epoch(commit_info).await?;
161 Ok(self.hummock_manager.get_version_stats().await)
162 }
163
164 #[await_tree::instrument("next_scheduled_barrier")]
165 async fn next_scheduled(&self) -> Scheduled {
166 self.scheduled_barriers.next_scheduled().await
167 }
168
169 async fn abort_and_mark_blocked(
170 &self,
171 recovery: RecoveryStart,
172 recovery_reason: &RecoveryReason,
173 ) -> MetaResult<()> {
174 let (database_id, database_job_ids) = match &recovery {
175 RecoveryStart::Global => (None, None),
176 RecoveryStart::Database {
177 database_id,
178 job_ids,
179 } => (Some(*database_id), Some(job_ids.clone())),
180 };
181 if database_id.is_none() {
182 self.set_status(BarrierManagerStatus::Recovering(recovery_reason.into()));
183 }
184
185 self.scheduled_barriers
187 .abort_and_mark_blocked(database_id, "cluster is under recovering");
188 self.iceberg_pk_index_sink_manager
189 .start_recovery(database_job_ids.as_ref());
190 self.sink_manager.start_recovery(recovery).await?;
191 Ok(())
192 }
193
194 async fn mark_ready(
195 &self,
196 options: MarkReadyOptions,
197 running_job_ids: HashSet<JobId>,
198 ) -> MetaResult<()> {
199 let recovery = match &options {
200 MarkReadyOptions::Database(database_id) => RecoverySucceeded::Database(*database_id),
201 MarkReadyOptions::Global { failed_databases } => RecoverySucceeded::Global {
202 failed_databases: failed_databases.clone(),
203 },
204 };
205 self.sink_manager.recovery_succeeded(recovery).await?;
206 let is_global = matches!(&options, MarkReadyOptions::Global { .. });
207 self.scheduled_barriers.mark_ready(options);
208 self.iceberg_pk_index_sink_manager
210 .on_jobs_ready(&running_job_ids)
211 .await;
212 if is_global {
213 self.set_status(BarrierManagerStatus::Running);
214 }
215 self.refresh_manager.notify_scheduler();
217 Ok(())
218 }
219
220 async fn resolve_log_store_epoch<'a>(
221 &'a self,
222 upstream_table_ids: impl Iterator<Item = TableId> + Send + 'a,
223 since_epoch: u64,
224 ) -> MetaResult<SinceTimestampResolvedEpoch> {
225 let upstream_table_ids = upstream_table_ids.collect::<Vec<_>>();
226 if upstream_table_ids.is_empty() {
227 return Err(
228 anyhow::anyhow!("since_timestamp requires at least one upstream table").into(),
229 );
230 }
231
232 self.hummock_manager
233 .on_current_version_and_table_change_log(|version, table_change_log| {
234 let mut unified_log_epochs = None;
235 for &upstream_table_id in &upstream_table_ids {
236 let upstream_committed_epoch = version
237 .state_table_info
238 .info()
239 .get(&upstream_table_id)
240 .map(|info| info.committed_epoch)
241 .ok_or_else(|| {
242 anyhow::anyhow!(
243 "cannot get committed epoch for upstream table {}",
244 upstream_table_id
245 )
246 })?;
247 let table_log_epochs = resolve_since_timestamp_log_store_epoch(
248 upstream_table_id,
249 since_epoch,
250 upstream_committed_epoch,
251 table_change_log,
252 )?;
253 if let Some(unified_log_epochs) = &unified_log_epochs {
254 if unified_log_epochs != &table_log_epochs {
255 return Err(anyhow::anyhow!(
256 "resolved since_timestamp log epochs for upstream table {} do not match previous upstream table log epochs: {:?} vs {:?}",
257 upstream_table_id,
258 table_log_epochs,
259 unified_log_epochs,
260 )
261 .into());
262 }
263 } else {
264 unified_log_epochs = Some(table_log_epochs);
265 }
266 }
267 unified_log_epochs.ok_or_else(|| {
268 anyhow::anyhow!("since_timestamp requires at least one upstream table").into()
269 })
270 })
271 .await
272 }
273
274 async fn refresh_table_refill_runtime_state_after_recovery(&self) -> MetaResult<()> {
275 let policies = self
276 .metadata_manager
277 .catalog_controller
278 .table_cache_refill_policies_snapshot()
279 .await?;
280 let (serving_workers, fragment_serving_infos) =
281 fetch_serving_infos(&self.metadata_manager).await?;
282
283 self.env.notification_manager().notify_hummock(
284 Operation::Update,
285 Info::TableRefillRuntimeConfig(PbTableRefillRuntimeConfig {
286 table_cache_refill_policies: Some(policies),
287 ..Default::default()
288 }),
289 );
290 sync_serving_table_vnode_mappings_to_hummock(
291 &self.env.notification_manager_ref(),
292 &self.serving_vnode_mapping,
293 &serving_workers,
294 &fragment_serving_infos,
295 )
296 .await;
297 Ok(())
298 }
299
300 #[await_tree::instrument("post_collect_command({command})")]
301 async fn post_collect_command(&self, command: PostCollectCommand) -> MetaResult<()> {
302 Box::pin(command.post_collect(self)).await
303 }
304
305 async fn notify_creating_job_failed(&self, database_id: Option<DatabaseId>, err: String) {
306 self.metadata_manager
307 .notify_finish_failed(database_id, err)
308 .await
309 }
310
311 #[await_tree::instrument("finish_creating_job({job})")]
312 async fn finish_creating_job(&self, job: TrackingJob) -> MetaResult<()> {
313 let job_id = job.job_id();
314 job.finish(&self.metadata_manager, &self.source_manager)
315 .await?;
316 self.env
317 .notification_manager()
318 .notify_local_subscribers(LocalNotification::StreamingJobBackfillFinished(job_id));
319 Ok(())
320 }
321
322 #[await_tree::instrument("finish_cdc_table_backfill({job})")]
323 async fn finish_cdc_table_backfill(&self, job: JobId) -> MetaResult<()> {
324 CdcTableBackfillTracker::mark_complete_job(&self.env.meta_store().conn, job).await
325 }
326
327 #[await_tree::instrument("new_control_stream({})", node.id)]
328 async fn new_control_stream(&self, node: &WorkerNode) -> MetaResult<StreamingControlHandle> {
329 self.new_control_stream_impl(node).await
330 }
331
332 async fn reload_runtime_info(&self) -> MetaResult<BarrierWorkerRuntimeInfoSnapshot> {
333 self.reload_runtime_info_impl().await
334 }
335
336 async fn reload_database_runtime_info(
337 &self,
338 database_id: DatabaseId,
339 ) -> MetaResult<DatabaseRuntimeInfoSnapshot> {
340 self.reload_database_runtime_info_impl(database_id).await
341 }
342
343 async fn handle_list_finished_source_ids(
344 &self,
345 list_finished: Vec<PbListFinishedSource>,
346 ) -> MetaResult<()> {
347 for (table_id, actors) in group_reporters(
348 list_finished
349 .into_iter()
350 .map(|report| (report.table_id, report.reporter_actor_id)),
351 ) {
352 self.refresh_manager
353 .report_stage(table_id, RefreshStage::List, actors)?;
354 }
355 Ok(())
356 }
357
358 async fn handle_load_finished_source_ids(
359 &self,
360 load_finished: Vec<PbLoadFinishedSource>,
361 ) -> MetaResult<()> {
362 for (table_id, actors) in group_reporters(
363 load_finished
364 .into_iter()
365 .map(|report| (report.table_id, report.reporter_actor_id)),
366 ) {
367 self.refresh_manager
368 .report_stage(table_id, RefreshStage::Fetch, actors)?;
369 }
370 Ok(())
371 }
372
373 async fn handle_refresh_finished_actors(
374 &self,
375 refresh_finished: Vec<PbRefreshFinishedActor>,
376 ) -> MetaResult<()> {
377 for (table_id, actors) in group_reporters(
378 refresh_finished
379 .into_iter()
380 .map(|report| (report.table_id, report.reporter_actor_id)),
381 ) {
382 self.refresh_manager
383 .report_stage(table_id, RefreshStage::Mview, actors)?;
384 }
385 Ok(())
386 }
387
388 async fn load_batch_refresh_trigger_context(
389 &self,
390 job_id: JobId,
391 database_id: DatabaseId,
392 last_committed_epoch: u64,
393 ) -> MetaResult<BatchRefreshJobTriggerContext> {
394 self.load_batch_refresh_trigger_context_impl(job_id, database_id, last_committed_epoch)
395 .await
396 }
397
398 #[await_tree::instrument]
399 async fn pre_commit_iceberg_pk_index_sink_metadata(
400 &self,
401 metadata: Vec<IcebergPkIndexPreCommitMetadata>,
402 ) -> MetaResult<Vec<SinkId>> {
403 let inputs = group_pre_commit_metadata(metadata)?;
404 let futs = FuturesUnordered::new();
405 for input in inputs {
406 let manager = &self.iceberg_pk_index_sink_manager;
407 futs.push(async move {
408 let sink_id = input.sink_id;
409 (sink_id, manager.pre_commit(input).await)
410 });
411 }
412
413 let results: Vec<(SinkId, anyhow::Result<()>)> = futs.collect().await;
416 let has_err = results.iter().any(|(_, result)| result.is_err());
417 if !has_err {
418 let success_ids = results.into_iter().map(|(id, _)| id).collect();
419 Ok(success_ids)
420 } else {
421 let errs = results
422 .into_iter()
423 .filter_map(|(id, result)| result.err().map(|error| (id, error)))
424 .collect();
425 Err(aggregate_sink_errors("pre-commit", errs).into())
426 }
427 }
428
429 #[await_tree::instrument]
430 async fn commit_iceberg_pk_index_sink_metadata(&self, sink_ids: Vec<SinkId>) -> MetaResult<()> {
431 let futs = FuturesUnordered::new();
432 for sink_id in sink_ids {
433 let manager = &self.iceberg_pk_index_sink_manager;
434 futs.push(async move { (sink_id, manager.commit_epoch(sink_id).await) });
435 }
436
437 let results: Vec<(SinkId, anyhow::Result<()>)> = futs.collect().await;
438 let errs: Vec<(SinkId, anyhow::Error)> = results
439 .into_iter()
440 .filter_map(|(id, r)| r.err().map(|e| (id, e)))
441 .collect();
442 if !errs.is_empty() {
443 return Err(aggregate_sink_errors("commit", errs).into());
444 }
445 Ok(())
446 }
447
448 fn advance_iceberg_pk_index_sink_committed_epochs(
449 &self,
450 epochs: impl IntoIterator<Item = (PartialGraphId, u64)>,
451 ) {
452 self.iceberg_pk_index_sink_manager
453 .advance_committed_epochs(epochs);
454 }
455}
456
457fn aggregate_sink_errors(
461 phase: &'static str,
462 mut errs: Vec<(SinkId, anyhow::Error)>,
463) -> anyhow::Error {
464 debug_assert!(!errs.is_empty());
465 let sink_ids: Vec<String> = errs.iter().map(|(id, _)| id.to_string()).collect();
466 let details: Vec<String> = errs
467 .iter()
468 .map(|(id, e)| format!("sink {}: {}", id, e.as_report()))
469 .collect();
470 let (_, first_err) = errs.remove(0);
472 first_err.context(format!(
473 "iceberg v3 sink {} failed for sink_id(s) [{}]: {}",
474 phase,
475 sink_ids.join(", "),
476 details.join("; ")
477 ))
478}
479
480impl GlobalBarrierWorkerContextImpl {
481 fn set_status(&self, new_status: BarrierManagerStatus) {
482 self.status.store(Arc::new(new_status));
483 }
484
485 async fn load_batch_refresh_trigger_context_impl(
487 &self,
488 job_id: JobId,
489 database_id: DatabaseId,
490 last_committed_epoch: u64,
491 ) -> MetaResult<BatchRefreshJobTriggerContext> {
492 use itertools::Itertools;
493 use sea_orm::TransactionTrait;
494
495 use crate::controller::scale::load_fragment_context_for_jobs;
496
497 let inner = self
499 .metadata_manager
500 .catalog_controller
501 .get_inner_read_guard()
502 .await;
503 let txn = inner.db.begin().await?;
504
505 let fragment_context =
507 load_fragment_context_for_jobs(&txn, HashSet::from([job_id])).await?;
508
509 let streaming_job_model = fragment_context
510 .job_map
511 .get(&job_id)
512 .ok_or_else(|| anyhow::anyhow!("streaming job model not found for job {}", job_id))?
513 .clone();
514
515 let database_model = fragment_context
516 .database_map
517 .get(&database_id)
518 .ok_or_else(|| {
519 anyhow::anyhow!("database model not found for database {}", database_id)
520 })?;
521 let database_resource_group = database_model.resource_group.clone();
522
523 let mut job_extra_info = self
525 .metadata_manager
526 .catalog_controller
527 .get_streaming_job_extra_info_in_txn(&txn, vec![job_id])
528 .await?;
529 let definition = job_extra_info
530 .remove(&job_id)
531 .ok_or_else(|| anyhow::anyhow!("extra info not found for job {}", job_id))?
532 .job_definition;
533
534 let fragments = fragment_context
536 .job_fragments
537 .get(&job_id)
538 .ok_or_else(|| anyhow::anyhow!("fragments not found for job {}", job_id))?
539 .clone();
540
541 let upstream_table_ids: HashSet<TableId> = {
543 use crate::stream::StreamFragmentGraph;
544 let snapshot_backfill_info = StreamFragmentGraph::collect_snapshot_backfill_info_impl(
545 fragments.values().map(|f| (&f.nodes, f.fragment_type_mask)),
546 )?
547 .0
548 .ok_or_else(|| {
549 anyhow::anyhow!("batch refresh job {} has no snapshot backfill info", job_id)
550 })?;
551 snapshot_backfill_info
552 .upstream_mv_table_id_to_backfill_epoch
553 .into_keys()
554 .collect()
555 };
556
557 let fragment_ids: Vec<_> = fragments.keys().copied().collect();
558 let downstreams = self
559 .metadata_manager
560 .catalog_controller
561 .get_fragment_downstream_relations_in_txn(&txn, fragment_ids)
562 .await?;
563
564 txn.commit().await?;
565 drop(inner);
566
567 let (upstream_table_log_epochs, target_upstream_epoch) = self
569 .hummock_manager
570 .on_current_version_and_table_change_log(|version, table_change_log| {
571 let mut target_upstream_epoch = last_committed_epoch;
572 let mut log_epochs: HashMap<TableId, Vec<(Vec<u64>, u64)>> = HashMap::new();
573
574 for &upstream_table_id in &upstream_table_ids {
575 let upstream_committed_epoch = version
576 .state_table_info
577 .info()
578 .get(&upstream_table_id)
579 .map(|info| info.committed_epoch)
580 .ok_or_else(|| {
581 anyhow::anyhow!(
582 "cannot get committed epoch for upstream table {}",
583 upstream_table_id
584 )
585 })?;
586
587 target_upstream_epoch =
588 std::cmp::max(target_upstream_epoch, upstream_committed_epoch);
589
590 if upstream_committed_epoch <= last_committed_epoch {
591 continue;
592 }
593
594 if let Some(change_log) = table_change_log.get(&upstream_table_id) {
595 let epochs = change_log
596 .filter_epoch((last_committed_epoch, upstream_committed_epoch))
597 .map(|epoch_log| {
598 (
599 epoch_log.non_checkpoint_epochs.clone(),
600 epoch_log.checkpoint_epoch,
601 )
602 })
603 .collect_vec();
604 if !epochs.is_empty() {
605 log_epochs.insert(upstream_table_id, epochs);
606 }
607 } else {
608 anyhow::bail!(
609 "upstream table {} has lagged downstream on epoch {} but no table change log (upstream committed: {})",
610 upstream_table_id,
611 last_committed_epoch,
612 upstream_committed_epoch,
613 );
614 }
615 }
616
617 Ok((log_epochs, target_upstream_epoch))
618 })
619 .await?;
620
621 Ok(BatchRefreshJobTriggerContext {
622 fragments,
623 downstreams,
624 streaming_job_model,
625 definition,
626 database_resource_group,
627 upstream_table_log_epochs,
628 target_upstream_epoch,
629 })
630 }
631}
632
633impl PostCollectCommand {
634 pub async fn post_collect(
637 self,
638 barrier_manager_context: &GlobalBarrierWorkerContextImpl,
639 ) -> MetaResult<()> {
640 match self {
641 PostCollectCommand::Command(_) => {}
642 PostCollectCommand::SourceChangeSplit {
643 split_assignment: assignment,
644 } => {
645 barrier_manager_context
646 .metadata_manager
647 .update_fragment_splits(&assignment)
648 .await?;
649 }
650
651 PostCollectCommand::DropStreamingJobs => {}
652 PostCollectCommand::RefreshStarted {
653 table_id,
654 database_id,
655 associated_source_id,
656 staging_table_id,
657 trigger_time,
658 actors,
659 } => {
660 barrier_manager_context
661 .refresh_manager
662 .cycle_started(
663 table_id,
664 database_id,
665 associated_source_id,
666 staging_table_id,
667 trigger_time,
668 actors,
669 )
670 .await?;
671 }
672 PostCollectCommand::FinishRefresh {
673 table_id,
674 trigger_time,
675 ..
676 } => {
677 barrier_manager_context
678 .refresh_manager
679 .complete_refresh(table_id, trigger_time)
680 .await?;
681 }
682 PostCollectCommand::ConnectorPropsChange(obj_id_map_props) => {
683 for object_id in obj_id_map_props.keys() {
686 if let Some(sink) = barrier_manager_context
687 .metadata_manager
688 .catalog_controller
689 .get_sink_by_id(object_id.as_sink_id())
690 .await?
691 && is_iceberg_pk_index_sink(&sink.properties)
692 {
693 let config = build_iceberg_config(&sink)?;
694 barrier_manager_context
695 .iceberg_pk_index_sink_manager
696 .on_sink_config_changed(sink.id, config);
697 }
698 }
699 barrier_manager_context
701 .source_manager
702 .apply_source_change(SourceChange::UpdateSourceProps {
703 source_id_map_new_props: obj_id_map_props
706 .iter()
707 .map(|(object_id, props)| (object_id.as_source_id(), props.clone()))
708 .collect(),
709 })
710 .await;
711 }
712 PostCollectCommand::ResumeBackfill { target } => match target {
713 ResumeBackfillTarget::Job(job_id) => {
714 barrier_manager_context
715 .metadata_manager
716 .catalog_controller
717 .update_backfill_orders_by_job_id(job_id, None)
718 .await?;
719 }
720 ResumeBackfillTarget::Fragment(fragment_id) => {
721 let mut job_ids = barrier_manager_context
722 .metadata_manager
723 .catalog_controller
724 .get_fragment_job_id(vec![fragment_id])
725 .await?;
726 let job_id = job_ids.pop().ok_or_else(|| {
727 MetaError::invalid_parameter("fragment not found".to_owned())
728 })?;
729 let job_id = JobId::new(job_id.as_raw_id());
730
731 let extra_info = barrier_manager_context
732 .metadata_manager
733 .catalog_controller
734 .get_streaming_job_extra_info(vec![job_id])
735 .await?;
736 let mut backfill_orders: BackfillOrders = extra_info
737 .get(&job_id)
738 .cloned()
739 .ok_or_else(|| MetaError::invalid_parameter("job not found".to_owned()))?
740 .backfill_orders
741 .unwrap_or_default();
742
743 let resumed_fragment_id = fragment_id.as_raw_id();
744 for children in backfill_orders.0.values_mut() {
745 children.retain(|child| *child != resumed_fragment_id);
746 }
747 backfill_orders.0.retain(|_, children| !children.is_empty());
748
749 barrier_manager_context
750 .metadata_manager
751 .catalog_controller
752 .update_backfill_orders_by_job_id(job_id, Some(backfill_orders))
753 .await?;
754 }
755 },
756 PostCollectCommand::CreateStreamingJob {
757 info,
758 job_type,
759 cross_db_snapshot_backfill_info,
760 resolved_split_assignment,
761 } => {
762 match &job_type {
763 CreateStreamingJobType::SinkIntoTable(_) | CreateStreamingJobType::Normal => {
764 barrier_manager_context
765 .metadata_manager
766 .catalog_controller
767 .fill_snapshot_backfill_epoch(
768 info.stream_job_fragments.fragments.iter().filter_map(
769 |(fragment_id, fragment)| {
770 if fragment.fragment_type_mask.contains(
771 FragmentTypeFlag::CrossDbSnapshotBackfillStreamScan,
772 ) {
773 Some(*fragment_id as _)
774 } else {
775 None
776 }
777 },
778 ),
779 None,
780 &cross_db_snapshot_backfill_info,
781 )
782 .await?
783 }
784 CreateStreamingJobType::Independent {
785 snapshot_backfill_info,
786 ..
787 } => {
788 barrier_manager_context
789 .metadata_manager
790 .catalog_controller
791 .fill_snapshot_backfill_epoch(
792 info.stream_job_fragments.fragments.iter().filter_map(
793 |(fragment_id, fragment)| {
794 if fragment.fragment_type_mask.contains_any([
795 FragmentTypeFlag::SnapshotBackfillStreamScan,
796 FragmentTypeFlag::CrossDbSnapshotBackfillStreamScan,
797 ]) {
798 Some(*fragment_id as _)
799 } else {
800 None
801 }
802 },
803 ),
804 Some(snapshot_backfill_info),
805 &cross_db_snapshot_backfill_info,
806 )
807 .await?
808 }
809 }
810
811 let CreateStreamingJobCommandInfo {
814 stream_job_fragments,
815 upstream_fragment_downstreams,
816 replace_sink,
817 streaming_job,
818 ..
819 } = info;
820 let new_job_id = stream_job_fragments.stream_job_id();
821 let new_sink_downstream =
822 if let CreateStreamingJobType::SinkIntoTable(ctx) = job_type {
823 let new_downstreams = ctx.new_sink_downstream.clone();
824 let new_downstreams = FragmentDownstreamRelation::from([(
825 ctx.sink_fragment_id,
826 vec![new_downstreams],
827 )]);
828 Some(new_downstreams)
829 } else {
830 None
831 };
832
833 let old_state_table_ids = barrier_manager_context
834 .metadata_manager
835 .catalog_controller
836 .post_collect_job_fragments(
837 new_job_id,
838 &upstream_fragment_downstreams,
839 new_sink_downstream,
840 Some(&resolved_split_assignment),
841 replace_sink.as_ref(),
842 replace_sink.is_none(),
843 )
844 .await?;
845
846 let added_source_fragments = stream_job_fragments.stream_source_fragments();
847 let added_backfill_fragments = stream_job_fragments.source_backfill_fragments();
848 if !added_source_fragments.is_empty() || !added_backfill_fragments.is_empty() {
850 barrier_manager_context
851 .source_manager
852 .apply_source_change(SourceChange::CreateJob {
853 added_source_fragments,
854 added_backfill_fragments,
855 })
856 .await;
857 }
858
859 if let StreamingJob::Sink(sink, _) = &streaming_job
862 && is_iceberg_pk_index_sink(&sink.properties)
863 {
864 barrier_manager_context
865 .iceberg_pk_index_sink_manager
866 .on_jobs_ready(&HashSet::from([sink.id.as_job_id()]))
867 .await;
868 }
869
870 if let Some(old_sink_id) = replace_sink {
871 cleanup_dropped_streaming_jobs(
872 &barrier_manager_context.refresh_manager,
873 &barrier_manager_context.hummock_manager,
874 &barrier_manager_context.metadata_manager,
875 &barrier_manager_context.sink_manager,
876 &barrier_manager_context.iceberg_pk_index_sink_manager,
877 [old_sink_id.as_job_id()],
878 old_state_table_ids.expect("replace sink should return old state tables"),
879 "replace_sink",
880 )
881 .await?;
882 }
883 }
884 PostCollectCommand::Reschedule { reschedules, .. } => {
885 let fragment_splits = reschedules
886 .iter()
887 .map(|(fragment_id, reschedule)| {
888 (*fragment_id, reschedule.actor_splits.clone())
889 })
890 .collect();
891
892 barrier_manager_context
893 .metadata_manager
894 .update_fragment_splits(&fragment_splits)
895 .await?;
896 }
897
898 PostCollectCommand::ReplaceStreamJob {
899 plan: replace_plan,
900 resolved_split_assignment,
901 } => {
902 let ReplaceStreamJobPlan {
903 old_fragments,
904 new_fragments,
905 upstream_fragment_downstreams,
906 to_drop_state_table_ids,
907 auto_refresh_schema_sinks,
908 ..
909 } = &replace_plan;
910 barrier_manager_context
912 .metadata_manager
913 .catalog_controller
914 .post_collect_job_fragments(
915 new_fragments.stream_job_id,
916 upstream_fragment_downstreams,
917 None,
918 Some(&resolved_split_assignment),
919 None,
920 false,
921 )
922 .await?;
923
924 if let Some(sinks) = auto_refresh_schema_sinks {
925 for sink in sinks {
926 barrier_manager_context
927 .metadata_manager
928 .catalog_controller
929 .post_collect_job_fragments(
930 sink.tmp_sink_id.as_job_id(),
931 &Default::default(), None, None, None,
935 false,
936 )
937 .await?;
938 }
939 }
940
941 barrier_manager_context
943 .source_manager
944 .handle_replace_job(
945 old_fragments,
946 new_fragments.stream_source_fragments(),
947 &replace_plan,
948 )
949 .await;
950 cleanup_dropped_streaming_jobs(
951 &barrier_manager_context.refresh_manager,
952 &barrier_manager_context.hummock_manager,
953 &barrier_manager_context.metadata_manager,
954 &barrier_manager_context.sink_manager,
955 &barrier_manager_context.iceberg_pk_index_sink_manager,
956 [],
957 to_drop_state_table_ids.clone(),
958 "replace_streaming_job",
959 )
960 .await?;
961 }
962
963 PostCollectCommand::CreateSubscription { subscription_id } => {
964 barrier_manager_context
965 .metadata_manager
966 .catalog_controller
967 .finish_create_subscription_catalog(subscription_id)
968 .await?
969 }
970 }
971
972 Ok(())
973 }
974}
975
976fn group_reporters(
977 reports: impl Iterator<Item = (TableId, ActorId)>,
978) -> HashMap<TableId, HashSet<ActorId>> {
979 let mut grouped: HashMap<TableId, HashSet<ActorId>> = HashMap::new();
980 for (table_id, actor_id) in reports {
981 grouped.entry(table_id).or_default().insert(actor_id);
982 }
983 grouped
984}
985
986#[cfg(test)]
987mod tests {
988 use risingwave_hummock_sdk::change_log::{EpochNewChangeLog, TableChangeLog};
989
990 use super::*;
991
992 fn test_change_logs(table_id: TableId) -> TableChangeLogs {
993 TableChangeLogs::from_iter([(
994 table_id,
995 TableChangeLog::new([
996 EpochNewChangeLog {
997 new_value: vec![],
998 old_value: vec![],
999 non_checkpoint_epochs: vec![10],
1000 checkpoint_epoch: 20,
1001 },
1002 EpochNewChangeLog {
1003 new_value: vec![],
1004 old_value: vec![],
1005 non_checkpoint_epochs: vec![30],
1006 checkpoint_epoch: 40,
1007 },
1008 EpochNewChangeLog {
1009 new_value: vec![],
1010 old_value: vec![],
1011 non_checkpoint_epochs: vec![50],
1012 checkpoint_epoch: 60,
1013 },
1014 EpochNewChangeLog {
1015 new_value: vec![],
1016 old_value: vec![],
1017 non_checkpoint_epochs: vec![],
1018 checkpoint_epoch: 80,
1019 },
1020 ]),
1021 )])
1022 }
1023
1024 #[test]
1025 fn test_resolve_since_timestamp_log_store_epoch() {
1026 let table_id = TableId::new(233);
1027 let change_logs = test_change_logs(table_id);
1028
1029 let resolved =
1030 resolve_since_timestamp_log_store_epoch(table_id, 45, 80, &change_logs).unwrap();
1031
1032 assert_eq!(resolved, (60, vec![(vec![], 80)]));
1033 }
1034
1035 #[test]
1036 fn test_resolve_since_timestamp_log_store_epoch_rejects_invalid_range() {
1037 let table_id = TableId::new(233);
1038 let change_logs = test_change_logs(table_id);
1039
1040 let err = resolve_since_timestamp_log_store_epoch(table_id, 9, 80, &change_logs)
1041 .unwrap_err()
1042 .to_string();
1043 assert!(err.contains("earlier than the retained changelog"));
1044
1045 let err = resolve_since_timestamp_log_store_epoch(table_id, 80, 80, &change_logs)
1046 .unwrap_err()
1047 .to_string();
1048 assert!(err.contains("is not before the committed epoch"));
1049
1050 let err = resolve_since_timestamp_log_store_epoch(table_id, 45, 60, &change_logs)
1051 .unwrap_err()
1052 .to_string();
1053 assert!(err.contains("does not match latest changelog epoch"));
1054 }
1055
1056 #[test]
1057 fn test_resolve_since_timestamp_log_store_epoch_rejects_empty_or_missing_log() {
1058 let table_id = TableId::new(233);
1059 let empty_change_logs = TableChangeLogs::from_iter([(table_id, TableChangeLog::new([]))]);
1060
1061 let err = resolve_since_timestamp_log_store_epoch(table_id, 30, 60, &empty_change_logs)
1062 .unwrap_err()
1063 .to_string();
1064 assert!(err.contains("empty table changelog"));
1065
1066 let err = resolve_since_timestamp_log_store_epoch(table_id, 30, 60, &Default::default())
1067 .unwrap_err()
1068 .to_string();
1069 assert!(err.contains("no table changelog"));
1070 }
1071
1072 #[test]
1073 fn test_resolve_since_timestamp_log_store_epoch_keeps_latest_non_checkpoint_epochs() {
1074 let table_id = TableId::new(233);
1075 let change_logs = TableChangeLogs::from_iter([(
1076 table_id,
1077 TableChangeLog::new([
1078 EpochNewChangeLog {
1079 new_value: vec![],
1080 old_value: vec![],
1081 non_checkpoint_epochs: vec![],
1082 checkpoint_epoch: 20,
1083 },
1084 EpochNewChangeLog {
1085 new_value: vec![],
1086 old_value: vec![],
1087 non_checkpoint_epochs: vec![30],
1088 checkpoint_epoch: 40,
1089 },
1090 ]),
1091 )]);
1092
1093 let resolved =
1094 resolve_since_timestamp_log_store_epoch(table_id, 20, 40, &change_logs).unwrap();
1095 assert_eq!(resolved, (20, vec![(vec![30], 40)]));
1096
1097 let err = resolve_since_timestamp_log_store_epoch(table_id, 30, 40, &change_logs)
1098 .unwrap_err()
1099 .to_string();
1100 assert!(err.contains("since_timestamp is too new"));
1101 }
1102
1103 #[test]
1104 fn test_skip_refresh_finish_when_associated_source_missing() {
1105 let err = MetaError::catalog_id_not_found("object", 42);
1106 assert!(err.is_catalog_id_not_found("object"));
1107 }
1108
1109 #[test]
1110 fn test_do_not_skip_refresh_finish_for_other_not_found_types() {
1111 let err = MetaError::catalog_id_not_found("table", 42);
1112 assert!(!err.is_catalog_id_not_found("object"));
1113 }
1114}