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