Skip to main content

risingwave_meta/barrier/context/
context_impl.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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    // `binary_search_by_checkpoint_epoch` searches only by the checkpoint epoch
109    // of each changelog entry. An entry may still cover earlier non-checkpoint
110    // epochs, e.g. `{ non_checkpoint_epochs: [30, 35], checkpoint_epoch: 40 }`
111    // covers `(20, 40]` if the previous checkpoint is 20. For `since_epoch =
112    // 35`, the search returns `Err(index_of_40)`, and 40 is the snapshot epoch.
113    // For `since_epoch = 40`, it returns `Ok(index_of_40)`. In both cases, the
114    // resolved snapshot epoch is the least checkpoint epoch >= `since_epoch`.
115    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    // A request inside the latest changelog entry may be smaller than
131    // `upstream_committed_epoch`, but still resolve to the latest checkpoint.
132    // That end-of-log case is not a usable historical snapshot epoch.
133    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        // Mark blocked and abort buffered schedules, they might be dirty already.
185        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        // The scheduler re-runs the cycles abandoned by the recovery.
211        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        // Drain all futures regardless of individual failures, so that no coordinator is left with
409        // state inconsistent vs. the caller's view.
410        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
452/// Combine per-sink errors from a fan-out into a single `anyhow::Error`. The first failing
453/// sink's error is used as the source so the original chain is preserved; the message lists
454/// every failing `sink_id` and its error stringified.
455fn 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    // Preserve the first error's chain as the cause.
466    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    /// Load the context metadata and resolve upstream log epochs for a batch refresh trigger.
481    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        // Load metadata from the catalog under a single transaction.
493        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        // 1. Load fragment context (job model, database model).
501        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        // 2. Load job definition.
519        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        // 3. Get fragments from fragment_context and load downstream relations.
530        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        // Derive upstream table IDs from the snapshot backfill scan nodes in the fragments.
537        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        // Resolve upstream log epochs from the hummock changelog.
563        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    /// Do some stuffs after barriers are collected and the new storage version is committed, for
630    /// the given command.
631    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                // todo: we dont know the type of the object id, it can be a source or a sink. Should carry more info in the barrier command.
679                barrier_manager_context
680                    .source_manager
681                    .apply_source_change(SourceChange::UpdateSourceProps {
682                        // Only sources are managed in source manager. Convert object IDs to source IDs and let
683                        // source manager ignore unknown/unregistered sources.
684                        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                // Do `post_collect_job_fragments` of the original streaming job in the end, so that in any previous failure,
791                // we won't mark the job as `Creating`, and then the job will be later clean by the recovery triggered by the returned error.
792                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                // Skip the source-manager notification (and its `core` lock) when the job has no source/backfill fragments.
827                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                // Update actors and actor_dispatchers for new table fragments.
880                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(), // upstream_fragment_downstreams is already inserted in the job of upstream table
901                                None, // no replace plan
902                                None, // no init split assignment
903                                None,
904                                false,
905                            )
906                            .await?;
907                    }
908                }
909
910                // Apply the split changes in source manager.
911                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}