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