Skip to main content

risingwave_meta/stream/
stream_manager.rs

1// Copyright 2022 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;
16use std::sync::Arc;
17
18use anyhow::Context;
19use await_tree::span;
20use futures::future::join_all;
21use itertools::Itertools;
22use risingwave_common::bail;
23use risingwave_common::catalog::{DatabaseId, Field, FragmentTypeFlag, FragmentTypeMask, TableId};
24use risingwave_common::hash::VnodeCountCompat;
25use risingwave_common::id::{JobId, SinkId};
26use risingwave_connector::source::CdcTableSnapshotSplitRaw;
27use risingwave_meta_model::prelude::Fragment as FragmentModel;
28use risingwave_meta_model::{StreamingParallelism, WorkerId, fragment, streaming_job};
29use risingwave_pb::catalog::{CreateType, PbSink, PbTable, Subscription};
30use risingwave_pb::ddl_service::streaming_job_resource_type;
31use risingwave_pb::expr::PbExprNode;
32use risingwave_pb::plan_common::{PbColumnCatalog, PbField};
33use risingwave_pb::serverless_backfill_controller::{
34    ProvisionRequest, node_group_controller_service_client,
35};
36use risingwave_rpc_client::error::TonicStatusWrapper;
37use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect};
38use thiserror_ext::AsReport;
39use tokio::sync::{Mutex, OwnedSemaphorePermit, oneshot};
40use tracing::Instrument;
41
42use super::{
43    GlobalRefreshManagerRef, ParallelismPolicy, ReschedulePolicy, ScaleControllerRef,
44    StreamFragmentGraph, UserDefinedFragmentBackfillOrder,
45};
46use crate::barrier::{
47    BarrierScheduler, BatchRefreshInfo, Command, CreateStreamingJobCommandInfo,
48    CreateStreamingJobType, ReplaceStreamJobPlan, SinceEpochInfo, SnapshotBackfillInfo,
49};
50use crate::controller::catalog::DropTableConnectorContext;
51use crate::controller::fragment::{InflightActorInfo, InflightFragmentInfo};
52use crate::error::bail_invalid_parameter;
53use crate::hummock::HummockManagerRef;
54use crate::manager::{
55    MetaSrvEnv, MetadataManager, NotificationVersion, StreamingJob, StreamingJobType,
56};
57use crate::model::{
58    ActorId, DownstreamFragmentRelation, Fragment, FragmentDownstreamRelation, FragmentId,
59    FragmentReplaceUpstream, StreamActor, StreamContext, StreamJobFragments,
60    StreamJobFragmentsToCreate, SubscriptionId,
61};
62use crate::stream::{ReplaceJobSplitPlan, SourceManagerRef};
63use crate::{MetaError, MetaResult};
64
65pub type GlobalStreamManagerRef = Arc<GlobalStreamManager>;
66
67pub(crate) async fn cleanup_dropped_streaming_jobs(
68    refresh_manager: &GlobalRefreshManagerRef,
69    hummock_manager: &HummockManagerRef,
70    metadata_manager: &MetadataManager,
71    streaming_job_ids: impl IntoIterator<Item = JobId>,
72    state_table_ids: Vec<TableId>,
73    progress_status: &str,
74) -> MetaResult<()> {
75    for job_id in streaming_job_ids {
76        refresh_manager.remove_progress_tracker(job_id.as_mv_table_id(), progress_status);
77    }
78
79    if state_table_ids.is_empty() {
80        return Ok(());
81    }
82
83    hummock_manager
84        .unregister_table_ids(state_table_ids.clone())
85        .await?;
86    metadata_manager
87        .catalog_controller
88        .complete_dropped_tables(state_table_ids)
89        .await;
90    Ok(())
91}
92
93#[derive(Default)]
94pub struct CreateStreamingJobOption {
95    // leave empty as a placeholder for future option if there is any
96}
97
98#[derive(Debug, Clone)]
99pub struct UpstreamSinkInfo {
100    pub sink_id: SinkId,
101    pub sink_fragment_id: FragmentId,
102    pub sink_output_fields: Vec<PbField>,
103    // for backwards compatibility
104    pub sink_original_target_columns: Vec<PbColumnCatalog>,
105    pub project_exprs: Vec<PbExprNode>,
106    pub new_sink_downstream: DownstreamFragmentRelation,
107}
108
109/// [`CreateStreamingJobContext`] carries one-time infos for creating a streaming job.
110///
111/// Note: for better readability, keep this struct complete and immutable once created.
112pub struct CreateStreamingJobContext {
113    /// New fragment relation to add from upstream fragments to downstream fragments.
114    pub upstream_fragment_downstreams: FragmentDownstreamRelation,
115
116    /// The resource group of the database this job belongs to.
117    pub database_resource_group: String,
118
119    /// DDL definition.
120    pub definition: String,
121
122    pub create_type: CreateType,
123
124    pub job_type: StreamingJobType,
125
126    /// Used for sink-into-table.
127    pub new_upstream_sink: Option<UpstreamSinkInfo>,
128
129    pub snapshot_backfill_info: Option<SnapshotBackfillInfo>,
130    pub cross_db_snapshot_backfill_info: SnapshotBackfillInfo,
131
132    pub cdc_table_snapshot_splits: Option<Vec<CdcTableSnapshotSplitRaw>>,
133
134    pub option: CreateStreamingJobOption,
135
136    pub streaming_job: StreamingJob,
137
138    pub fragment_backfill_ordering: UserDefinedFragmentBackfillOrder,
139
140    pub locality_fragment_state_table_mapping: HashMap<FragmentId, Vec<TableId>>,
141
142    pub is_serverless_backfill: bool,
143
144    pub resource_type: streaming_job_resource_type::ResourceType,
145
146    /// The `streaming_job::Model` for this job, loaded from meta store.
147    pub streaming_job_model: streaming_job::Model,
148
149    /// If set, this create command replaces an existing sink while creating the new sink job.
150    pub replace_sink: Option<SinkId>,
151
152    /// Batch refresh interval in seconds. If set, the MV uses batch refresh semantics.
153    pub refresh_interval_sec: Option<u64>,
154
155    pub since_timestamp_epoch: Option<u64>,
156}
157
158struct StreamingJobExecution {
159    id: JobId,
160    shutdown_tx: Option<oneshot::Sender<oneshot::Sender<bool>>>,
161    _permit: OwnedSemaphorePermit,
162}
163
164impl StreamingJobExecution {
165    fn new(
166        id: JobId,
167        shutdown_tx: oneshot::Sender<oneshot::Sender<bool>>,
168        permit: OwnedSemaphorePermit,
169    ) -> Self {
170        Self {
171            id,
172            shutdown_tx: Some(shutdown_tx),
173            _permit: permit,
174        }
175    }
176}
177
178#[derive(Default)]
179struct CreatingStreamingJobInfo {
180    streaming_jobs: Mutex<HashMap<JobId, StreamingJobExecution>>,
181}
182
183impl CreatingStreamingJobInfo {
184    async fn add_job(&self, job: StreamingJobExecution) {
185        let mut jobs = self.streaming_jobs.lock().await;
186        jobs.insert(job.id, job);
187    }
188
189    async fn delete_job(&self, job_id: JobId) {
190        let mut jobs = self.streaming_jobs.lock().await;
191        jobs.remove(&job_id);
192    }
193
194    async fn cancel_jobs(
195        &self,
196        job_ids: Vec<JobId>,
197    ) -> MetaResult<(HashMap<JobId, oneshot::Receiver<bool>>, Vec<JobId>)> {
198        let mut jobs = self.streaming_jobs.lock().await;
199        let mut receivers = HashMap::new();
200        let mut background_job_ids = vec![];
201        for job_id in job_ids {
202            if let Some(job) = jobs.get_mut(&job_id) {
203                if let Some(shutdown_tx) = job.shutdown_tx.take() {
204                    let (tx, rx) = oneshot::channel();
205                    match shutdown_tx.send(tx) {
206                        Ok(()) => {
207                            receivers.insert(job_id, rx);
208                        }
209                        Err(_) => {
210                            return Err(anyhow::anyhow!(
211                                "failed to send shutdown signal for streaming job {}: receiver dropped",
212                                job_id
213                            )
214                            .into());
215                        }
216                    }
217                }
218            } else {
219                // If these job ids do not exist in streaming_jobs, they should be background creating jobs.
220                background_job_ids.push(job_id);
221            }
222        }
223
224        Ok((receivers, background_job_ids))
225    }
226}
227
228type CreatingStreamingJobInfoRef = Arc<CreatingStreamingJobInfo>;
229
230#[derive(Debug, Clone)]
231pub struct AutoRefreshSchemaSinkContext {
232    pub tmp_sink_id: SinkId,
233    pub original_sink: PbSink,
234    pub original_fragment: Fragment,
235    pub new_schema: Vec<PbColumnCatalog>,
236    pub newly_add_fields: Vec<Field>,
237    pub removed_column_names: Vec<String>,
238    pub new_fragment: Fragment,
239    pub new_log_store_table: Option<Box<PbTable>>,
240    /// The sink's own stream context (timezone, `config_override`).
241    pub ctx: StreamContext,
242}
243
244impl AutoRefreshSchemaSinkContext {
245    pub fn new_fragment_info(
246        &self,
247        stream_actors: &HashMap<FragmentId, Vec<StreamActor>>,
248        actor_location: &HashMap<ActorId, WorkerId>,
249    ) -> InflightFragmentInfo {
250        InflightFragmentInfo {
251            fragment_id: self.new_fragment.fragment_id,
252            distribution_type: self.new_fragment.distribution_type.into(),
253            fragment_type_mask: self.new_fragment.fragment_type_mask,
254            vnode_count: self.new_fragment.vnode_count(),
255            nodes: self.new_fragment.nodes.clone(),
256            actors: stream_actors
257                .get(&self.new_fragment.fragment_id)
258                .into_iter()
259                .flatten()
260                .map(|actor| {
261                    (
262                        actor.actor_id,
263                        InflightActorInfo {
264                            worker_id: actor_location[&actor.actor_id],
265                            vnode_bitmap: actor.vnode_bitmap.clone(),
266                            splits: vec![],
267                        },
268                    )
269                })
270                .collect(),
271            state_table_ids: self.new_fragment.state_table_ids.iter().copied().collect(),
272        }
273    }
274}
275
276/// [`ReplaceStreamJobContext`] carries one-time infos for replacing the plan of an existing stream job.
277///
278/// Note: for better readability, keep this struct complete and immutable once created.
279pub struct ReplaceStreamJobContext {
280    /// The old job fragments to be replaced.
281    pub old_fragments: StreamJobFragments,
282
283    /// The updates to be applied to the downstream chain actors. Used for schema change.
284    pub replace_upstream: FragmentReplaceUpstream,
285
286    /// New fragment relation to add from existing upstream fragment to downstream fragment.
287    pub upstream_fragment_downstreams: FragmentDownstreamRelation,
288
289    pub streaming_job: StreamingJob,
290
291    /// The resource group of the database this job belongs to.
292    pub database_resource_group: String,
293
294    pub tmp_id: JobId,
295
296    /// Used for dropping an associated source. Dropping source and related internal tables.
297    pub drop_table_connector_ctx: Option<DropTableConnectorContext>,
298
299    pub auto_refresh_schema_sinks: Option<Vec<AutoRefreshSchemaSinkContext>>,
300
301    /// The `streaming_job::Model` for this job, loaded from meta store.
302    pub streaming_job_model: streaming_job::Model,
303}
304
305/// `GlobalStreamManager` manages all the streams in the system.
306pub struct GlobalStreamManager {
307    pub env: MetaSrvEnv,
308
309    pub metadata_manager: MetadataManager,
310
311    /// Broadcasts and collect barriers
312    pub barrier_scheduler: BarrierScheduler,
313
314    pub hummock_manager: HummockManagerRef,
315
316    /// Maintains streaming sources from external system like kafka
317    pub source_manager: SourceManagerRef,
318
319    pub refresh_manager: GlobalRefreshManagerRef,
320
321    /// Creating streaming job info.
322    creating_job_info: CreatingStreamingJobInfoRef,
323
324    pub scale_controller: ScaleControllerRef,
325}
326
327impl GlobalStreamManager {
328    pub fn new(
329        env: MetaSrvEnv,
330        metadata_manager: MetadataManager,
331        barrier_scheduler: BarrierScheduler,
332        hummock_manager: HummockManagerRef,
333        source_manager: SourceManagerRef,
334        refresh_manager: GlobalRefreshManagerRef,
335        scale_controller: ScaleControllerRef,
336    ) -> MetaResult<Self> {
337        Ok(Self {
338            env,
339            metadata_manager,
340            barrier_scheduler,
341            hummock_manager,
342            source_manager,
343            refresh_manager,
344            creating_job_info: Arc::new(CreatingStreamingJobInfo::default()),
345            scale_controller,
346        })
347    }
348
349    /// Create streaming job, it works as follows:
350    ///
351    /// 1. Broadcast the actor info based on the scheduling result in the context, build the hanging
352    ///    channels in upstream worker nodes.
353    /// 2. (optional) Get the split information of the `StreamSource` via source manager and patch
354    ///    actors.
355    /// 3. Notify related worker nodes to update and build the actors.
356    /// 4. Store related meta data.
357    ///
358    /// This function is a wrapper over [`Self::run_create_streaming_job_command`].
359    #[await_tree::instrument]
360    pub async fn create_streaming_job(
361        self: &Arc<Self>,
362        stream_job_fragments: StreamJobFragmentsToCreate,
363        ctx: CreateStreamingJobContext,
364        permit: OwnedSemaphorePermit,
365    ) -> MetaResult<NotificationVersion> {
366        let await_tree_key = format!("Create Streaming Job Worker ({})", ctx.streaming_job.id());
367        let await_tree_span = span!(
368            "{:?}({})",
369            ctx.streaming_job.job_type(),
370            ctx.streaming_job.name()
371        );
372
373        let job_id = stream_job_fragments.stream_job_id();
374        let database_id = ctx.streaming_job.database_id();
375
376        let (cancel_tx, cancel_rx) = oneshot::channel();
377        let execution = StreamingJobExecution::new(job_id, cancel_tx, permit);
378        self.creating_job_info.add_job(execution).await;
379
380        let stream_manager = self.clone();
381        let fut = async move {
382            let create_type = ctx.create_type;
383            let streaming_job = stream_manager
384                .run_create_streaming_job_command(stream_job_fragments, ctx)
385                .await?;
386            let version = match create_type {
387                CreateType::Background => {
388                    stream_manager
389                        .env
390                        .notification_manager_ref()
391                        .current_version()
392                        .await
393                }
394                CreateType::Foreground => {
395                    stream_manager
396                        .metadata_manager
397                        .wait_streaming_job_finished(database_id, streaming_job.id() as _)
398                        .await?
399                }
400                CreateType::Unspecified => unreachable!(),
401            };
402
403            tracing::debug!(?streaming_job, "stream job finish");
404            Ok(version)
405        }
406        .in_current_span();
407
408        let create_fut = (self.env.await_tree_reg())
409            .register(await_tree_key, await_tree_span)
410            .instrument(Box::pin(fut));
411
412        let result = async {
413            tokio::select! {
414                biased;
415
416                res = create_fut => res,
417                notifier = cancel_rx => {
418                    let notifier = notifier.expect("sender should not be dropped");
419                    tracing::debug!(id=%job_id, "cancelling streaming job");
420
421                    enum CancelResult {
422                        Completed(MetaResult<NotificationVersion>),
423                        Failed(MetaError),
424                        Cancelled,
425                    }
426
427                    let cancel_res = if let Ok(job_fragments) =
428                        self.metadata_manager.get_job_fragments_by_id(job_id).await
429                    {
430                        // try to cancel buffered creating command.
431                        if self
432                            .barrier_scheduler
433                            .try_cancel_scheduled_create(database_id, job_id)
434                        {
435                            tracing::debug!(
436                                id=%job_id,
437                                "cancelling streaming job in buffer queue."
438                            );
439                            CancelResult::Cancelled
440                        } else if !job_fragments.is_created() {
441                            tracing::debug!(
442                                id=%job_id,
443                                "cancelling streaming job by issue cancel command."
444                            );
445
446                            let cancel_result: MetaResult<()> = async {
447                                let cancel_command = self.metadata_manager.catalog_controller
448                                    .build_cancel_command(&job_fragments)
449                                    .await?;
450                                let cleanup_state_table_ids =
451                                    job_fragments.all_table_ids().collect_vec();
452                                self.metadata_manager.catalog_controller
453                                    .try_abort_creating_streaming_job(job_id, true)
454                                    .await?;
455
456                                self.barrier_scheduler
457                                    .run_command(database_id, cancel_command)
458                                    .await?;
459                                cleanup_dropped_streaming_jobs(
460                                    &self.refresh_manager,
461                                    &self.hummock_manager,
462                                    &self.metadata_manager,
463                                    [job_id],
464                                    cleanup_state_table_ids,
465                                    "cancel_streaming_job",
466                                )
467                                .await?;
468                                Ok(())
469                            }
470                            .await;
471
472                            match cancel_result {
473                                Ok(()) => CancelResult::Cancelled,
474                                Err(err) => {
475                                    tracing::warn!(
476                                        error = ?err.as_report(),
477                                        id = %job_id,
478                                        "failed to run cancel command for creating streaming job"
479                                    );
480                                    CancelResult::Failed(err)
481                                }
482                            }
483                        } else {
484                            // streaming job is already completed
485                            CancelResult::Completed(
486                                self.metadata_manager
487                                    .wait_streaming_job_finished(database_id, job_id)
488                                    .await,
489                            )
490                        }
491                    } else {
492                        CancelResult::Cancelled
493                    };
494
495                    let (cancelled, result) = match cancel_res {
496                        CancelResult::Completed(result) => (false, result),
497                        CancelResult::Failed(err) => (false, Err(err)),
498                        CancelResult::Cancelled => (true, Err(MetaError::cancelled("create"))),
499                    };
500
501                    let _ = notifier
502                        .send(cancelled)
503                        .inspect_err(|err| tracing::warn!("failed to notify cancellation result: {err}"));
504
505                    result
506                }
507            }
508        }
509        .await;
510
511        tracing::debug!("cleaning creating job info: {}", job_id);
512        self.creating_job_info.delete_job(job_id).await;
513        result
514    }
515
516    async fn provision_serverless_backfill_resource_group(&self) -> MetaResult<String> {
517        let sbc_addr = &self.env.opts.serverless_backfill_controller_addr;
518        if sbc_addr.is_empty() {
519            bail_invalid_parameter!(
520                "Serverless Backfill is disabled. Use RisingWave cloud at https://cloud.risingwave.com/auth/signup to try this feature"
521            );
522        }
523
524        let request = tonic::Request::new(ProvisionRequest {});
525        let mut client =
526            node_group_controller_service_client::NodeGroupControllerServiceClient::connect(
527                sbc_addr.clone(),
528            )
529            .await
530            .with_context(|| {
531                format!(
532                    "unable to reach serverless backfill controller at addr {}",
533                    sbc_addr
534                )
535            })?;
536
537        match client.provision(request).await {
538            Ok(resp) => Ok(resp.into_inner().resource_group),
539            Err(e) => Err(anyhow::Error::new(TonicStatusWrapper::new(e))
540                .context("serverless backfill controller returned error")
541                .into()),
542        }
543    }
544
545    async fn finalize_create_streaming_job_resource_group(
546        &self,
547        resource_type: &streaming_job_resource_type::ResourceType,
548        streaming_job_model: &mut streaming_job::Model,
549    ) -> MetaResult<()> {
550        if !matches!(
551            resource_type,
552            streaming_job_resource_type::ResourceType::ServerlessBackfill(true)
553        ) {
554            return Ok(());
555        }
556
557        let group = self.provision_serverless_backfill_resource_group().await?;
558        tracing::info!(
559            resource_group = group,
560            "provisioning serverless backfill resource group"
561        );
562
563        self.metadata_manager
564            .catalog_controller
565            .update_streaming_job_resource_group(streaming_job_model.job_id, group.clone())
566            .await?;
567        streaming_job_model.specific_resource_group = Some(group);
568
569        Ok(())
570    }
571
572    /// The function will return after barrier collected
573    /// ([`crate::manager::MetadataManager::wait_streaming_job_finished`]).
574    #[await_tree::instrument]
575    async fn run_create_streaming_job_command(
576        &self,
577        stream_job_fragments: StreamJobFragmentsToCreate,
578        CreateStreamingJobContext {
579            streaming_job,
580            upstream_fragment_downstreams,
581            database_resource_group,
582            definition,
583            create_type,
584            job_type,
585            new_upstream_sink,
586            snapshot_backfill_info,
587            cross_db_snapshot_backfill_info,
588            fragment_backfill_ordering,
589            locality_fragment_state_table_mapping,
590            cdc_table_snapshot_splits,
591            is_serverless_backfill,
592            resource_type,
593            mut streaming_job_model,
594            replace_sink,
595            refresh_interval_sec,
596            since_timestamp_epoch,
597            ..
598        }: CreateStreamingJobContext,
599    ) -> MetaResult<StreamingJob> {
600        tracing::debug!(
601            table_id = %stream_job_fragments.stream_job_id(),
602            "built actors finished"
603        );
604
605        // Phase 1: Gather fragment-level split information.
606        // - For source fragments: discover splits from the external source.
607        // - For backfill fragments: splits will be aligned in Phase 2 inside the barrier worker
608        //   using the actor-level no-shuffle mapping produced by render_actors.
609        let init_split_assignment = self
610            .source_manager
611            .discover_splits(&stream_job_fragments)
612            .await?;
613
614        let fragment_backfill_ordering =
615            StreamFragmentGraph::extend_fragment_backfill_ordering_with_locality_backfill(
616                fragment_backfill_ordering,
617                &stream_job_fragments.downstreams,
618                || {
619                    stream_job_fragments
620                        .fragments
621                        .iter()
622                        .map(|(fragment_id, fragment)| {
623                            (*fragment_id, fragment.fragment_type_mask, &fragment.nodes)
624                        })
625                },
626            );
627
628        self.finalize_create_streaming_job_resource_group(&resource_type, &mut streaming_job_model)
629            .await?;
630
631        let info = CreateStreamingJobCommandInfo {
632            stream_job_fragments,
633            upstream_fragment_downstreams,
634            init_split_assignment,
635            definition: definition.clone(),
636            streaming_job: streaming_job.clone(),
637            job_type,
638            create_type,
639            database_resource_group,
640            fragment_backfill_ordering,
641            cdc_table_snapshot_splits,
642            locality_fragment_state_table_mapping,
643            is_serverless: is_serverless_backfill,
644            streaming_job_model,
645            replace_sink,
646            refresh_interval_sec,
647        };
648
649        let job_type = if let Some(refresh_interval_sec) = refresh_interval_sec {
650            if since_timestamp_epoch.is_some() {
651                bail!("since_timestamp should not be specified when no snapshot backfill");
652            }
653            let snapshot_backfill_info = snapshot_backfill_info.ok_or_else(|| {
654                anyhow::anyhow!(
655                    "batch refresh materialized view must have snapshot backfill upstream"
656                )
657            })?;
658            // Batch refresh jobs must not contain source or source-backfill nodes,
659            // because we skip split assignment resolution for them.
660            for fragment in info.stream_job_fragments.inner.fragments.values() {
661                let mask = fragment.fragment_type_mask;
662                if mask.contains(FragmentTypeFlag::Source)
663                    || mask.contains(FragmentTypeFlag::SourceScan)
664                {
665                    bail!(
666                        "batch refresh materialized views must not depend on sources directly; \
667                         fragment {} has source/source-backfill nodes",
668                        fragment.fragment_id
669                    );
670                }
671            }
672            tracing::debug!(
673                ?snapshot_backfill_info,
674                refresh_interval_sec,
675                "sending Command::CreateBatchRefreshStreamingJob"
676            );
677            CreateStreamingJobType::BatchRefresh(BatchRefreshInfo {
678                snapshot_backfill_info,
679                refresh_interval_sec,
680            })
681        } else if let Some(snapshot_backfill_info) = snapshot_backfill_info {
682            tracing::debug!(
683                ?snapshot_backfill_info,
684                "sending Command::CreateSnapshotBackfillStreamingJob"
685            );
686            CreateStreamingJobType::SnapshotBackfill {
687                snapshot_backfill_info,
688                since_epoch: since_timestamp_epoch.map(|provided_since_epoch| SinceEpochInfo {
689                    provided_since_epoch,
690                    resolved: None,
691                }),
692            }
693        } else {
694            if since_timestamp_epoch.is_some() {
695                bail!("since_timestamp should not be specified when no snapshot backfill");
696            }
697            tracing::debug!("sending Command::CreateStreamingJob");
698            if let Some(new_upstream_sink) = new_upstream_sink {
699                CreateStreamingJobType::SinkIntoTable(new_upstream_sink)
700            } else {
701                CreateStreamingJobType::Normal
702            }
703        };
704
705        let command = Command::CreateStreamingJob {
706            info,
707            job_type,
708            cross_db_snapshot_backfill_info,
709        };
710
711        self.barrier_scheduler
712            .run_command(streaming_job.database_id(), command)
713            .await?;
714
715        tracing::debug!(?streaming_job, "first barrier collected for stream job");
716
717        Ok(streaming_job)
718    }
719
720    /// Send replace job command to barrier scheduler.
721    pub async fn replace_stream_job(
722        &self,
723        new_fragments: StreamJobFragmentsToCreate,
724        ReplaceStreamJobContext {
725            old_fragments,
726            replace_upstream,
727            upstream_fragment_downstreams,
728            tmp_id,
729            streaming_job,
730            drop_table_connector_ctx,
731            auto_refresh_schema_sinks,
732            streaming_job_model,
733            database_resource_group,
734        }: ReplaceStreamJobContext,
735    ) -> MetaResult<()> {
736        // Phase 1: Gather fragment-level split information.
737        // For replace source with existing downstream, splits will be aligned
738        // in Phase 2 inside the barrier worker using actor-level no-shuffle produced by render_actors.
739        // For replace source with no downstream (or non-source), discover splits fresh.
740        let split_plan = if streaming_job.is_source() {
741            match self
742                .source_manager
743                .discover_splits_for_replace_source(&new_fragments, &replace_upstream)
744                .await?
745            {
746                Some(discovered) => ReplaceJobSplitPlan::Discovered(discovered),
747                None => ReplaceJobSplitPlan::AlignFromPrevious,
748            }
749        } else {
750            let discovered = self.source_manager.discover_splits(&new_fragments).await?;
751            ReplaceJobSplitPlan::Discovered(discovered)
752        };
753        tracing::info!("replace_stream_job - split plan: {:?}", split_plan);
754
755        self.barrier_scheduler
756            .run_command(
757                streaming_job.database_id(),
758                Command::ReplaceStreamJob(ReplaceStreamJobPlan {
759                    old_fragments,
760                    new_fragments,
761                    database_resource_group,
762                    replace_upstream,
763                    upstream_fragment_downstreams,
764                    split_plan,
765                    streaming_job,
766                    streaming_job_model,
767                    tmp_id,
768                    to_drop_state_table_ids: {
769                        if let Some(drop_table_connector_ctx) = &drop_table_connector_ctx {
770                            vec![drop_table_connector_ctx.to_remove_state_table_id]
771                        } else {
772                            Vec::new()
773                        }
774                    },
775                    auto_refresh_schema_sinks,
776                }),
777            )
778            .await?;
779
780        Ok(())
781    }
782
783    /// Drop streaming jobs by barrier manager, and clean up all related resources. The error will
784    /// be ignored because the recovery process will take over it in cleaning part. Check
785    /// [`Command::DropStreamingJobs`] for details.
786    pub async fn drop_streaming_jobs(
787        &self,
788        database_id: DatabaseId,
789        streaming_job_ids: Vec<JobId>,
790        state_table_ids: Vec<TableId>,
791        dropped_sink_fragment_by_targets: HashMap<FragmentId, Vec<FragmentId>>,
792    ) {
793        if !streaming_job_ids.is_empty() || !state_table_ids.is_empty() {
794            let cleanup_streaming_job_ids = streaming_job_ids.clone();
795            let cleanup_state_table_ids = state_table_ids.clone();
796            let run_result = self
797                .barrier_scheduler
798                .run_command(
799                    database_id,
800                    Command::DropStreamingJobs {
801                        streaming_job_ids: streaming_job_ids.into_iter().collect(),
802                        unregistered_state_table_ids: state_table_ids.iter().copied().collect(),
803                        dropped_sink_fragment_by_targets,
804                    },
805                )
806                .await;
807            let result = match run_result {
808                Ok(()) => {
809                    cleanup_dropped_streaming_jobs(
810                        &self.refresh_manager,
811                        &self.hummock_manager,
812                        &self.metadata_manager,
813                        cleanup_streaming_job_ids,
814                        cleanup_state_table_ids,
815                        "drop_streaming_jobs",
816                    )
817                    .await
818                }
819                Err(err) => Err(err),
820            };
821            let _ = result.inspect_err(|err| {
822                tracing::error!(error = ?err.as_report(), "failed to run drop command");
823            });
824        }
825    }
826
827    /// Cancel streaming jobs and return the canceled table ids.
828    /// 1. Send cancel message to stream jobs (via `cancel_jobs`).
829    /// 2. Send cancel message to recovered stream jobs (via `barrier_scheduler`).
830    ///
831    /// Cleanup of their state is handled by the caller after the drop command is collected.
832    pub async fn cancel_streaming_jobs(&self, job_ids: Vec<JobId>) -> MetaResult<Vec<JobId>> {
833        if job_ids.is_empty() {
834            return Ok(vec![]);
835        }
836
837        let _reschedule_job_lock = self.reschedule_lock_read_guard().await;
838        let (receivers, background_job_ids) = self.creating_job_info.cancel_jobs(job_ids).await?;
839
840        let futures = receivers.into_iter().map(|(id, receiver)| async move {
841            if let Ok(cancelled) = receiver.await
842                && cancelled
843            {
844                tracing::info!("canceled streaming job {id}");
845                Ok(id)
846            } else {
847                Err(MetaError::from(anyhow::anyhow!(
848                    "failed to cancel streaming job {id}"
849                )))
850            }
851        });
852        let mut cancelled_ids = join_all(futures)
853            .await
854            .into_iter()
855            .collect::<MetaResult<Vec<_>>>()?;
856
857        // NOTE(kwannoel): For background_job_ids stream jobs that not tracked in streaming manager,
858        // we can directly cancel them by running the barrier command.
859        let futures = background_job_ids.into_iter().map(|id| async move {
860            let fragment = self.metadata_manager.get_job_fragments_by_id(id).await?;
861            if fragment.is_created() {
862                tracing::warn!(
863                    "streaming job {} is already created, ignore cancel request",
864                    id
865                );
866                return Ok(None);
867            }
868            if fragment.is_created() {
869                Err(MetaError::invalid_parameter(format!(
870                    "streaming job {} is already created",
871                    id
872                )))?;
873            }
874
875            let cancel_command = self
876                .metadata_manager
877                .catalog_controller
878                .build_cancel_command(&fragment)
879                .await?;
880            let cleanup_state_table_ids = fragment.all_table_ids().collect_vec();
881
882            let (_, database_id) = self
883                .metadata_manager
884                .catalog_controller
885                .try_abort_creating_streaming_job(id, true)
886                .await?;
887
888            if let Some(database_id) = database_id {
889                self.barrier_scheduler
890                    .run_command(database_id, cancel_command)
891                    .await?;
892                cleanup_dropped_streaming_jobs(
893                    &self.refresh_manager,
894                    &self.hummock_manager,
895                    &self.metadata_manager,
896                    [id],
897                    cleanup_state_table_ids,
898                    "cancel_streaming_job",
899                )
900                .await?;
901            }
902
903            tracing::info!(?id, "cancelled background streaming job");
904            Ok(Some(id))
905        });
906        let cancelled_recovered_ids = join_all(futures)
907            .await
908            .into_iter()
909            .collect::<MetaResult<Vec<_>>>()?;
910
911        cancelled_ids.extend(cancelled_recovered_ids.into_iter().flatten());
912        Ok(cancelled_ids)
913    }
914
915    pub(crate) async fn reschedule_streaming_job(
916        &self,
917        job_id: JobId,
918        policy: ReschedulePolicy,
919        deferred: bool,
920    ) -> MetaResult<()> {
921        let _reschedule_job_lock = self.reschedule_lock_write_guard().await;
922
923        let background_jobs = self
924            .metadata_manager
925            .list_background_creating_jobs()
926            .await?;
927
928        if !background_jobs.is_empty() {
929            let blocked_jobs = self
930                .metadata_manager
931                .collect_reschedule_blocked_jobs_for_creating_jobs(&background_jobs, !deferred)
932                .await?;
933
934            if blocked_jobs.contains(&job_id) {
935                bail!(
936                    "Cannot alter the job {} because it is blocked by creating unreschedulable backfill jobs",
937                    job_id,
938                );
939            }
940        }
941
942        let commands = self
943            .scale_controller
944            .reschedule_inplace(HashMap::from([(job_id, policy)]))
945            .await?;
946
947        if !deferred {
948            let _source_pause_guard = self.source_manager.pause_tick().await;
949
950            for (database_id, command) in commands {
951                self.barrier_scheduler
952                    .run_command(database_id, command)
953                    .await?;
954            }
955        }
956
957        Ok(())
958    }
959
960    pub(crate) async fn reschedule_streaming_job_backfill_parallelism(
961        &self,
962        job_id: JobId,
963        parallelism: Option<ParallelismPolicy>,
964        deferred: bool,
965    ) -> MetaResult<()> {
966        let _reschedule_job_lock = self.reschedule_lock_write_guard().await;
967
968        let background_jobs = self
969            .metadata_manager
970            .list_background_creating_jobs()
971            .await?;
972
973        if !background_jobs.is_empty() {
974            let unreschedulable = self
975                .metadata_manager
976                .collect_unreschedulable_backfill_jobs(&background_jobs, !deferred)
977                .await?;
978
979            if unreschedulable.contains(&job_id) {
980                bail!(
981                    "Cannot alter the job {} because it is a non-reschedulable background backfill job",
982                    job_id,
983                );
984            }
985        }
986
987        let commands = self
988            .scale_controller
989            .reschedule_backfill_parallelism_inplace(HashMap::from([(job_id, parallelism)]))
990            .await?;
991
992        if !deferred {
993            let _source_pause_guard = self.source_manager.pause_tick().await;
994
995            for (database_id, command) in commands {
996                self.barrier_scheduler
997                    .run_command(database_id, command)
998                    .await?;
999            }
1000        }
1001
1002        Ok(())
1003    }
1004
1005    /// This method is copied from `GlobalStreamManager::reschedule_streaming_job` and modified to handle reschedule CDC table backfill.
1006    pub(crate) async fn reschedule_cdc_table_backfill(
1007        &self,
1008        job_id: JobId,
1009        target: ReschedulePolicy,
1010    ) -> MetaResult<()> {
1011        let _reschedule_job_lock = self.reschedule_lock_write_guard().await;
1012
1013        let parallelism_policy = match target {
1014            ReschedulePolicy::Parallelism(policy)
1015                if matches!(policy.parallelism, StreamingParallelism::Fixed(_)) =>
1016            {
1017                policy
1018            }
1019            _ => bail_invalid_parameter!(
1020                "CDC backfill reschedule only supports fixed parallelism targets"
1021            ),
1022        };
1023
1024        let cdc_fragment_id = {
1025            let inner = self.metadata_manager.catalog_controller.inner.read().await;
1026            let fragments: Vec<(risingwave_meta_model::FragmentId, i32)> = FragmentModel::find()
1027                .select_only()
1028                .columns([
1029                    fragment::Column::FragmentId,
1030                    fragment::Column::FragmentTypeMask,
1031                ])
1032                .filter(fragment::Column::JobId.eq(job_id))
1033                .into_tuple()
1034                .all(&inner.db)
1035                .await?;
1036
1037            let cdc_fragments = fragments
1038                .into_iter()
1039                .filter_map(|(fragment_id, mask)| {
1040                    FragmentTypeMask::from(mask)
1041                        .contains(FragmentTypeFlag::StreamCdcScan)
1042                        .then_some(fragment_id)
1043                })
1044                .collect_vec();
1045
1046            match cdc_fragments.len() {
1047                0 => bail_invalid_parameter!("no StreamCdcScan fragments found for job {}", job_id),
1048                1 => cdc_fragments[0],
1049                _ => bail_invalid_parameter!(
1050                    "multiple StreamCdcScan fragments found for job {}; expected exactly one",
1051                    job_id
1052                ),
1053            }
1054        };
1055
1056        let fragment_policy = HashMap::from([(
1057            cdc_fragment_id,
1058            Some(parallelism_policy.parallelism.clone()),
1059        )]);
1060
1061        let commands = self
1062            .scale_controller
1063            .reschedule_fragment_inplace(fragment_policy)
1064            .await?;
1065
1066        let _source_pause_guard = self.source_manager.pause_tick().await;
1067
1068        for (database_id, command) in commands {
1069            self.barrier_scheduler
1070                .run_command(database_id, command)
1071                .await?;
1072        }
1073
1074        Ok(())
1075    }
1076
1077    pub(crate) async fn reschedule_fragments(
1078        &self,
1079        fragment_targets: HashMap<FragmentId, Option<StreamingParallelism>>,
1080    ) -> MetaResult<()> {
1081        if fragment_targets.is_empty() {
1082            return Ok(());
1083        }
1084
1085        let _reschedule_job_lock = self.reschedule_lock_write_guard().await;
1086
1087        let fragment_policy = fragment_targets
1088            .into_iter()
1089            .map(|(fragment_id, parallelism)| (fragment_id as _, parallelism))
1090            .collect();
1091
1092        let commands = self
1093            .scale_controller
1094            .reschedule_fragment_inplace(fragment_policy)
1095            .await?;
1096
1097        let _source_pause_guard = self.source_manager.pause_tick().await;
1098
1099        for (database_id, command) in commands {
1100            self.barrier_scheduler
1101                .run_command(database_id, command)
1102                .await?;
1103        }
1104
1105        Ok(())
1106    }
1107
1108    // Don't need to add actor, just send a command
1109    pub async fn create_subscription(
1110        self: &Arc<Self>,
1111        subscription: &Subscription,
1112    ) -> MetaResult<()> {
1113        let command = Command::CreateSubscription {
1114            subscription_id: subscription.id,
1115            upstream_mv_table_id: subscription.dependent_table_id,
1116            retention_second: subscription.retention_seconds,
1117        };
1118
1119        tracing::debug!("sending Command::CreateSubscription");
1120        self.barrier_scheduler
1121            .run_command(subscription.database_id, command)
1122            .await?;
1123        Ok(())
1124    }
1125
1126    // Don't need to add actor, just send a command
1127    pub async fn drop_subscription(
1128        self: &Arc<Self>,
1129        database_id: DatabaseId,
1130        subscription_id: SubscriptionId,
1131        table_id: TableId,
1132    ) {
1133        let command = Command::DropSubscription {
1134            subscription_id,
1135            upstream_mv_table_id: table_id,
1136        };
1137
1138        tracing::debug!("sending Command::DropSubscriptions");
1139        let _ = self
1140            .barrier_scheduler
1141            .run_command(database_id, command)
1142            .await
1143            .inspect_err(|err| {
1144                tracing::error!(error = ?err.as_report(), "failed to run drop command");
1145            });
1146    }
1147
1148    pub async fn alter_subscription_retention(
1149        self: &Arc<Self>,
1150        database_id: DatabaseId,
1151        subscription_id: SubscriptionId,
1152        table_id: TableId,
1153        retention_second: u64,
1154    ) -> MetaResult<()> {
1155        let command = Command::AlterSubscriptionRetention {
1156            subscription_id,
1157            upstream_mv_table_id: table_id,
1158            retention_second,
1159        };
1160
1161        tracing::debug!("sending Command::AlterSubscriptionRetention");
1162        self.barrier_scheduler
1163            .run_command(database_id, command)
1164            .await?;
1165        Ok(())
1166    }
1167}