1use 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
72pub type CreateStreamingJobResult =
75 Result<NotificationVersion, (MetaError, bool, Option<oneshot::Sender<bool>>)>;
76
77const FOREGROUND_DDL_EARLY_FAILURE_TIMEOUT: Duration = Duration::from_secs(30);
81
82pub(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 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 }
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 pub sink_original_target_columns: Vec<PbColumnCatalog>,
145 pub project_exprs: Vec<PbExprNode>,
146 pub new_sink_downstream: DownstreamFragmentRelation,
147}
148
149pub struct CreateStreamingJobContext {
153 pub upstream_fragment_downstreams: FragmentDownstreamRelation,
155
156 pub database_resource_group: String,
158
159 pub definition: String,
161
162 pub create_type: CreateType,
163
164 pub job_type: StreamingJobType,
165
166 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 pub streaming_job_model: streaming_job::Model,
188
189 pub replace_sink: Option<SinkId>,
191
192 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 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 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
316pub struct ReplaceStreamJobContext {
320 pub old_fragments: StreamJobFragments,
322
323 pub replace_upstream: FragmentReplaceUpstream,
325
326 pub upstream_fragment_downstreams: FragmentDownstreamRelation,
328
329 pub streaming_job: StreamingJob,
330
331 pub database_resource_group: String,
333
334 pub tmp_id: JobId,
335
336 pub drop_table_connector_ctx: Option<DropTableConnectorContext>,
338
339 pub auto_refresh_schema_sinks: Option<Vec<AutoRefreshSchemaSinkContext>>,
340
341 pub streaming_job_model: streaming_job::Model,
343}
344
345pub struct GlobalStreamManager {
347 pub env: MetaSrvEnv,
348
349 pub metadata_manager: MetadataManager,
350
351 pub barrier_scheduler: BarrierScheduler,
353
354 pub hummock_manager: HummockManagerRef,
355
356 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_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 #[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 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 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 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 #[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 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 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 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 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 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 #[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 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 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 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 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 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 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}