1use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17
18use anyhow::Context;
19use futures::StreamExt;
20use futures::stream::FuturesUnordered;
21use risingwave_common::catalog::{DatabaseId, FragmentTypeFlag, TableId};
22use risingwave_common::id::{JobId, SinkId};
23use risingwave_hummock_sdk::change_log::TableChangeLogs;
24use risingwave_meta_model::ActorId;
25use risingwave_meta_model::streaming_job::BackfillOrders;
26use risingwave_pb::common::WorkerNode;
27use risingwave_pb::hummock::HummockVersionStats;
28use risingwave_pb::id::SourceId;
29use risingwave_pb::stream_service::barrier_complete_response::{
30 PbIcebergPkIndexSinkMetadata, PbListFinishedSource, PbLoadFinishedSource,
31};
32use risingwave_pb::stream_service::streaming_control_stream_request::PbInitRequest;
33use risingwave_rpc_client::StreamingControlHandle;
34use thiserror_ext::AsReport;
35
36use crate::barrier::cdc_progress::CdcTableBackfillTracker;
37use crate::barrier::checkpoint::independent_job::BatchRefreshJobTriggerContext;
38use crate::barrier::command::{
39 PostCollectCommand, ResumeBackfillTarget, SinceTimestampResolvedEpoch,
40};
41use crate::barrier::context::{GlobalBarrierWorkerContext, GlobalBarrierWorkerContextImpl};
42use crate::barrier::progress::TrackingJob;
43use crate::barrier::schedule::MarkReadyOptions;
44use crate::barrier::{
45 BarrierManagerStatus, BarrierWorkerRuntimeInfoSnapshot, BatchRefreshInfo, Command,
46 CreateStreamingJobCommandInfo, CreateStreamingJobType, DatabaseRuntimeInfoSnapshot,
47 RecoveryReason, ReplaceStreamJobPlan, Scheduled,
48};
49use crate::hummock::CommitEpochInfo;
50use crate::manager::LocalNotification;
51use crate::model::FragmentDownstreamRelation;
52use crate::stream::{SourceChange, cleanup_dropped_streaming_jobs};
53use crate::{MetaError, MetaResult};
54
55fn resolve_since_timestamp_log_store_epoch(
56 table_id: TableId,
57 since_epoch: u64,
58 upstream_committed_epoch: u64,
59 table_change_log: &TableChangeLogs,
60) -> MetaResult<SinceTimestampResolvedEpoch> {
61 let change_log = table_change_log.get(&table_id).ok_or_else(|| {
62 anyhow::anyhow!(
63 "no table changelog found for upstream table {} when resolving since_timestamp",
64 table_id
65 )
66 })?;
67 let Some(first_log) = change_log.first() else {
68 return Err(anyhow::anyhow!(
69 "empty table changelog found for upstream table {} when resolving since_timestamp",
70 table_id
71 )
72 .into());
73 };
74 let first_checkpoint_epoch = first_log.checkpoint_epoch;
75 if since_epoch < first_checkpoint_epoch {
76 return Err(anyhow::anyhow!(
77 "since_timestamp is earlier than the retained changelog of upstream table {}: requested epoch {}, first retained checkpoint epoch {}",
78 table_id,
79 since_epoch,
80 first_checkpoint_epoch,
81 )
82 .into());
83 }
84 if since_epoch >= upstream_committed_epoch {
85 return Err(anyhow::anyhow!(
86 "since_timestamp is not before the committed epoch of upstream table {}: requested epoch {}, committed epoch {}",
87 table_id,
88 since_epoch,
89 upstream_committed_epoch,
90 )
91 .into());
92 }
93 let latest_log = change_log.last().expect("checked non-empty");
94 if upstream_committed_epoch != latest_log.checkpoint_epoch {
95 return Err(anyhow::anyhow!(
96 "upstream committed epoch {} does not match latest changelog epoch {} for upstream table {}",
97 upstream_committed_epoch,
98 latest_log.checkpoint_epoch,
99 table_id,
100 )
101 .into());
102 }
103
104 let snapshot_epoch_index = match change_log.binary_search_by_checkpoint_epoch(since_epoch) {
112 Ok(index) => index,
113 Err(index) => index,
114 };
115 let snapshot_epoch = change_log
116 .get(snapshot_epoch_index)
117 .ok_or_else(|| {
118 anyhow::anyhow!(
119 "since_timestamp is later than the latest changelog of upstream table {}: requested epoch {}, latest changelog epoch {}",
120 table_id,
121 since_epoch,
122 upstream_committed_epoch,
123 )
124 })?
125 .checkpoint_epoch;
126 if snapshot_epoch >= upstream_committed_epoch {
130 return Err(anyhow::anyhow!(
131 "since_timestamp is too new for upstream table {}: requested epoch {}, resolved snapshot epoch {}, latest changelog epoch {}",
132 table_id,
133 since_epoch,
134 snapshot_epoch,
135 upstream_committed_epoch,
136 )
137 .into());
138 }
139 let epochs = change_log
140 .range((snapshot_epoch_index + 1)..)
141 .map(|epoch_log| {
142 (
143 epoch_log.non_checkpoint_epochs.clone(),
144 epoch_log.checkpoint_epoch,
145 )
146 })
147 .collect::<Vec<_>>();
148
149 Ok((snapshot_epoch, epochs))
150}
151
152impl GlobalBarrierWorkerContext for GlobalBarrierWorkerContextImpl {
153 #[await_tree::instrument]
154 async fn commit_epoch(&self, commit_info: CommitEpochInfo) -> MetaResult<HummockVersionStats> {
155 self.hummock_manager.commit_epoch(commit_info).await?;
156 Ok(self.hummock_manager.get_version_stats().await)
157 }
158
159 #[await_tree::instrument("next_scheduled_barrier")]
160 async fn next_scheduled(&self) -> Scheduled {
161 self.scheduled_barriers.next_scheduled().await
162 }
163
164 fn abort_and_mark_blocked(
165 &self,
166 database_id: Option<DatabaseId>,
167 recovery_reason: RecoveryReason,
168 ) {
169 if database_id.is_none() {
170 self.set_status(BarrierManagerStatus::Recovering(recovery_reason));
171 }
172
173 self.scheduled_barriers
175 .abort_and_mark_blocked(database_id, "cluster is under recovering");
176 }
177
178 fn mark_ready(&self, options: MarkReadyOptions) {
179 let is_global = matches!(&options, MarkReadyOptions::Global { .. });
180 self.scheduled_barriers.mark_ready(options);
181 if is_global {
182 self.set_status(BarrierManagerStatus::Running);
183 }
184 }
185
186 async fn resolve_log_store_epoch<'a>(
187 &'a self,
188 upstream_table_ids: impl Iterator<Item = TableId> + Send + 'a,
189 since_epoch: u64,
190 ) -> MetaResult<SinceTimestampResolvedEpoch> {
191 let upstream_table_ids = upstream_table_ids.collect::<Vec<_>>();
192 if upstream_table_ids.is_empty() {
193 return Err(
194 anyhow::anyhow!("since_timestamp requires at least one upstream table").into(),
195 );
196 }
197
198 self.hummock_manager
199 .on_current_version_and_table_change_log(|version, table_change_log| {
200 let mut unified_log_epochs = None;
201 for &upstream_table_id in &upstream_table_ids {
202 let upstream_committed_epoch = version
203 .state_table_info
204 .info()
205 .get(&upstream_table_id)
206 .map(|info| info.committed_epoch)
207 .ok_or_else(|| {
208 anyhow::anyhow!(
209 "cannot get committed epoch for upstream table {}",
210 upstream_table_id
211 )
212 })?;
213 let table_log_epochs = resolve_since_timestamp_log_store_epoch(
214 upstream_table_id,
215 since_epoch,
216 upstream_committed_epoch,
217 table_change_log,
218 )?;
219 if let Some(unified_log_epochs) = &unified_log_epochs {
220 if unified_log_epochs != &table_log_epochs {
221 return Err(anyhow::anyhow!(
222 "resolved since_timestamp log epochs for upstream table {} do not match previous upstream table log epochs: {:?} vs {:?}",
223 upstream_table_id,
224 table_log_epochs,
225 unified_log_epochs,
226 )
227 .into());
228 }
229 } else {
230 unified_log_epochs = Some(table_log_epochs);
231 }
232 }
233 unified_log_epochs.ok_or_else(|| {
234 anyhow::anyhow!("since_timestamp requires at least one upstream table").into()
235 })
236 })
237 .await
238 }
239
240 #[await_tree::instrument("post_collect_command({command})")]
241 async fn post_collect_command(&self, command: PostCollectCommand) -> MetaResult<()> {
242 Box::pin(command.post_collect(self)).await
243 }
244
245 async fn notify_creating_job_failed(&self, database_id: Option<DatabaseId>, err: String) {
246 self.metadata_manager
247 .notify_finish_failed(database_id, err)
248 .await
249 }
250
251 #[await_tree::instrument("finish_creating_job({job})")]
252 async fn finish_creating_job(&self, job: TrackingJob) -> MetaResult<()> {
253 let job_id = job.job_id();
254 job.finish(&self.metadata_manager, &self.source_manager)
255 .await?;
256 self.env
257 .notification_manager()
258 .notify_local_subscribers(LocalNotification::StreamingJobBackfillFinished(job_id));
259 Ok(())
260 }
261
262 #[await_tree::instrument("finish_cdc_table_backfill({job})")]
263 async fn finish_cdc_table_backfill(&self, job: JobId) -> MetaResult<()> {
264 CdcTableBackfillTracker::mark_complete_job(&self.env.meta_store().conn, job).await
265 }
266
267 #[await_tree::instrument("new_control_stream({})", node.id)]
268 async fn new_control_stream(
269 &self,
270 node: &WorkerNode,
271 init_request: &PbInitRequest,
272 ) -> MetaResult<StreamingControlHandle> {
273 self.new_control_stream_impl(node, init_request).await
274 }
275
276 async fn reload_runtime_info(&self) -> MetaResult<BarrierWorkerRuntimeInfoSnapshot> {
277 self.reload_runtime_info_impl().await
278 }
279
280 async fn reload_database_runtime_info(
281 &self,
282 database_id: DatabaseId,
283 ) -> MetaResult<DatabaseRuntimeInfoSnapshot> {
284 self.reload_database_runtime_info_impl(database_id).await
285 }
286
287 async fn handle_list_finished_source_ids(
288 &self,
289 list_finished: Vec<PbListFinishedSource>,
290 ) -> MetaResult<()> {
291 let mut list_finished_info: HashMap<(TableId, SourceId), HashSet<ActorId>> = HashMap::new();
292
293 for list_finished in list_finished {
294 let table_id = list_finished.table_id;
295 let associated_source_id = list_finished.associated_source_id;
296 list_finished_info
297 .entry((table_id, associated_source_id))
298 .or_default()
299 .insert(list_finished.reporter_actor_id);
300 }
301
302 for ((table_id, associated_source_id), actors) in list_finished_info {
303 let allow_yield = self
304 .refresh_manager
305 .mark_list_stage_finished(table_id, &actors)?;
306
307 if !allow_yield {
308 continue;
309 }
310
311 let Some(database_id) = self
312 .get_source_database_id_for_refresh_stage(table_id, associated_source_id, "list")
313 .await?
314 else {
315 continue;
316 };
317
318 let list_finish_command = Command::ListFinish {
320 table_id,
321 associated_source_id,
322 };
323
324 self.barrier_scheduler
326 .run_command_no_wait(database_id, list_finish_command)
327 .context("Failed to schedule ListFinish command")?;
328
329 tracing::info!(
330 %table_id,
331 %associated_source_id,
332 "ListFinish command scheduled successfully"
333 );
334 }
335 Ok(())
336 }
337
338 async fn handle_load_finished_source_ids(
339 &self,
340 load_finished: Vec<PbLoadFinishedSource>,
341 ) -> MetaResult<()> {
342 let mut load_finished_info: HashMap<(TableId, SourceId), HashSet<ActorId>> = HashMap::new();
343
344 for load_finished in load_finished {
345 let table_id = load_finished.table_id;
346 let associated_source_id = load_finished.associated_source_id;
347 load_finished_info
348 .entry((table_id, associated_source_id))
349 .or_default()
350 .insert(load_finished.reporter_actor_id);
351 }
352
353 for ((table_id, associated_source_id), actors) in load_finished_info {
354 let allow_yield = self
355 .refresh_manager
356 .mark_load_stage_finished(table_id, &actors)?;
357
358 if !allow_yield {
359 continue;
360 }
361
362 let Some(database_id) = self
363 .get_source_database_id_for_refresh_stage(table_id, associated_source_id, "load")
364 .await?
365 else {
366 continue;
367 };
368
369 let load_finish_command = Command::LoadFinish {
371 table_id,
372 associated_source_id,
373 };
374
375 self.barrier_scheduler
377 .run_command_no_wait(database_id, load_finish_command)
378 .context("Failed to schedule LoadFinish command")?;
379
380 tracing::info!(
381 %table_id,
382 %associated_source_id,
383 "LoadFinish command scheduled successfully"
384 );
385 }
386
387 Ok(())
388 }
389
390 async fn handle_refresh_finished_table_ids(
391 &self,
392 refresh_finished_table_job_ids: Vec<JobId>,
393 ) -> MetaResult<()> {
394 for job_id in refresh_finished_table_job_ids {
395 let table_id = job_id.as_mv_table_id();
396
397 self.refresh_manager.mark_refresh_complete(table_id).await?;
398 }
399
400 Ok(())
401 }
402
403 async fn load_batch_refresh_trigger_context(
404 &self,
405 job_id: JobId,
406 database_id: DatabaseId,
407 last_committed_epoch: u64,
408 ) -> MetaResult<BatchRefreshJobTriggerContext> {
409 self.load_batch_refresh_trigger_context_impl(job_id, database_id, last_committed_epoch)
410 .await
411 }
412
413 #[await_tree::instrument]
414 async fn pre_commit_iceberg_pk_index_sink_metadata(
415 &self,
416 reports: Vec<PbIcebergPkIndexSinkMetadata>,
417 ) -> MetaResult<Vec<SinkId>> {
418 let grouped = group_reports_by_sink(reports)?;
419 let success_ids: Vec<SinkId> = grouped.keys().cloned().collect();
420 let futs = FuturesUnordered::new();
421 for (sink_id, (prev_epoch, reports)) in grouped {
422 if reports.is_empty() {
423 continue;
424 }
425 let manager = &self.iceberg_pk_index_sink_manager;
426 futs.push(async move {
427 (
428 sink_id,
429 manager.pre_commit_epoch(sink_id, prev_epoch, reports).await,
430 )
431 });
432 }
433
434 let results: Vec<(SinkId, anyhow::Result<()>)> = futs.collect().await;
437 let errs: Vec<(SinkId, anyhow::Error)> = results
438 .into_iter()
439 .filter_map(|(id, r)| r.err().map(|e| (id, e)))
440 .collect();
441 if errs.is_empty() {
442 Ok(success_ids)
443 } else {
444 Err(aggregate_sink_errors("pre-commit", errs).into())
445 }
446 }
447
448 #[await_tree::instrument]
449 async fn commit_iceberg_pk_index_sink_metadata(&self, sink_ids: Vec<SinkId>) -> MetaResult<()> {
450 let futs = FuturesUnordered::new();
451 for sink_id in sink_ids {
452 let manager = &self.iceberg_pk_index_sink_manager;
453 futs.push(async move { (sink_id, manager.commit_epoch(sink_id).await) });
454 }
455
456 let results: Vec<(SinkId, anyhow::Result<()>)> = futs.collect().await;
457 let errs: Vec<(SinkId, anyhow::Error)> = results
458 .into_iter()
459 .filter_map(|(id, r)| r.err().map(|e| (id, e)))
460 .collect();
461 if errs.is_empty() {
462 Ok(())
463 } else {
464 Err(aggregate_sink_errors("commit", errs).into())
465 }
466 }
467}
468
469fn aggregate_sink_errors(
473 phase: &'static str,
474 mut errs: Vec<(SinkId, anyhow::Error)>,
475) -> anyhow::Error {
476 debug_assert!(!errs.is_empty());
477 let sink_ids: Vec<String> = errs.iter().map(|(id, _)| id.to_string()).collect();
478 let details: Vec<String> = errs
479 .iter()
480 .map(|(id, e)| format!("sink {}: {}", id, e.as_report()))
481 .collect();
482 let (_, first_err) = errs.remove(0);
484 first_err.context(format!(
485 "iceberg v3 sink {} failed for sink_id(s) [{}]: {}",
486 phase,
487 sink_ids.join(", "),
488 details.join("; ")
489 ))
490}
491
492fn group_reports_by_sink(
493 reports: Vec<PbIcebergPkIndexSinkMetadata>,
494) -> MetaResult<HashMap<SinkId, (u64, Vec<PbIcebergPkIndexSinkMetadata>)>> {
495 let mut grouped: HashMap<SinkId, (u64, Vec<PbIcebergPkIndexSinkMetadata>)> = HashMap::new();
496 for r in reports {
497 let sink_id = r.sink_id;
498 let prev_epoch = r.prev_epoch;
499 let entry = grouped.entry(sink_id).or_insert((prev_epoch, Vec::new()));
500 if entry.0 != prev_epoch {
501 return Err(anyhow::anyhow!(
502 "iceberg v3 sink {} reports disagree on prev_epoch: {} vs {}",
503 sink_id,
504 entry.0,
505 prev_epoch
506 )
507 .into());
508 }
509 entry.1.push(r);
510 }
511 Ok(grouped)
512}
513
514impl GlobalBarrierWorkerContextImpl {
515 async fn get_source_database_id_for_refresh_stage(
516 &self,
517 table_id: TableId,
518 associated_source_id: SourceId,
519 stage: &'static str,
520 ) -> MetaResult<Option<DatabaseId>> {
521 match self
522 .metadata_manager
523 .catalog_controller
524 .get_object_database_id(associated_source_id)
525 .await
526 {
527 Ok(database_id) => Ok(Some(database_id)),
528 Err(err) if err.is_catalog_id_not_found("object") => {
529 tracing::warn!(
530 %table_id,
531 %associated_source_id,
532 stage,
533 "skip refresh finish command because associated source is already dropped"
534 );
535 Ok(None)
536 }
537 Err(err) => Err(err)
538 .with_context(|| {
539 format!(
540 "failed to get database id for refresh stage: table_id={}, associated_source_id={}, stage={stage}",
541 table_id, associated_source_id
542 )
543 })
544 .map_err(Into::into),
545 }
546 }
547
548 fn set_status(&self, new_status: BarrierManagerStatus) {
549 self.status.store(Arc::new(new_status));
550 }
551
552 async fn load_batch_refresh_trigger_context_impl(
554 &self,
555 job_id: JobId,
556 database_id: DatabaseId,
557 last_committed_epoch: u64,
558 ) -> MetaResult<BatchRefreshJobTriggerContext> {
559 use itertools::Itertools;
560 use sea_orm::TransactionTrait;
561
562 use crate::controller::scale::load_fragment_context_for_jobs;
563
564 let inner = self
566 .metadata_manager
567 .catalog_controller
568 .get_inner_read_guard()
569 .await;
570 let txn = inner.db.begin().await?;
571
572 let fragment_context =
574 load_fragment_context_for_jobs(&txn, HashSet::from([job_id])).await?;
575
576 let streaming_job_model = fragment_context
577 .job_map
578 .get(&job_id)
579 .ok_or_else(|| anyhow::anyhow!("streaming job model not found for job {}", job_id))?
580 .clone();
581
582 let database_model = fragment_context
583 .database_map
584 .get(&database_id)
585 .ok_or_else(|| {
586 anyhow::anyhow!("database model not found for database {}", database_id)
587 })?;
588 let database_resource_group = database_model.resource_group.clone();
589
590 let mut job_extra_info = self
592 .metadata_manager
593 .catalog_controller
594 .get_streaming_job_extra_info_in_txn(&txn, vec![job_id])
595 .await?;
596 let definition = job_extra_info
597 .remove(&job_id)
598 .ok_or_else(|| anyhow::anyhow!("extra info not found for job {}", job_id))?
599 .job_definition;
600
601 let fragments = fragment_context
603 .job_fragments
604 .get(&job_id)
605 .ok_or_else(|| anyhow::anyhow!("fragments not found for job {}", job_id))?
606 .clone();
607
608 let upstream_table_ids: HashSet<TableId> = {
610 use crate::stream::StreamFragmentGraph;
611 let snapshot_backfill_info = StreamFragmentGraph::collect_snapshot_backfill_info_impl(
612 fragments.values().map(|f| (&f.nodes, f.fragment_type_mask)),
613 )?
614 .0
615 .ok_or_else(|| {
616 anyhow::anyhow!("batch refresh job {} has no snapshot backfill info", job_id)
617 })?;
618 snapshot_backfill_info
619 .upstream_mv_table_id_to_backfill_epoch
620 .into_keys()
621 .collect()
622 };
623
624 let fragment_ids: Vec<_> = fragments.keys().copied().collect();
625 let downstreams = self
626 .metadata_manager
627 .catalog_controller
628 .get_fragment_downstream_relations_in_txn(&txn, fragment_ids)
629 .await?;
630
631 txn.commit().await?;
632 drop(inner);
633
634 let (upstream_table_log_epochs, target_upstream_epoch) = self
636 .hummock_manager
637 .on_current_version_and_table_change_log(|version, table_change_log| {
638 let mut target_upstream_epoch = last_committed_epoch;
639 let mut log_epochs: HashMap<TableId, Vec<(Vec<u64>, u64)>> = HashMap::new();
640
641 for &upstream_table_id in &upstream_table_ids {
642 let upstream_committed_epoch = version
643 .state_table_info
644 .info()
645 .get(&upstream_table_id)
646 .map(|info| info.committed_epoch)
647 .ok_or_else(|| {
648 anyhow::anyhow!(
649 "cannot get committed epoch for upstream table {}",
650 upstream_table_id
651 )
652 })?;
653
654 target_upstream_epoch =
655 std::cmp::max(target_upstream_epoch, upstream_committed_epoch);
656
657 if upstream_committed_epoch <= last_committed_epoch {
658 continue;
659 }
660
661 if let Some(change_log) = table_change_log.get(&upstream_table_id) {
662 let epochs = change_log
663 .filter_epoch((last_committed_epoch, upstream_committed_epoch))
664 .map(|epoch_log| {
665 (
666 epoch_log.non_checkpoint_epochs.clone(),
667 epoch_log.checkpoint_epoch,
668 )
669 })
670 .collect_vec();
671 if !epochs.is_empty() {
672 log_epochs.insert(upstream_table_id, epochs);
673 }
674 } else {
675 anyhow::bail!(
676 "upstream table {} has lagged downstream on epoch {} but no table change log (upstream committed: {})",
677 upstream_table_id,
678 last_committed_epoch,
679 upstream_committed_epoch,
680 );
681 }
682 }
683
684 Ok((log_epochs, target_upstream_epoch))
685 })
686 .await?;
687
688 Ok(BatchRefreshJobTriggerContext {
689 fragments,
690 downstreams,
691 streaming_job_model,
692 definition,
693 database_resource_group,
694 upstream_table_log_epochs,
695 target_upstream_epoch,
696 })
697 }
698}
699
700impl PostCollectCommand {
701 pub async fn post_collect(
704 self,
705 barrier_manager_context: &GlobalBarrierWorkerContextImpl,
706 ) -> MetaResult<()> {
707 match self {
708 PostCollectCommand::Command(_) => {}
709 PostCollectCommand::SourceChangeSplit {
710 split_assignment: assignment,
711 } => {
712 barrier_manager_context
713 .metadata_manager
714 .update_fragment_splits(&assignment)
715 .await?;
716 }
717
718 PostCollectCommand::DropStreamingJobs => {}
719 PostCollectCommand::ConnectorPropsChange(obj_id_map_props) => {
720 barrier_manager_context
722 .source_manager
723 .apply_source_change(SourceChange::UpdateSourceProps {
724 source_id_map_new_props: obj_id_map_props
727 .iter()
728 .map(|(object_id, props)| (object_id.as_source_id(), props.clone()))
729 .collect(),
730 })
731 .await;
732 }
733 PostCollectCommand::ResumeBackfill { target } => match target {
734 ResumeBackfillTarget::Job(job_id) => {
735 barrier_manager_context
736 .metadata_manager
737 .catalog_controller
738 .update_backfill_orders_by_job_id(job_id, None)
739 .await?;
740 }
741 ResumeBackfillTarget::Fragment(fragment_id) => {
742 let mut job_ids = barrier_manager_context
743 .metadata_manager
744 .catalog_controller
745 .get_fragment_job_id(vec![fragment_id])
746 .await?;
747 let job_id = job_ids.pop().ok_or_else(|| {
748 MetaError::invalid_parameter("fragment not found".to_owned())
749 })?;
750 let job_id = JobId::new(job_id.as_raw_id());
751
752 let extra_info = barrier_manager_context
753 .metadata_manager
754 .catalog_controller
755 .get_streaming_job_extra_info(vec![job_id])
756 .await?;
757 let mut backfill_orders: BackfillOrders = extra_info
758 .get(&job_id)
759 .cloned()
760 .ok_or_else(|| MetaError::invalid_parameter("job not found".to_owned()))?
761 .backfill_orders
762 .unwrap_or_default();
763
764 let resumed_fragment_id = fragment_id.as_raw_id();
765 for children in backfill_orders.0.values_mut() {
766 children.retain(|child| *child != resumed_fragment_id);
767 }
768 backfill_orders.0.retain(|_, children| !children.is_empty());
769
770 barrier_manager_context
771 .metadata_manager
772 .catalog_controller
773 .update_backfill_orders_by_job_id(job_id, Some(backfill_orders))
774 .await?;
775 }
776 },
777 PostCollectCommand::CreateStreamingJob {
778 info,
779 job_type,
780 cross_db_snapshot_backfill_info,
781 resolved_split_assignment,
782 } => {
783 match &job_type {
784 CreateStreamingJobType::SinkIntoTable(_) | CreateStreamingJobType::Normal => {
785 barrier_manager_context
786 .metadata_manager
787 .catalog_controller
788 .fill_snapshot_backfill_epoch(
789 info.stream_job_fragments.fragments.iter().filter_map(
790 |(fragment_id, fragment)| {
791 if fragment.fragment_type_mask.contains(
792 FragmentTypeFlag::CrossDbSnapshotBackfillStreamScan,
793 ) {
794 Some(*fragment_id as _)
795 } else {
796 None
797 }
798 },
799 ),
800 None,
801 &cross_db_snapshot_backfill_info,
802 )
803 .await?
804 }
805 CreateStreamingJobType::SnapshotBackfill {
806 snapshot_backfill_info,
807 ..
808 }
809 | CreateStreamingJobType::BatchRefresh(BatchRefreshInfo {
810 snapshot_backfill_info,
811 ..
812 }) => {
813 barrier_manager_context
814 .metadata_manager
815 .catalog_controller
816 .fill_snapshot_backfill_epoch(
817 info.stream_job_fragments.fragments.iter().filter_map(
818 |(fragment_id, fragment)| {
819 if fragment.fragment_type_mask.contains_any([
820 FragmentTypeFlag::SnapshotBackfillStreamScan,
821 FragmentTypeFlag::CrossDbSnapshotBackfillStreamScan,
822 ]) {
823 Some(*fragment_id as _)
824 } else {
825 None
826 }
827 },
828 ),
829 Some(snapshot_backfill_info),
830 &cross_db_snapshot_backfill_info,
831 )
832 .await?
833 }
834 }
835
836 let CreateStreamingJobCommandInfo {
839 stream_job_fragments,
840 upstream_fragment_downstreams,
841 replace_sink,
842 ..
843 } = info;
844 let new_job_id = stream_job_fragments.stream_job_id();
845 let new_sink_downstream =
846 if let CreateStreamingJobType::SinkIntoTable(ctx) = job_type {
847 let new_downstreams = ctx.new_sink_downstream.clone();
848 let new_downstreams = FragmentDownstreamRelation::from([(
849 ctx.sink_fragment_id,
850 vec![new_downstreams],
851 )]);
852 Some(new_downstreams)
853 } else {
854 None
855 };
856
857 let old_state_table_ids = barrier_manager_context
858 .metadata_manager
859 .catalog_controller
860 .post_collect_job_fragments(
861 new_job_id,
862 &upstream_fragment_downstreams,
863 new_sink_downstream,
864 Some(&resolved_split_assignment),
865 replace_sink.as_ref(),
866 )
867 .await?;
868
869 let added_source_fragments = stream_job_fragments.stream_source_fragments();
870 let added_backfill_fragments = stream_job_fragments.source_backfill_fragments();
871 if !added_source_fragments.is_empty() || !added_backfill_fragments.is_empty() {
873 barrier_manager_context
874 .source_manager
875 .apply_source_change(SourceChange::CreateJob {
876 added_source_fragments,
877 added_backfill_fragments,
878 })
879 .await;
880 }
881
882 if let Some(old_sink_id) = replace_sink {
883 barrier_manager_context
884 .sink_manager
885 .stop_sink_coordinator(vec![old_sink_id])
886 .await;
887 cleanup_dropped_streaming_jobs(
888 &barrier_manager_context.refresh_manager,
889 &barrier_manager_context.hummock_manager,
890 &barrier_manager_context.metadata_manager,
891 [old_sink_id.as_job_id()],
892 old_state_table_ids.expect("replace sink should return old state tables"),
893 "replace_sink",
894 )
895 .await?;
896 }
897 }
898 PostCollectCommand::Reschedule { reschedules, .. } => {
899 let fragment_splits = reschedules
900 .iter()
901 .map(|(fragment_id, reschedule)| {
902 (*fragment_id, reschedule.actor_splits.clone())
903 })
904 .collect();
905
906 barrier_manager_context
907 .metadata_manager
908 .update_fragment_splits(&fragment_splits)
909 .await?;
910 }
911
912 PostCollectCommand::ReplaceStreamJob {
913 plan: replace_plan,
914 resolved_split_assignment,
915 } => {
916 let ReplaceStreamJobPlan {
917 old_fragments,
918 new_fragments,
919 upstream_fragment_downstreams,
920 to_drop_state_table_ids,
921 auto_refresh_schema_sinks,
922 ..
923 } = &replace_plan;
924 barrier_manager_context
926 .metadata_manager
927 .catalog_controller
928 .post_collect_job_fragments(
929 new_fragments.stream_job_id,
930 upstream_fragment_downstreams,
931 None,
932 Some(&resolved_split_assignment),
933 None,
934 )
935 .await?;
936
937 if let Some(sinks) = auto_refresh_schema_sinks {
938 for sink in sinks {
939 barrier_manager_context
940 .metadata_manager
941 .catalog_controller
942 .post_collect_job_fragments(
943 sink.tmp_sink_id.as_job_id(),
944 &Default::default(), None, None, None,
948 )
949 .await?;
950 }
951 }
952
953 barrier_manager_context
955 .source_manager
956 .handle_replace_job(
957 old_fragments,
958 new_fragments.stream_source_fragments(),
959 &replace_plan,
960 )
961 .await;
962 cleanup_dropped_streaming_jobs(
963 &barrier_manager_context.refresh_manager,
964 &barrier_manager_context.hummock_manager,
965 &barrier_manager_context.metadata_manager,
966 [],
967 to_drop_state_table_ids.clone(),
968 "replace_streaming_job",
969 )
970 .await?;
971 }
972
973 PostCollectCommand::CreateSubscription { subscription_id } => {
974 barrier_manager_context
975 .metadata_manager
976 .catalog_controller
977 .finish_create_subscription_catalog(subscription_id)
978 .await?
979 }
980 }
981
982 Ok(())
983 }
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}