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, 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
69pub type CreateStreamingJobResult =
72 Result<NotificationVersion, (MetaError, bool, Option<oneshot::Sender<bool>>)>;
73
74const 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 }
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 pub sink_original_target_columns: Vec<PbColumnCatalog>,
117 pub project_exprs: Vec<PbExprNode>,
118 pub new_sink_downstream: DownstreamFragmentRelation,
119}
120
121pub struct CreateStreamingJobContext {
125 pub upstream_fragment_downstreams: FragmentDownstreamRelation,
127
128 pub database_resource_group: String,
130
131 pub definition: String,
133
134 pub create_type: CreateType,
135
136 pub job_type: StreamingJobType,
137
138 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 pub streaming_job_model: streaming_job::Model,
160
161 pub replace_sink: Option<SinkId>,
163
164 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 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 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
288pub struct ReplaceStreamJobContext {
292 pub old_fragments: StreamJobFragments,
294
295 pub replace_upstream: FragmentReplaceUpstream,
297
298 pub upstream_fragment_downstreams: FragmentDownstreamRelation,
300
301 pub streaming_job: StreamingJob,
302
303 pub database_resource_group: String,
305
306 pub tmp_id: JobId,
307
308 pub drop_table_connector_ctx: Option<DropTableConnectorContext>,
310
311 pub auto_refresh_schema_sinks: Option<Vec<AutoRefreshSchemaSinkContext>>,
312
313 pub streaming_job_model: streaming_job::Model,
315}
316
317pub struct GlobalStreamManager {
319 pub env: MetaSrvEnv,
320
321 pub metadata_manager: MetadataManager,
322
323 pub barrier_scheduler: BarrierScheduler,
325
326 pub hummock_manager: HummockManagerRef,
327
328 pub source_manager: SourceManagerRef,
330
331 pub refresh_manager: GlobalRefreshManagerRef,
332
333 pub iceberg_compaction_manager: IcebergCompactionManagerRef,
334
335 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 #[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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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}