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