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