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 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    // `binary_search_by_checkpoint_epoch` searches only by the checkpoint epoch
105    // of each changelog entry. An entry may still cover earlier non-checkpoint
106    // epochs, e.g. `{ non_checkpoint_epochs: [30, 35], checkpoint_epoch: 40 }`
107    // covers `(20, 40]` if the previous checkpoint is 20. For `since_epoch =
108    // 35`, the search returns `Err(index_of_40)`, and 40 is the snapshot epoch.
109    // For `since_epoch = 40`, it returns `Ok(index_of_40)`. In both cases, the
110    // resolved snapshot epoch is the least checkpoint epoch >= `since_epoch`.
111    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    // A request inside the latest changelog entry may be smaller than
127    // `upstream_committed_epoch`, but still resolve to the latest checkpoint.
128    // That end-of-log case is not a usable historical snapshot epoch.
129    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        // Mark blocked and abort buffered schedules, they might be dirty already.
174        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            // Create ListFinish command
319            let list_finish_command = Command::ListFinish {
320                table_id,
321                associated_source_id,
322            };
323
324            // Schedule the command through the barrier system without waiting
325            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            // Create LoadFinish command
370            let load_finish_command = Command::LoadFinish {
371                table_id,
372                associated_source_id,
373            };
374
375            // Schedule the command through the barrier system without waiting
376            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        // Drain all futures regardless of individual failures, so that no coordinator is left with
435        // state inconsistent vs. the caller's view.
436        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
469/// Combine per-sink errors from a fan-out into a single `anyhow::Error`. The first failing
470/// sink's error is used as the source so the original chain is preserved; the message lists
471/// every failing `sink_id` and its error stringified.
472fn 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    // Preserve the first error's chain as the cause.
483    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    /// Load the context metadata and resolve upstream log epochs for a batch refresh trigger.
553    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        // Load metadata from the catalog under a single transaction.
565        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        // 1. Load fragment context (job model, database model).
573        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        // 2. Load job definition.
591        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        // 3. Get fragments from fragment_context and load downstream relations.
602        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        // Derive upstream table IDs from the snapshot backfill scan nodes in the fragments.
609        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        // Resolve upstream log epochs from the hummock changelog.
635        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    /// Do some stuffs after barriers are collected and the new storage version is committed, for
702    /// the given command.
703    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                // 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.
721                barrier_manager_context
722                    .source_manager
723                    .apply_source_change(SourceChange::UpdateSourceProps {
724                        // Only sources are managed in source manager. Convert object IDs to source IDs and let
725                        // source manager ignore unknown/unregistered sources.
726                        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                // Do `post_collect_job_fragments` of the original streaming job in the end, so that in any previous failure,
837                // we won't mark the job as `Creating`, and then the job will be later clean by the recovery triggered by the returned error.
838                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                // Skip the source-manager notification (and its `core` lock) when the job has no source/backfill fragments.
872                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                // Update actors and actor_dispatchers for new table fragments.
925                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(), // upstream_fragment_downstreams is already inserted in the job of upstream table
945                                None, // no replace plan
946                                None, // no init split assignment
947                                None,
948                            )
949                            .await?;
950                    }
951                }
952
953                // Apply the split changes in source manager.
954                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}