1use std::cmp::Ordering;
16use std::collections::{HashMap, HashSet};
17use std::num::NonZeroUsize;
18use std::sync::Arc;
19use std::sync::atomic::AtomicU64;
20use std::time::Duration;
21
22use anyhow::{Context, anyhow};
23use await_tree::InstrumentAwait;
24use either::Either;
25use itertools::Itertools;
26use risingwave_common::catalog::{
27 AlterDatabaseParam, ColumnCatalog, ColumnId, Field, FragmentTypeFlag,
28};
29use risingwave_common::hash::VnodeCountCompat;
30use risingwave_common::id::{JobId, TableId};
31use risingwave_common::secret::{LocalSecretManager, SecretEncryption};
32use risingwave_common::system_param::adaptive_parallelism_strategy::parse_strategy;
33use risingwave_common::system_param::reader::SystemParamsRead;
34use risingwave_common::util::stream_graph_visitor::visit_stream_node_cont_mut;
35use risingwave_common::{bail, bail_not_implemented};
36use risingwave_connector::WithOptionsSecResolved;
37use risingwave_connector::connector_common::validate_connection;
38use risingwave_connector::sink::SinkParam;
39use risingwave_connector::sink::iceberg::IcebergSink;
40use risingwave_connector::source::cdc::CdcScanOptions;
41use risingwave_connector::source::{
42 ConnectorProperties, SourceEnumeratorContext, UPSTREAM_SOURCE_KEY,
43};
44use risingwave_meta_model::object::ObjectType;
45use risingwave_meta_model::{
46 ConnectionId, DatabaseId, DispatcherType, FragmentId, FunctionId, IndexId, JobStatus, ObjectId,
47 SchemaId, SecretId, SinkId, SourceId, StreamingParallelism, SubscriptionId, UserId, ViewId,
48 streaming_job,
49};
50use risingwave_pb::catalog::{
51 Comment, Connection, CreateType, Database, Function, PbTable, Schema, Secret, Source,
52 Subscription, Table, View,
53};
54use risingwave_pb::ddl_service::alter_owner_request::Object;
55use risingwave_pb::ddl_service::{
56 DdlProgress, TableJobType, WaitVersion, alter_name_request, alter_set_schema_request,
57 alter_swap_rename_request, streaming_job_resource_type,
58};
59use risingwave_pb::meta::table_fragments::fragment::FragmentDistributionType as PbFragmentDistributionType;
60use risingwave_pb::plan_common::PbColumnCatalog;
61use risingwave_pb::stream_plan::stream_node::NodeBody;
62use risingwave_pb::stream_plan::{
63 PbDispatchOutputMapping, PbStreamFragmentGraph, PbStreamNode, PbUpstreamSinkInfo,
64 StreamFragmentGraph as StreamFragmentGraphProto,
65};
66use risingwave_pb::telemetry::{PbTelemetryDatabaseObject, PbTelemetryEventStage};
67use strum::Display;
68use thiserror_ext::AsReport;
69use tokio::sync::Semaphore;
70use tokio::time::sleep;
71use tracing::Instrument;
72
73use crate::barrier::{BarrierManagerRef, Command};
74use crate::controller::catalog::{DropTableConnectorContext, ReleaseContext};
75use crate::controller::streaming_job::{FinishAutoRefreshSchemaSinkContext, SinkIntoTableContext};
76use crate::controller::utils::build_select_node_list;
77use crate::error::{MetaErrorInner, bail_invalid_parameter};
78use crate::manager::iceberg_compaction::IcebergCompactionManagerRef;
79use crate::manager::iceberg_pk_index_sink::IcebergPkIndexSinkManager;
80use crate::manager::sink_coordination::SinkCoordinatorManager;
81use crate::manager::{
82 IGNORED_NOTIFICATION_VERSION, LocalNotification, MetaSrvEnv, MetadataManager,
83 NotificationVersion, StreamingJob, StreamingJobType,
84};
85use crate::model::{
86 DownstreamFragmentRelation, FragmentDownstreamRelation, FragmentId as CatalogFragmentId,
87 StreamContext, StreamJobFragments, StreamJobFragmentsToCreate,
88};
89use crate::stream::cdc::{
90 parallel_cdc_table_backfill_fragment, try_init_parallel_cdc_table_snapshot_splits,
91};
92use crate::stream::{
93 ActorGraphBuildResult, ActorGraphBuilder, AutoRefreshSchemaSinkContext,
94 CompleteStreamFragmentGraph, CreateStreamingJobContext, CreateStreamingJobOption,
95 FragmentGraphDownstreamContext, FragmentGraphUpstreamContext, GlobalStreamManagerRef,
96 ParallelismPolicy, ReplaceStreamJobContext, ReschedulePolicy, SourceChange, SourceManagerRef,
97 StreamFragmentGraph, UpstreamSinkInfo, check_sink_fragments_support_refresh_schema,
98 create_source_worker, rewrite_refresh_schema_sink_fragment, state_match, validate_sink,
99};
100use crate::telemetry::report_event;
101use crate::{MetaError, MetaResult};
102
103#[derive(PartialEq)]
104pub enum DropMode {
105 Restrict,
106 Cascade,
107}
108
109impl DropMode {
110 pub fn from_request_setting(cascade: bool) -> DropMode {
111 if cascade {
112 DropMode::Cascade
113 } else {
114 DropMode::Restrict
115 }
116 }
117}
118
119#[derive(strum::AsRefStr)]
120pub enum StreamingJobId {
121 MaterializedView(TableId),
122 Sink(SinkId),
123 Table(Option<SourceId>, TableId),
124 Index(IndexId),
125}
126
127impl std::fmt::Display for StreamingJobId {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 write!(f, "{}", self.as_ref())?;
130 write!(f, "({})", self.id())
131 }
132}
133
134impl StreamingJobId {
135 fn id(&self) -> JobId {
136 match self {
137 StreamingJobId::MaterializedView(id) | StreamingJobId::Table(_, id) => id.as_job_id(),
138 StreamingJobId::Index(id) => id.as_job_id(),
139 StreamingJobId::Sink(id) => id.as_job_id(),
140 }
141 }
142}
143
144pub struct ReplaceStreamJobInfo {
147 pub streaming_job: StreamingJob,
148 pub fragment_graph: StreamFragmentGraphProto,
149}
150
151#[derive(Display)]
152pub enum DdlCommand {
153 CreateDatabase(Database),
154 DropDatabase(DatabaseId),
155 CreateSchema(Schema),
156 DropSchema(SchemaId, DropMode),
157 CreateNonSharedSource(Source),
158 DropSource(SourceId, DropMode),
159 ResetSource(SourceId),
160 CreateFunction(Function),
161 DropFunction(FunctionId, DropMode),
162 CreateView(View, HashSet<ObjectId>),
163 DropView(ViewId, DropMode),
164 CreateStreamingJob {
165 stream_job: StreamingJob,
166 fragment_graph: StreamFragmentGraphProto,
167 dependencies: HashSet<ObjectId>,
168 resource_type: streaming_job_resource_type::ResourceType,
169 if_not_exists: bool,
170 refresh_interval_sec: Option<u64>,
171 replace_sink: Option<SinkId>,
172 since_timestamp_epoch: Option<u64>,
173 },
174 DropStreamingJob {
175 job_id: StreamingJobId,
176 drop_mode: DropMode,
177 },
178 AlterName(alter_name_request::Object, String),
179 AlterSwapRename(alter_swap_rename_request::Object),
180 ReplaceStreamJob(ReplaceStreamJobInfo),
181 AlterNonSharedSource(Source),
182 AlterObjectOwner(Object, UserId),
183 AlterSetSchema(alter_set_schema_request::Object, SchemaId),
184 CreateConnection(Connection),
185 DropConnection(ConnectionId, DropMode),
186 CreateSecret(Secret),
187 AlterSecret(Secret),
188 DropSecret(SecretId, DropMode),
189 CommentOn(Comment),
190 CreateSubscription(Subscription),
191 DropSubscription(SubscriptionId, DropMode),
192 AlterSubscriptionRetention {
193 subscription_id: SubscriptionId,
194 retention_seconds: u64,
195 definition: String,
196 },
197 AlterDatabaseParam(DatabaseId, AlterDatabaseParam),
198 AlterDatabaseResourceGroup(DatabaseId, Option<String>, bool),
199 AlterStreamingJobConfig(JobId, HashMap<String, String>, Vec<String>),
200}
201
202impl DdlCommand {
203 fn object(&self) -> Either<String, ObjectId> {
205 use Either::*;
206 match self {
207 DdlCommand::CreateDatabase(database) => Left(database.name.clone()),
208 DdlCommand::DropDatabase(id) => Right(id.as_object_id()),
209 DdlCommand::CreateSchema(schema) => Left(schema.name.clone()),
210 DdlCommand::DropSchema(id, _) => Right(id.as_object_id()),
211 DdlCommand::CreateNonSharedSource(source) => Left(source.name.clone()),
212 DdlCommand::DropSource(id, _) => Right(id.as_object_id()),
213 DdlCommand::ResetSource(id) => Right(id.as_object_id()),
214 DdlCommand::CreateFunction(function) => Left(function.name.clone()),
215 DdlCommand::DropFunction(id, _) => Right(id.as_object_id()),
216 DdlCommand::CreateView(view, _) => Left(view.name.clone()),
217 DdlCommand::DropView(id, _) => Right(id.as_object_id()),
218 DdlCommand::CreateStreamingJob { stream_job, .. } => Left(stream_job.name()),
219 DdlCommand::DropStreamingJob { job_id, .. } => Right(job_id.id().as_object_id()),
220 DdlCommand::AlterName(object, _) => Left(format!("{object:?}")),
221 DdlCommand::AlterSwapRename(object) => Left(format!("{object:?}")),
222 DdlCommand::ReplaceStreamJob(info) => Left(info.streaming_job.name()),
223 DdlCommand::AlterNonSharedSource(source) => Left(source.name.clone()),
224 DdlCommand::AlterObjectOwner(object, _) => Left(format!("{object:?}")),
225 DdlCommand::AlterSetSchema(object, _) => Left(format!("{object:?}")),
226 DdlCommand::CreateConnection(connection) => Left(connection.name.clone()),
227 DdlCommand::DropConnection(id, _) => Right(id.as_object_id()),
228 DdlCommand::CreateSecret(secret) => Left(secret.name.clone()),
229 DdlCommand::AlterSecret(secret) => Left(secret.name.clone()),
230 DdlCommand::DropSecret(id, _) => Right(id.as_object_id()),
231 DdlCommand::CommentOn(comment) => Right(comment.table_id.into()),
232 DdlCommand::CreateSubscription(subscription) => Left(subscription.name.clone()),
233 DdlCommand::DropSubscription(id, _) => Right(id.as_object_id()),
234 DdlCommand::AlterSubscriptionRetention {
235 subscription_id, ..
236 } => Right(subscription_id.as_object_id()),
237 DdlCommand::AlterDatabaseParam(id, _) => Right(id.as_object_id()),
238 DdlCommand::AlterDatabaseResourceGroup(id, _, _) => Right(id.as_object_id()),
239 DdlCommand::AlterStreamingJobConfig(job_id, _, _) => Right(job_id.as_object_id()),
240 }
241 }
242
243 fn allow_in_recovery(&self) -> bool {
244 match self {
245 DdlCommand::DropDatabase(_)
246 | DdlCommand::DropSchema(_, _)
247 | DdlCommand::DropSource(_, _)
248 | DdlCommand::DropFunction(_, _)
249 | DdlCommand::DropView(_, _)
250 | DdlCommand::DropStreamingJob { .. }
251 | DdlCommand::DropConnection(_, _)
252 | DdlCommand::DropSecret(_, _)
253 | DdlCommand::DropSubscription(_, _)
254 | DdlCommand::AlterName(_, _)
255 | DdlCommand::AlterObjectOwner(_, _)
256 | DdlCommand::AlterSetSchema(_, _)
257 | DdlCommand::CreateDatabase(_)
258 | DdlCommand::CreateSchema(_)
259 | DdlCommand::CreateFunction(_)
260 | DdlCommand::CreateView(_, _)
261 | DdlCommand::CreateConnection(_)
262 | DdlCommand::CommentOn(_)
263 | DdlCommand::CreateSecret(_)
264 | DdlCommand::AlterSecret(_)
265 | DdlCommand::AlterSwapRename(_)
266 | DdlCommand::AlterDatabaseParam(_, _)
267 | DdlCommand::AlterDatabaseResourceGroup(_, _, _)
268 | DdlCommand::AlterStreamingJobConfig(_, _, _)
269 | DdlCommand::AlterSubscriptionRetention { .. } => true,
270 DdlCommand::CreateStreamingJob { .. }
271 | DdlCommand::CreateNonSharedSource(_)
272 | DdlCommand::ReplaceStreamJob(_)
273 | DdlCommand::AlterNonSharedSource(_)
274 | DdlCommand::ResetSource(_)
275 | DdlCommand::CreateSubscription(_) => false,
276 }
277 }
278}
279
280#[derive(Clone)]
281pub struct DdlController {
282 pub(crate) env: MetaSrvEnv,
283
284 pub(crate) metadata_manager: MetadataManager,
285 pub(crate) stream_manager: GlobalStreamManagerRef,
286 pub(crate) source_manager: SourceManagerRef,
287 barrier_manager: BarrierManagerRef,
288 sink_manager: SinkCoordinatorManager,
289 iceberg_compaction_manager: IcebergCompactionManagerRef,
290 iceberg_pk_index_sink_manager: IcebergPkIndexSinkManager,
291
292 pub(crate) creating_streaming_job_permits: Arc<CreatingStreamingJobPermit>,
294
295 seq: Arc<AtomicU64>,
297}
298
299#[derive(Clone)]
300pub struct CreatingStreamingJobPermit {
301 pub(crate) semaphore: Arc<Semaphore>,
302}
303
304impl CreatingStreamingJobPermit {
305 async fn new(env: &MetaSrvEnv) -> Self {
306 let mut permits = env
307 .system_params_reader()
308 .await
309 .max_concurrent_creating_streaming_jobs() as usize;
310 if permits == 0 {
311 permits = Semaphore::MAX_PERMITS;
313 }
314 let semaphore = Arc::new(Semaphore::new(permits));
315
316 let (local_notification_tx, mut local_notification_rx) =
317 tokio::sync::mpsc::unbounded_channel();
318 env.notification_manager()
319 .insert_local_sender(local_notification_tx);
320 let semaphore_clone = semaphore.clone();
321 tokio::spawn(async move {
322 while let Some(notification) = local_notification_rx.recv().await {
323 let LocalNotification::SystemParamsChange(p) = ¬ification else {
324 continue;
325 };
326 let mut new_permits = p.max_concurrent_creating_streaming_jobs() as usize;
327 if new_permits == 0 {
328 new_permits = Semaphore::MAX_PERMITS;
329 }
330 match permits.cmp(&new_permits) {
331 Ordering::Less => {
332 semaphore_clone.add_permits(new_permits - permits);
333 }
334 Ordering::Equal => continue,
335 Ordering::Greater => {
336 let to_release = permits - new_permits;
337 let reduced = semaphore_clone.forget_permits(to_release);
338 if reduced != to_release {
340 tracing::warn!(
341 "no enough permits to release, expected {}, but reduced {}",
342 to_release,
343 reduced
344 );
345 }
346 }
347 }
348 tracing::info!(
349 "max_concurrent_creating_streaming_jobs changed from {} to {}",
350 permits,
351 new_permits
352 );
353 permits = new_permits;
354 }
355 });
356
357 Self { semaphore }
358 }
359}
360
361impl DdlController {
362 fn validate_specified_parallelism(
363 specified_parallelism: Option<NonZeroUsize>,
364 specified_backfill_parallelism: Option<NonZeroUsize>,
365 max_parallelism: NonZeroUsize,
366 ) -> MetaResult<()> {
367 if let Some(parallelism) = specified_parallelism
368 && parallelism > max_parallelism
369 {
370 bail_invalid_parameter!(
371 "specified parallelism {} should not exceed max parallelism {}",
372 parallelism,
373 max_parallelism,
374 );
375 }
376 if let Some(backfill_parallelism) = specified_backfill_parallelism
377 && backfill_parallelism > max_parallelism
378 {
379 bail_invalid_parameter!(
380 "specified backfill parallelism {} should not exceed max parallelism {}",
381 backfill_parallelism,
382 max_parallelism,
383 );
384 }
385 Ok(())
386 }
387
388 fn validate_serverless_backfill_enabled(
389 &self,
390 resource_type: &streaming_job_resource_type::ResourceType,
391 ) -> MetaResult<()> {
392 if matches!(
393 resource_type,
394 streaming_job_resource_type::ResourceType::ServerlessBackfill(true)
395 ) && self.env.opts.serverless_backfill_controller_addr.is_empty()
396 {
397 bail_invalid_parameter!(
398 "Serverless Backfill is disabled. Use RisingWave cloud at https://cloud.risingwave.com/auth/signup to try this feature"
399 );
400 }
401
402 Ok(())
403 }
404
405 pub async fn new(
406 env: MetaSrvEnv,
407 metadata_manager: MetadataManager,
408 stream_manager: GlobalStreamManagerRef,
409 source_manager: SourceManagerRef,
410 barrier_manager: BarrierManagerRef,
411 sink_manager: SinkCoordinatorManager,
412 iceberg_compaction_manager: IcebergCompactionManagerRef,
413 iceberg_pk_index_sink_manager: IcebergPkIndexSinkManager,
414 ) -> Self {
415 let creating_streaming_job_permits = Arc::new(CreatingStreamingJobPermit::new(&env).await);
416 Self {
417 env,
418 metadata_manager,
419 stream_manager,
420 source_manager,
421 barrier_manager,
422 sink_manager,
423 iceberg_compaction_manager,
424 iceberg_pk_index_sink_manager,
425 creating_streaming_job_permits,
426 seq: Arc::new(AtomicU64::new(0)),
427 }
428 }
429
430 pub fn next_seq(&self) -> u64 {
432 self.seq.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
434 }
435
436 pub async fn run_command(&self, command: DdlCommand) -> MetaResult<Option<WaitVersion>> {
443 if !command.allow_in_recovery() {
444 self.barrier_manager.check_status_running()?;
445 }
446
447 let await_tree_key = format!("DDL Command {}", self.next_seq());
448 let await_tree_span = await_tree::span!("{command}({})", command.object());
449
450 let ctrl = self.clone();
451 let fut = Box::pin(async move {
452 match command {
453 DdlCommand::CreateDatabase(database) => ctrl.create_database(database).await,
454 DdlCommand::DropDatabase(database_id) => ctrl.drop_database(database_id).await,
455 DdlCommand::CreateSchema(schema) => ctrl.create_schema(schema).await,
456 DdlCommand::DropSchema(schema_id, drop_mode) => {
457 ctrl.drop_schema(schema_id, drop_mode).await
458 }
459 DdlCommand::CreateNonSharedSource(source) => {
460 ctrl.create_non_shared_source(source).await
461 }
462 DdlCommand::DropSource(source_id, drop_mode) => {
463 ctrl.drop_source(source_id, drop_mode).await
464 }
465 DdlCommand::ResetSource(source_id) => ctrl.reset_source(source_id).await,
466 DdlCommand::CreateFunction(function) => ctrl.create_function(function).await,
467 DdlCommand::DropFunction(function_id, drop_mode) => {
468 ctrl.drop_function(function_id, drop_mode).await
469 }
470 DdlCommand::CreateView(view, dependencies) => {
471 ctrl.create_view(view, dependencies).await
472 }
473 DdlCommand::DropView(view_id, drop_mode) => {
474 ctrl.drop_view(view_id, drop_mode).await
475 }
476 DdlCommand::CreateStreamingJob {
477 stream_job,
478 fragment_graph,
479 dependencies,
480 resource_type,
481 if_not_exists,
482 refresh_interval_sec,
483 replace_sink,
484 since_timestamp_epoch,
485 } => {
486 ctrl.create_streaming_job(
487 stream_job,
488 fragment_graph,
489 dependencies,
490 resource_type,
491 if_not_exists,
492 refresh_interval_sec,
493 replace_sink,
494 since_timestamp_epoch,
495 )
496 .await
497 }
498 DdlCommand::DropStreamingJob { job_id, drop_mode } => {
499 ctrl.drop_streaming_job(job_id, drop_mode).await
500 }
501 DdlCommand::ReplaceStreamJob(ReplaceStreamJobInfo {
502 streaming_job,
503 fragment_graph,
504 }) => ctrl.replace_job(streaming_job, fragment_graph).await,
505 DdlCommand::AlterName(relation, name) => ctrl.alter_name(relation, &name).await,
506 DdlCommand::AlterObjectOwner(object, owner_id) => {
507 ctrl.alter_owner(object, owner_id).await
508 }
509 DdlCommand::AlterSetSchema(object, new_schema_id) => {
510 ctrl.alter_set_schema(object, new_schema_id).await
511 }
512 DdlCommand::CreateConnection(connection) => {
513 ctrl.create_connection(connection).await
514 }
515 DdlCommand::DropConnection(connection_id, drop_mode) => {
516 ctrl.drop_connection(connection_id, drop_mode).await
517 }
518 DdlCommand::CreateSecret(secret) => ctrl.create_secret(secret).await,
519 DdlCommand::DropSecret(secret_id, drop_mode) => {
520 ctrl.drop_secret(secret_id, drop_mode).await
521 }
522 DdlCommand::AlterSecret(secret) => ctrl.alter_secret(secret).await,
523 DdlCommand::AlterNonSharedSource(source) => {
524 ctrl.alter_non_shared_source(source).await
525 }
526 DdlCommand::CommentOn(comment) => ctrl.comment_on(comment).await,
527 DdlCommand::CreateSubscription(subscription) => {
528 ctrl.create_subscription(subscription).await
529 }
530 DdlCommand::DropSubscription(subscription_id, drop_mode) => {
531 ctrl.drop_subscription(subscription_id, drop_mode).await
532 }
533 DdlCommand::AlterSubscriptionRetention {
534 subscription_id,
535 retention_seconds,
536 definition,
537 } => {
538 ctrl.alter_subscription_retention(
539 subscription_id,
540 retention_seconds,
541 definition,
542 )
543 .await
544 }
545 DdlCommand::AlterSwapRename(objects) => ctrl.alter_swap_rename(objects).await,
546 DdlCommand::AlterDatabaseParam(database_id, param) => {
547 ctrl.alter_database_param(database_id, param).await
548 }
549 DdlCommand::AlterDatabaseResourceGroup(database_id, resource_group, deferred) => {
550 ctrl.alter_database_resource_group(database_id, resource_group, deferred)
551 .await
552 }
553 DdlCommand::AlterStreamingJobConfig(job_id, entries_to_add, keys_to_remove) => {
554 ctrl.alter_streaming_job_config(job_id, entries_to_add, keys_to_remove)
555 .await
556 }
557 }
558 })
559 .in_current_span();
560 let fut = (self.env.await_tree_reg())
561 .register(await_tree_key, await_tree_span)
562 .instrument(Box::pin(fut));
563 let notification_version = tokio::spawn(fut).await.map_err(|e| anyhow!(e))??;
564 Ok(Some(WaitVersion {
565 catalog_version: notification_version,
566 hummock_version_id: self.barrier_manager.get_hummock_version_id().await,
567 }))
568 }
569
570 pub async fn get_ddl_progress(&self) -> MetaResult<Vec<DdlProgress>> {
571 self.barrier_manager.get_ddl_progress().await
572 }
573
574 async fn create_database(&self, database: Database) -> MetaResult<NotificationVersion> {
575 let (version, updated_db) = self
576 .metadata_manager
577 .catalog_controller
578 .create_database(database)
579 .await?;
580 self.barrier_manager
582 .update_database_barrier(
583 updated_db.database_id,
584 updated_db.barrier_interval_ms.map(|v| v as u32),
585 updated_db.checkpoint_frequency.map(|v| v as u64),
586 )
587 .await?;
588 Ok(version)
589 }
590
591 #[tracing::instrument(skip(self), level = "debug")]
592 pub async fn reschedule_streaming_job(
593 &self,
594 job_id: JobId,
595 target: ReschedulePolicy,
596 mut deferred: bool,
597 ) -> MetaResult<()> {
598 tracing::info!("altering parallelism for job {}", job_id);
599 if self.barrier_manager.check_status_running().is_err() {
600 tracing::info!(
601 "alter parallelism is set to deferred mode because the system is in recovery state"
602 );
603 deferred = true;
604 }
605
606 self.stream_manager
607 .reschedule_streaming_job(job_id, target, deferred)
608 .await
609 }
610
611 pub async fn reschedule_streaming_job_backfill_parallelism(
612 &self,
613 job_id: JobId,
614 parallelism: Option<ParallelismPolicy>,
615 mut deferred: bool,
616 ) -> MetaResult<()> {
617 tracing::info!("altering backfill parallelism for job {}", job_id);
618 if self.barrier_manager.check_status_running().is_err() {
619 tracing::info!(
620 "alter backfill parallelism is set to deferred mode because the system is in recovery state"
621 );
622 deferred = true;
623 }
624
625 self.stream_manager
626 .reschedule_streaming_job_backfill_parallelism(job_id, parallelism, deferred)
627 .await
628 }
629
630 pub async fn reschedule_cdc_table_backfill(
631 &self,
632 job_id: JobId,
633 target: ReschedulePolicy,
634 ) -> MetaResult<()> {
635 tracing::info!("alter CDC table backfill parallelism");
636 if self.barrier_manager.check_status_running().is_err() {
637 return Err(anyhow::anyhow!("CDC table backfill reschedule is unavailable because the system is in recovery state").into());
638 }
639 self.stream_manager
640 .reschedule_cdc_table_backfill(job_id, target)
641 .await
642 }
643
644 pub async fn reschedule_fragments(
645 &self,
646 fragment_targets: HashMap<FragmentId, Option<StreamingParallelism>>,
647 ) -> MetaResult<()> {
648 tracing::info!(
649 "altering parallelism for fragments {:?}",
650 fragment_targets.keys()
651 );
652 let fragment_targets = fragment_targets
653 .into_iter()
654 .map(|(fragment_id, parallelism)| (fragment_id as CatalogFragmentId, parallelism))
655 .collect();
656
657 self.stream_manager
658 .reschedule_fragments(fragment_targets)
659 .await
660 }
661
662 async fn drop_database(&self, database_id: DatabaseId) -> MetaResult<NotificationVersion> {
663 self.drop_object(ObjectType::Database, database_id, DropMode::Cascade)
664 .await
665 }
666
667 async fn create_schema(&self, schema: Schema) -> MetaResult<NotificationVersion> {
668 self.metadata_manager
669 .catalog_controller
670 .create_schema(schema)
671 .await
672 }
673
674 async fn drop_schema(
675 &self,
676 schema_id: SchemaId,
677 drop_mode: DropMode,
678 ) -> MetaResult<NotificationVersion> {
679 self.drop_object(ObjectType::Schema, schema_id, drop_mode)
680 .await
681 }
682
683 async fn create_non_shared_source(&self, source: Source) -> MetaResult<NotificationVersion> {
685 let handle = create_source_worker(
686 &source,
687 self.source_manager.metrics.clone(),
688 self.env.await_tree_reg().clone(),
689 )
690 .await
691 .context("failed to create source worker")?;
692
693 let (source_id, version) = self
694 .metadata_manager
695 .catalog_controller
696 .create_source(source)
697 .await?;
698 self.source_manager
699 .register_source_with_handle(source_id, handle)
700 .await;
701 Ok(version)
702 }
703
704 async fn drop_source(
705 &self,
706 source_id: SourceId,
707 drop_mode: DropMode,
708 ) -> MetaResult<NotificationVersion> {
709 self.drop_object(ObjectType::Source, source_id, drop_mode)
710 .await
711 }
712
713 async fn reset_source(&self, source_id: SourceId) -> MetaResult<NotificationVersion> {
714 tracing::info!(source_id = %source_id, "resetting CDC source offset to latest");
715
716 let database_id = self
718 .metadata_manager
719 .catalog_controller
720 .get_object_database_id(source_id)
721 .await?;
722
723 self.stream_manager
724 .barrier_scheduler
725 .run_command(database_id, Command::ResetSource { source_id })
726 .await?;
727
728 let version = self
730 .metadata_manager
731 .catalog_controller
732 .notify_frontend_trivial()
733 .await;
734 Ok(version)
735 }
736
737 async fn alter_non_shared_source(&self, source: Source) -> MetaResult<NotificationVersion> {
740 self.metadata_manager
741 .catalog_controller
742 .alter_non_shared_source(source)
743 .await
744 }
745
746 async fn create_function(&self, function: Function) -> MetaResult<NotificationVersion> {
747 self.metadata_manager
748 .catalog_controller
749 .create_function(function)
750 .await
751 }
752
753 async fn drop_function(
754 &self,
755 function_id: FunctionId,
756 drop_mode: DropMode,
757 ) -> MetaResult<NotificationVersion> {
758 self.drop_object(ObjectType::Function, function_id, drop_mode)
759 .await
760 }
761
762 async fn create_view(
763 &self,
764 view: View,
765 dependencies: HashSet<ObjectId>,
766 ) -> MetaResult<NotificationVersion> {
767 self.metadata_manager
768 .catalog_controller
769 .create_view(view, dependencies)
770 .await
771 }
772
773 async fn drop_view(
774 &self,
775 view_id: ViewId,
776 drop_mode: DropMode,
777 ) -> MetaResult<NotificationVersion> {
778 self.drop_object(ObjectType::View, view_id, drop_mode).await
779 }
780
781 async fn create_connection(&self, connection: Connection) -> MetaResult<NotificationVersion> {
782 validate_connection(&connection).await?;
783 self.metadata_manager
784 .catalog_controller
785 .create_connection(connection)
786 .await
787 }
788
789 async fn drop_connection(
790 &self,
791 connection_id: ConnectionId,
792 drop_mode: DropMode,
793 ) -> MetaResult<NotificationVersion> {
794 self.drop_object(ObjectType::Connection, connection_id, drop_mode)
795 .await
796 }
797
798 async fn alter_database_param(
799 &self,
800 database_id: DatabaseId,
801 param: AlterDatabaseParam,
802 ) -> MetaResult<NotificationVersion> {
803 let (version, updated_db) = self
804 .metadata_manager
805 .catalog_controller
806 .alter_database_param(database_id, param)
807 .await?;
808 self.barrier_manager
810 .update_database_barrier(
811 database_id,
812 updated_db.barrier_interval_ms.map(|v| v as u32),
813 updated_db.checkpoint_frequency.map(|v| v as u64),
814 )
815 .await?;
816 Ok(version)
817 }
818
819 async fn alter_database_resource_group(
820 &self,
821 database_id: DatabaseId,
822 resource_group: Option<String>,
823 _deferred: bool,
824 ) -> MetaResult<NotificationVersion> {
825 let version = self
826 .metadata_manager
827 .catalog_controller
828 .alter_database_resource_group(database_id, resource_group)
829 .await?;
830
831 Ok(version)
832 }
833
834 fn get_encrypted_payload(&self, secret: &Secret) -> MetaResult<Vec<u8>> {
837 let secret_store_private_key = self
838 .env
839 .opts
840 .secret_store_private_key
841 .clone()
842 .ok_or_else(|| anyhow!("secret_store_private_key is not configured"))?;
843
844 let encrypted_payload = SecretEncryption::encrypt(
845 secret_store_private_key.as_slice(),
846 secret.get_value().as_slice(),
847 )
848 .context(format!("failed to encrypt secret {}", secret.name))?;
849 Ok(encrypted_payload
850 .serialize()
851 .context(format!("failed to serialize secret {}", secret.name))?)
852 }
853
854 async fn create_secret(&self, mut secret: Secret) -> MetaResult<NotificationVersion> {
855 let secret_plain_payload = secret.value.clone();
858 let encrypted_payload = self.get_encrypted_payload(&secret)?;
859 secret.value = encrypted_payload;
860
861 self.metadata_manager
862 .catalog_controller
863 .create_secret(secret, secret_plain_payload)
864 .await
865 }
866
867 async fn drop_secret(
868 &self,
869 secret_id: SecretId,
870 drop_mode: DropMode,
871 ) -> MetaResult<NotificationVersion> {
872 self.drop_object(ObjectType::Secret, secret_id, drop_mode)
873 .await
874 }
875
876 async fn alter_secret(&self, mut secret: Secret) -> MetaResult<NotificationVersion> {
877 let secret_plain_payload = secret.value.clone();
878 let encrypted_payload = self.get_encrypted_payload(&secret)?;
879 secret.value = encrypted_payload;
880 self.metadata_manager
881 .catalog_controller
882 .alter_secret(secret, secret_plain_payload)
883 .await
884 }
885
886 async fn create_subscription(
887 &self,
888 mut subscription: Subscription,
889 ) -> MetaResult<NotificationVersion> {
890 tracing::debug!("create subscription");
891 let _permit = self
892 .creating_streaming_job_permits
893 .semaphore
894 .acquire()
895 .await
896 .unwrap();
897 let _reschedule_job_lock = self.stream_manager.reschedule_lock_read_guard().await;
898 self.metadata_manager
899 .catalog_controller
900 .create_subscription_catalog(&mut subscription)
901 .await?;
902 if let Err(err) = self.stream_manager.create_subscription(&subscription).await {
903 tracing::debug!(error = %err.as_report(), "failed to create subscription");
904 let _ = self
905 .metadata_manager
906 .catalog_controller
907 .try_abort_creating_subscription(subscription.id)
908 .await
909 .inspect_err(|e| {
910 tracing::error!(
911 error = %e.as_report(),
912 "failed to abort create subscription after failure"
913 );
914 });
915 return Err(err);
916 }
917
918 let version = self
919 .metadata_manager
920 .catalog_controller
921 .notify_create_subscription(subscription.id)
922 .await?;
923 tracing::debug!("finish create subscription");
924 Ok(version)
925 }
926
927 async fn drop_subscription(
928 &self,
929 subscription_id: SubscriptionId,
930 drop_mode: DropMode,
931 ) -> MetaResult<NotificationVersion> {
932 tracing::debug!("preparing drop subscription");
933 let _reschedule_job_lock = self.stream_manager.reschedule_lock_read_guard().await;
934 let subscription = self
935 .metadata_manager
936 .catalog_controller
937 .get_subscription_by_id(subscription_id)
938 .await?;
939 let table_id = subscription.dependent_table_id;
940 let database_id = subscription.database_id;
941 let (_, version) = self
942 .metadata_manager
943 .catalog_controller
944 .drop_object(ObjectType::Subscription, subscription_id, drop_mode)
945 .await?;
946 self.stream_manager
947 .drop_subscription(database_id, subscription_id, table_id)
948 .await;
949 tracing::debug!("finish drop subscription");
950 Ok(version)
951 }
952
953 async fn alter_subscription_retention(
954 &self,
955 subscription_id: SubscriptionId,
956 retention_seconds: u64,
957 definition: String,
958 ) -> MetaResult<NotificationVersion> {
959 tracing::debug!("alter subscription retention");
960 let _reschedule_job_lock = self.stream_manager.reschedule_lock_read_guard().await;
961 let (version, subscription) = self
962 .metadata_manager
963 .catalog_controller
964 .alter_subscription_retention(subscription_id, retention_seconds, definition)
965 .await?;
966 self.stream_manager
967 .alter_subscription_retention(
968 subscription.database_id,
969 subscription.id,
970 subscription.dependent_table_id,
971 subscription.retention_seconds,
972 )
973 .await?;
974 tracing::debug!("finish alter subscription retention");
975 Ok(version)
976 }
977
978 #[await_tree::instrument]
980 pub(crate) async fn validate_cdc_table(
981 &self,
982 table: &Table,
983 table_fragments: &StreamJobFragments,
984 ) -> MetaResult<()> {
985 let stream_scan_fragment =
986 Itertools::exactly_one(table_fragments.fragments.values().filter(|f| {
987 f.fragment_type_mask.contains(FragmentTypeFlag::StreamScan)
988 || f.fragment_type_mask
989 .contains(FragmentTypeFlag::StreamCdcScan)
990 }))
991 .ok()
992 .with_context(|| {
993 format!(
994 "expect exactly one stream scan fragment, got: {:?}",
995 table_fragments.fragments
996 )
997 })?;
998 fn assert_parallelism(
999 distribution_type: PbFragmentDistributionType,
1000 node_body: &Option<NodeBody>,
1001 ) {
1002 if let Some(NodeBody::StreamCdcScan(node)) = node_body {
1003 if let Some(o) = node.options
1004 && CdcScanOptions::from_proto(&o).is_parallelized_backfill()
1005 {
1006 } else {
1008 assert_eq!(
1009 distribution_type,
1010 PbFragmentDistributionType::Single,
1011 "Non-parallelized CDC scan fragment should have Single distribution"
1012 );
1013 }
1014 }
1015 }
1016 let mut found_cdc_scan = false;
1017 match &stream_scan_fragment.nodes.node_body {
1018 Some(NodeBody::StreamCdcScan(_)) => {
1019 assert_parallelism(
1020 stream_scan_fragment.distribution_type,
1021 &stream_scan_fragment.nodes.node_body,
1022 );
1023 if self
1024 .validate_cdc_table_inner(&stream_scan_fragment.nodes.node_body, table.id)
1025 .await?
1026 {
1027 found_cdc_scan = true;
1028 }
1029 }
1030 Some(NodeBody::Project(_)) => {
1032 for input in &stream_scan_fragment.nodes.input {
1033 assert_parallelism(stream_scan_fragment.distribution_type, &input.node_body);
1034 if self
1035 .validate_cdc_table_inner(&input.node_body, table.id)
1036 .await?
1037 {
1038 found_cdc_scan = true;
1039 }
1040 }
1041 }
1042 _ => {
1043 bail!("Unexpected node body for stream cdc scan");
1044 }
1045 };
1046 if !found_cdc_scan {
1047 bail!("No stream cdc scan node found in stream scan fragment");
1048 }
1049 Ok(())
1050 }
1051
1052 async fn validate_cdc_table_inner(
1053 &self,
1054 node_body: &Option<NodeBody>,
1055 table_id: TableId,
1056 ) -> MetaResult<bool> {
1057 if let Some(NodeBody::StreamCdcScan(stream_cdc_scan)) = node_body
1058 && let Some(ref cdc_table_desc) = stream_cdc_scan.cdc_table_desc
1059 {
1060 let options_with_secret = WithOptionsSecResolved::new(
1061 cdc_table_desc.connect_properties.clone(),
1062 cdc_table_desc.secret_refs.clone(),
1063 );
1064
1065 let mut props = ConnectorProperties::extract(options_with_secret, true)?;
1066 props.init_from_pb_cdc_table_desc(cdc_table_desc);
1067
1068 let _enumerator = props
1070 .create_split_enumerator(SourceEnumeratorContext::dummy().into())
1071 .await?;
1072
1073 tracing::debug!(?table_id, "validate cdc table success");
1074 Ok(true)
1075 } else {
1076 Ok(false)
1077 }
1078 }
1079
1080 pub async fn validate_table_for_sink(&self, table_id: TableId) -> MetaResult<()> {
1081 let migrated = self
1082 .metadata_manager
1083 .catalog_controller
1084 .has_table_been_migrated(table_id)
1085 .await?;
1086 if !migrated {
1087 Err(anyhow::anyhow!("Creating sink into table is not allowed for unmigrated table {}. Please migrate it first.", table_id).into())
1088 } else {
1089 Ok(())
1090 }
1091 }
1092
1093 #[await_tree::instrument(boxed, "create_streaming_job({streaming_job})")]
1096 pub async fn create_streaming_job(
1097 &self,
1098 mut streaming_job: StreamingJob,
1099 fragment_graph: StreamFragmentGraphProto,
1100 dependencies: HashSet<ObjectId>,
1101 resource_type: streaming_job_resource_type::ResourceType,
1102 if_not_exists: bool,
1103 refresh_interval_sec: Option<u64>,
1104 replace_sink: Option<SinkId>,
1105 since_timestamp_epoch: Option<u64>,
1106 ) -> MetaResult<NotificationVersion> {
1107 let replace_sink_info = if let Some(old_sink_id) = replace_sink {
1108 let StreamingJob::Sink(sink) = &streaming_job else {
1109 bail!("replace sink requires a sink job")
1110 };
1111 if sink.target_table.is_some() {
1112 bail_not_implemented!("replace sink into table")
1113 }
1114
1115 Some(old_sink_id)
1116 } else {
1117 if let StreamingJob::Sink(sink) = &streaming_job
1118 && let Some(target_table) = sink.target_table
1119 {
1120 self.validate_table_for_sink(target_table).await?;
1121 }
1122 None
1123 };
1124 self.validate_serverless_backfill_enabled(&resource_type)?;
1125 let ctx = StreamContext::from_protobuf(fragment_graph.get_ctx().unwrap());
1126 let adaptive_parallelism_strategy =
1127 (!fragment_graph.adaptive_parallelism_strategy.is_empty()).then(|| {
1128 parse_strategy(&fragment_graph.adaptive_parallelism_strategy)
1129 .expect("adaptive parallelism strategy should be validated in frontend")
1130 });
1131 let backfill_adaptive_parallelism_strategy = (!fragment_graph
1132 .backfill_adaptive_parallelism_strategy
1133 .is_empty())
1134 .then(|| {
1135 parse_strategy(&fragment_graph.backfill_adaptive_parallelism_strategy)
1136 .expect("backfill adaptive parallelism strategy should be validated in frontend")
1137 });
1138
1139 let streaming_job_model = match self
1140 .metadata_manager
1141 .catalog_controller
1142 .create_job_catalog(
1143 &mut streaming_job,
1144 &ctx,
1145 &fragment_graph.parallelism,
1146 fragment_graph.max_parallelism as _,
1147 dependencies,
1148 resource_type.clone(),
1149 &fragment_graph.backfill_parallelism,
1150 adaptive_parallelism_strategy,
1151 backfill_adaptive_parallelism_strategy,
1152 replace_sink_info.as_ref(),
1153 refresh_interval_sec,
1154 )
1155 .await
1156 {
1157 Ok(model) => model,
1158 Err(meta_err) => {
1159 if !if_not_exists {
1160 return Err(meta_err);
1161 }
1162 return if let MetaErrorInner::Duplicated(_, _, Some(job_id)) = meta_err.inner() {
1163 if streaming_job.create_type() == CreateType::Foreground {
1164 let database_id = streaming_job.database_id();
1165 self.metadata_manager
1166 .wait_streaming_job_finished(database_id, *job_id)
1167 .await
1168 } else {
1169 Ok(IGNORED_NOTIFICATION_VERSION)
1170 }
1171 } else {
1172 Err(meta_err)
1173 };
1174 }
1175 };
1176 let job_id = streaming_job.id();
1177 if let Some(old_sink_id) = replace_sink_info.as_ref() {
1178 tracing::debug!(
1179 old_sink_id = %old_sink_id,
1180 new_sink_id = %job_id,
1181 definition = streaming_job.definition(),
1182 create_type = streaming_job.create_type().as_str_name(),
1183 "starting replacement sink",
1184 );
1185 } else {
1186 tracing::debug!(
1187 id = %job_id,
1188 definition = streaming_job.definition(),
1189 create_type = streaming_job.create_type().as_str_name(),
1190 job_type = ?streaming_job.job_type(),
1191 "starting streaming job",
1192 );
1193 }
1194 let permit = self
1196 .creating_streaming_job_permits
1197 .semaphore
1198 .clone()
1199 .acquire_owned()
1200 .instrument_await("acquire_creating_streaming_job_permit")
1201 .await
1202 .unwrap();
1203 let _reschedule_job_lock = self.stream_manager.reschedule_lock_read_guard().await;
1204
1205 let name = streaming_job.name();
1206 let definition = streaming_job.definition();
1207 let source_id = match &streaming_job {
1208 StreamingJob::Table(Some(src), _, _) | StreamingJob::Source(src) => Some(src.id),
1209 _ => None,
1210 };
1211 let create_result = match self
1214 .generate_streaming_job(
1215 ctx,
1216 streaming_job,
1217 fragment_graph,
1218 resource_type.clone(),
1219 streaming_job_model,
1220 replace_sink_info,
1221 since_timestamp_epoch,
1222 )
1223 .await
1224 {
1225 Ok((stream_job_fragments, ctx)) => self
1226 .stream_manager
1227 .create_streaming_job(stream_job_fragments, ctx, permit)
1228 .await
1229 .map_err(|err| (err, true)),
1230 Err(err) => Err((err, false)),
1231 };
1232
1233 match create_result {
1234 Ok(version) => Ok(version),
1235 Err((err, is_cancelled)) => {
1236 tracing::error!(id = %job_id, error = %err.as_report(), "failed to create streaming job");
1237 let event = risingwave_pb::meta::event_log::EventCreateStreamJobFail {
1238 id: job_id,
1239 name,
1240 definition,
1241 error: err.as_report().to_string(),
1242 };
1243 self.env.event_log_manager_ref().add_event_logs(vec![
1244 risingwave_pb::meta::event_log::Event::CreateStreamJobFail(event),
1245 ]);
1246 let abort_result = self
1247 .metadata_manager
1248 .catalog_controller
1249 .try_abort_creating_streaming_job(job_id, is_cancelled)
1250 .await?;
1251 self.iceberg_compaction_manager
1252 .clear_maintenance_for_aborted_job(&abort_result);
1253 if abort_result.aborted {
1254 tracing::warn!(id = %job_id, is_cancelled, "aborted streaming job");
1255 if let Some(source_id) = source_id {
1257 self.source_manager
1258 .apply_source_change(SourceChange::DropSource {
1259 dropped_source_ids: vec![source_id],
1260 })
1261 .await;
1262 }
1263 }
1264 Err(err)
1265 }
1266 }
1267 }
1268
1269 #[await_tree::instrument(boxed)]
1270 async fn generate_streaming_job(
1271 &self,
1272 ctx: StreamContext,
1273 mut streaming_job: StreamingJob,
1274 fragment_graph: StreamFragmentGraphProto,
1275 resource_type: streaming_job_resource_type::ResourceType,
1276 streaming_job_model: streaming_job::Model,
1277 replace_sink: Option<SinkId>,
1278 since_timestamp_epoch: Option<u64>,
1279 ) -> MetaResult<(StreamJobFragmentsToCreate, CreateStreamingJobContext)> {
1280 let mut fragment_graph =
1281 StreamFragmentGraph::new(&self.env, fragment_graph, &streaming_job)?;
1282 streaming_job.set_info_from_graph(&fragment_graph);
1283
1284 let incomplete_internal_tables = fragment_graph
1286 .incomplete_internal_tables()
1287 .into_values()
1288 .collect_vec();
1289 let table_id_map = self
1290 .metadata_manager
1291 .catalog_controller
1292 .create_internal_table_catalog(&streaming_job, incomplete_internal_tables)
1293 .await?;
1294 fragment_graph.refill_internal_table_ids(table_id_map);
1295
1296 tracing::debug!(id = %streaming_job.id(), "building streaming job");
1298 let (mut ctx, stream_job_fragments) = self
1299 .build_stream_job(
1300 ctx,
1301 streaming_job,
1302 fragment_graph,
1303 resource_type,
1304 streaming_job_model,
1305 since_timestamp_epoch,
1306 )
1307 .await?;
1308 ctx.replace_sink = replace_sink;
1309
1310 let streaming_job = &ctx.streaming_job;
1311
1312 match streaming_job {
1313 StreamingJob::Table(None, table, TableJobType::SharedCdcSource) => {
1314 self.validate_cdc_table(table, &stream_job_fragments)
1315 .await?;
1316 }
1317 StreamingJob::Table(Some(source), ..) => {
1318 self.source_manager.register_source(source).await?;
1320 let connector_name = source
1321 .get_with_properties()
1322 .get(UPSTREAM_SOURCE_KEY)
1323 .cloned();
1324 let attr = source.info.as_ref().map(|source_info| {
1325 jsonbb::json!({
1326 "format": source_info.format().as_str_name(),
1327 "encode": source_info.row_encode().as_str_name(),
1328 })
1329 });
1330 report_create_object(
1331 streaming_job.id(),
1332 "source",
1333 PbTelemetryDatabaseObject::Source,
1334 connector_name,
1335 attr,
1336 );
1337 }
1338 StreamingJob::Sink(sink) => {
1339 if sink.auto_refresh_schema_from_table.is_some() {
1340 check_sink_fragments_support_refresh_schema(&stream_job_fragments.fragments)?;
1341 }
1342 validate_sink(sink).await?;
1344 if crate::manager::iceberg_pk_index_sink::is_iceberg_pk_index_sink(&sink.properties)
1349 {
1350 let iceberg_config =
1351 crate::manager::iceberg_pk_index_sink::build_iceberg_config(sink)?;
1352 self.iceberg_pk_index_sink_manager
1353 .register_sink(sink.id, iceberg_config)
1354 .await
1355 .map_err(|e| anyhow!(e).context("register v3 sink worker"))?;
1356 }
1357 let connector_name = sink.get_properties().get(UPSTREAM_SOURCE_KEY).cloned();
1358 let attr = sink.format_desc.as_ref().map(|sink_info| {
1359 jsonbb::json!({
1360 "format": sink_info.format().as_str_name(),
1361 "encode": sink_info.encode().as_str_name(),
1362 })
1363 });
1364 report_create_object(
1365 streaming_job.id(),
1366 "sink",
1367 PbTelemetryDatabaseObject::Sink,
1368 connector_name,
1369 attr,
1370 );
1371 }
1372 StreamingJob::Source(source) => {
1373 self.source_manager.register_source(source).await?;
1375 let connector_name = source
1376 .get_with_properties()
1377 .get(UPSTREAM_SOURCE_KEY)
1378 .cloned();
1379 let attr = source.info.as_ref().map(|source_info| {
1380 jsonbb::json!({
1381 "format": source_info.format().as_str_name(),
1382 "encode": source_info.row_encode().as_str_name(),
1383 })
1384 });
1385 report_create_object(
1386 streaming_job.id(),
1387 "source",
1388 PbTelemetryDatabaseObject::Source,
1389 connector_name,
1390 attr,
1391 );
1392 }
1393 _ => {}
1394 }
1395
1396 let backfill_orders = ctx.fragment_backfill_ordering.to_meta_model();
1397 let notify_creating = replace_sink.is_none();
1400 if notify_creating {
1401 self.metadata_manager
1402 .catalog_controller
1403 .prepare_stream_job_fragments(
1404 &stream_job_fragments,
1405 streaming_job,
1406 false,
1407 Some(backfill_orders),
1408 )
1409 .await?;
1410 } else {
1411 self.metadata_manager
1412 .catalog_controller
1413 .prepare_streaming_job(
1414 stream_job_fragments.stream_job_id(),
1415 || stream_job_fragments.fragments.values(),
1416 &stream_job_fragments.downstreams,
1417 false,
1418 None,
1419 Some(backfill_orders),
1420 )
1421 .await?;
1422 }
1423
1424 Ok((stream_job_fragments, ctx))
1425 }
1426
1427 pub async fn drop_object(
1429 &self,
1430 object_type: ObjectType,
1431 object_id: impl Into<ObjectId>,
1432 drop_mode: DropMode,
1433 ) -> MetaResult<NotificationVersion> {
1434 let object_id = object_id.into();
1435 let _reschedule_job_lock = self.stream_manager.reschedule_lock_read_guard().await;
1438 let _source_tick_pause_guard = self.source_manager.pause_tick().await;
1439
1440 let (release_ctx, version) = self
1441 .metadata_manager
1442 .catalog_controller
1443 .drop_object(object_type, object_id, drop_mode)
1444 .await?;
1445
1446 if object_type == ObjectType::Source {
1447 self.env
1448 .notification_manager_ref()
1449 .notify_local_subscribers(LocalNotification::SourceDropped(object_id));
1450 }
1451
1452 let ReleaseContext {
1453 database_id,
1454 removed_streaming_job_ids,
1455 removed_state_table_ids,
1456 removed_source_ids,
1457 removed_secret_ids: secret_ids,
1458 removed_source_fragments,
1459 removed_fragments,
1460 removed_sink_fragment_by_targets,
1461 removed_iceberg_table_sinks,
1462 removed_iceberg_sink_ids,
1463 removed_iceberg_pk_index_sink_ids,
1464 } = release_ctx;
1465
1466 self.env
1470 .notification_manager_ref()
1471 .notify_serving_fragment_mapping_delete(
1472 removed_fragments.iter().map(|id| *id as _).collect(),
1473 );
1474
1475 self.stream_manager
1476 .drop_streaming_jobs(
1477 database_id,
1478 removed_streaming_job_ids,
1479 removed_state_table_ids,
1480 removed_sink_fragment_by_targets
1481 .into_iter()
1482 .map(|(target, sinks)| {
1483 (target as _, sinks.into_iter().map(|id| id as _).collect())
1484 })
1485 .collect(),
1486 )
1487 .await;
1488
1489 self.source_manager
1492 .apply_source_change(SourceChange::DropSource {
1493 dropped_source_ids: removed_source_ids.into_iter().map(|id| id as _).collect(),
1494 })
1495 .await;
1496
1497 let dropped_source_fragments = removed_source_fragments;
1500 self.source_manager
1501 .apply_source_change(SourceChange::DropMv {
1502 dropped_source_fragments,
1503 })
1504 .await;
1505
1506 let iceberg_sink_ids: Vec<SinkId> = removed_iceberg_table_sinks
1508 .iter()
1509 .map(|sink| sink.id)
1510 .collect();
1511
1512 for sink in removed_iceberg_table_sinks {
1513 let sink_param = SinkParam::try_from_sink_catalog(sink.into())
1514 .expect("Iceberg sink should be valid");
1515 let iceberg_sink =
1516 IcebergSink::try_from(sink_param).expect("Iceberg sink should be valid");
1517 if let Ok(iceberg_catalog) = iceberg_sink.config.create_catalog().await {
1518 let table_identifier = iceberg_sink.config.full_table_name().unwrap();
1519 tracing::info!(
1520 "dropping iceberg table {} for dropped sink",
1521 table_identifier
1522 );
1523
1524 let _ = iceberg_catalog
1525 .drop_table(&table_identifier)
1526 .await
1527 .inspect_err(|err| {
1528 tracing::error!(
1529 "failed to drop iceberg table {} during cleanup: {}",
1530 table_identifier,
1531 err.as_report()
1532 );
1533 });
1534 }
1535 }
1536
1537 if !iceberg_sink_ids.is_empty() {
1539 self.sink_manager
1540 .stop_sink_coordinator(iceberg_sink_ids)
1541 .await;
1542 }
1543
1544 for sink_id in removed_iceberg_sink_ids {
1547 self.iceberg_compaction_manager
1548 .clear_iceberg_maintenance_by_sink_id(sink_id);
1549 }
1550
1551 if !removed_iceberg_pk_index_sink_ids.is_empty() {
1555 self.iceberg_pk_index_sink_manager
1556 .unregister_sinks(removed_iceberg_pk_index_sink_ids);
1557 }
1558
1559 for secret in secret_ids {
1561 LocalSecretManager::global().remove_secret(secret);
1562 }
1563 Ok(version)
1564 }
1565
1566 #[await_tree::instrument(boxed, "replace_streaming_job({streaming_job})")]
1568 pub async fn replace_job(
1569 &self,
1570 mut streaming_job: StreamingJob,
1571 fragment_graph: StreamFragmentGraphProto,
1572 ) -> MetaResult<NotificationVersion> {
1573 match &streaming_job {
1574 StreamingJob::Table(..)
1575 | StreamingJob::Source(..)
1576 | StreamingJob::MaterializedView(..) => {}
1577 StreamingJob::Sink(..) | StreamingJob::Index(..) => {
1578 bail_not_implemented!("schema change for {}", streaming_job.job_type_str())
1579 }
1580 }
1581
1582 let job_id = streaming_job.id();
1583
1584 let _reschedule_job_lock = self.stream_manager.reschedule_lock_read_guard().await;
1585 let ctx = StreamContext::from_protobuf(fragment_graph.get_ctx().unwrap());
1586
1587 let original_max_parallelism = self
1589 .metadata_manager
1590 .get_job_max_parallelism(streaming_job.id())
1591 .await?;
1592 let fragment_graph = PbStreamFragmentGraph {
1593 max_parallelism: original_max_parallelism as _,
1594 ..fragment_graph
1595 };
1596
1597 let fragment_graph = StreamFragmentGraph::new(&self.env, fragment_graph, &streaming_job)?;
1599 streaming_job.set_info_from_graph(&fragment_graph);
1600
1601 let streaming_job = streaming_job;
1603
1604 let auto_refresh_schema_sinks = if let StreamingJob::Table(_, table, _) = &streaming_job {
1605 let auto_refresh_schema_sinks = self
1606 .metadata_manager
1607 .catalog_controller
1608 .get_sink_auto_refresh_schema_from(table.id)
1609 .await?;
1610 if !auto_refresh_schema_sinks.is_empty() {
1611 let original_table_columns = self
1612 .metadata_manager
1613 .catalog_controller
1614 .get_table_columns(table.id)
1615 .await?;
1616 let original_table_column_ids: HashSet<_> = original_table_columns
1618 .iter()
1619 .map(|col| col.column_id())
1620 .collect();
1621 let new_table_column_ids: HashSet<_> = table
1622 .columns
1623 .iter()
1624 .map(|col| ColumnId::new(col.column_desc.as_ref().unwrap().column_id as _))
1625 .collect();
1626 let newly_added_columns = table
1627 .columns
1628 .iter()
1629 .filter(|col| {
1630 !original_table_column_ids.contains(&ColumnId::new(
1631 col.column_desc.as_ref().unwrap().column_id as _,
1632 ))
1633 })
1634 .map(|col| ColumnCatalog::from(col.clone()))
1635 .collect_vec();
1636 let removed_columns = original_table_columns
1637 .iter()
1638 .filter(|col| !new_table_column_ids.contains(&col.column_id()))
1639 .cloned()
1640 .collect_vec();
1641 let mut sinks = Vec::with_capacity(auto_refresh_schema_sinks.len());
1642 for sink in auto_refresh_schema_sinks {
1643 let sink_job_fragments = self
1644 .metadata_manager
1645 .get_job_fragments_by_id(sink.id.as_job_id())
1646 .await?;
1647 if sink_job_fragments.fragments.len() != 1 {
1648 return Err(anyhow!(
1649 "auto schema refresh sink must have only one fragment, but got {}",
1650 sink_job_fragments.fragments.len()
1651 )
1652 .into());
1653 }
1654 let sink_ctx = sink_job_fragments.ctx;
1655 let original_sink_fragment =
1656 sink_job_fragments.fragments.into_values().next().unwrap();
1657 let (new_sink_fragment, new_schema, new_log_store_table) =
1658 rewrite_refresh_schema_sink_fragment(
1659 &original_sink_fragment,
1660 &sink,
1661 &newly_added_columns,
1662 &removed_columns,
1663 table,
1664 fragment_graph.table_fragment_id(),
1665 self.env.id_gen_manager(),
1666 )?;
1667
1668 let streaming_job = StreamingJob::Sink(sink);
1669
1670 let tmp_sink_model = self
1671 .metadata_manager
1672 .catalog_controller
1673 .create_job_catalog_for_replace(&streaming_job, None, None, None)
1674 .await?;
1675 let tmp_sink_id = tmp_sink_model.job_id.as_sink_id();
1676 let StreamingJob::Sink(sink) = streaming_job else {
1677 unreachable!()
1678 };
1679
1680 sinks.push(AutoRefreshSchemaSinkContext {
1681 tmp_sink_id,
1682 original_sink: sink,
1683 original_fragment: original_sink_fragment,
1684 new_schema,
1685 newly_add_fields: newly_added_columns
1686 .iter()
1687 .map(|col| Field::from(&col.column_desc))
1688 .collect(),
1689 removed_column_names: removed_columns
1690 .iter()
1691 .map(|col| col.name.clone())
1692 .collect(),
1693 new_fragment: new_sink_fragment,
1694 new_log_store_table: new_log_store_table.map(Box::new),
1695 ctx: sink_ctx,
1696 });
1697 }
1698 Some(sinks)
1699 } else {
1700 None
1701 }
1702 } else {
1703 None
1704 };
1705
1706 let streaming_job_model = self
1707 .metadata_manager
1708 .catalog_controller
1709 .create_job_catalog_for_replace(
1710 &streaming_job,
1711 Some(&ctx),
1712 fragment_graph.specified_parallelism().as_ref(),
1713 Some(fragment_graph.max_parallelism()),
1714 )
1715 .await?;
1716 let tmp_id = streaming_job_model.job_id;
1717
1718 let tmp_sink_ids = auto_refresh_schema_sinks.as_ref().map(|sinks| {
1719 sinks
1720 .iter()
1721 .map(|sink| sink.tmp_sink_id.as_object_id())
1722 .collect_vec()
1723 });
1724
1725 tracing::debug!(id = %job_id, "building replace streaming job");
1726 let mut updated_sink_catalogs = vec![];
1727
1728 let mut drop_table_connector_ctx = None;
1729 let result: MetaResult<_> = try {
1730 let (mut ctx, mut stream_job_fragments) = self
1731 .build_replace_job(
1732 ctx,
1733 &streaming_job,
1734 fragment_graph,
1735 tmp_id,
1736 auto_refresh_schema_sinks,
1737 streaming_job_model,
1738 )
1739 .await?;
1740 drop_table_connector_ctx = ctx.drop_table_connector_ctx.clone();
1741 let auto_refresh_schema_sink_finish_ctx =
1742 ctx.auto_refresh_schema_sinks.as_ref().map(|sinks| {
1743 sinks
1744 .iter()
1745 .map(|sink| FinishAutoRefreshSchemaSinkContext {
1746 tmp_sink_id: sink.tmp_sink_id,
1747 original_sink_id: sink.original_sink.id,
1748 columns: sink.new_schema.clone(),
1749 new_log_store_table: sink.new_log_store_table.clone(),
1750 })
1751 .collect()
1752 });
1753
1754 if let StreamingJob::Table(_, table, ..) = &streaming_job {
1756 let union_fragment = stream_job_fragments.inner.union_fragment_for_table();
1757 let upstream_infos = self
1758 .metadata_manager
1759 .catalog_controller
1760 .get_all_upstream_sink_infos(table, union_fragment.fragment_id as _)
1761 .await?;
1762 refill_upstream_sink_union_in_table(&mut union_fragment.nodes, &upstream_infos);
1763
1764 for upstream_info in &upstream_infos {
1765 let upstream_fragment_id = upstream_info.sink_fragment_id;
1766 ctx.upstream_fragment_downstreams
1767 .entry(upstream_fragment_id)
1768 .or_default()
1769 .push(upstream_info.new_sink_downstream.clone());
1770 if upstream_info.sink_original_target_columns.is_empty() {
1771 updated_sink_catalogs.push(upstream_info.sink_id);
1772 }
1773 }
1774 }
1775
1776 let replace_upstream = ctx.replace_upstream.clone();
1777
1778 if let Some(sinks) = &ctx.auto_refresh_schema_sinks {
1779 let empty_downstreams = FragmentDownstreamRelation::default();
1780 for sink in sinks {
1781 self.metadata_manager
1782 .catalog_controller
1783 .prepare_streaming_job(
1784 sink.tmp_sink_id.as_job_id(),
1785 || [&sink.new_fragment].into_iter(),
1786 &empty_downstreams,
1787 true,
1788 None,
1789 None,
1790 )
1791 .await?;
1792 }
1793 }
1794
1795 self.metadata_manager
1796 .catalog_controller
1797 .prepare_stream_job_fragments(&stream_job_fragments, &streaming_job, true, None)
1798 .await?;
1799
1800 self.stream_manager
1801 .replace_stream_job(stream_job_fragments, ctx)
1802 .await?;
1803 (replace_upstream, auto_refresh_schema_sink_finish_ctx)
1804 };
1805
1806 match result {
1807 Ok((replace_upstream, auto_refresh_schema_sink_finish_ctx)) => {
1808 let version = self
1809 .metadata_manager
1810 .catalog_controller
1811 .finish_replace_streaming_job(
1812 tmp_id,
1813 streaming_job,
1814 replace_upstream,
1815 SinkIntoTableContext {
1816 updated_sink_catalogs,
1817 },
1818 drop_table_connector_ctx.as_ref(),
1819 auto_refresh_schema_sink_finish_ctx,
1820 )
1821 .await?;
1822 if let Some(drop_table_connector_ctx) = &drop_table_connector_ctx {
1823 self.source_manager
1824 .apply_source_change(SourceChange::DropSource {
1825 dropped_source_ids: vec![drop_table_connector_ctx.to_remove_source_id],
1826 })
1827 .await;
1828 }
1829 Ok(version)
1830 }
1831 Err(err) => {
1832 tracing::error!(id = %job_id, error = ?err.as_report(), "failed to replace job");
1833 let _ = self.metadata_manager
1834 .catalog_controller
1835 .try_abort_replacing_streaming_job(tmp_id, tmp_sink_ids)
1836 .await.inspect_err(|err| {
1837 tracing::error!(id = %job_id, error = ?err.as_report(), "failed to abort replacing job");
1838 });
1839 Err(err)
1840 }
1841 }
1842 }
1843
1844 #[await_tree::instrument(boxed, "drop_streaming_job{}({job_id})", if let DropMode::Cascade = drop_mode { "_cascade" } else { "" }
1845 )]
1846 async fn drop_streaming_job(
1847 &self,
1848 job_id: StreamingJobId,
1849 drop_mode: DropMode,
1850 ) -> MetaResult<NotificationVersion> {
1851 let (object_id, object_type) = match job_id {
1852 StreamingJobId::MaterializedView(id) => (id.as_object_id(), ObjectType::Table),
1853 StreamingJobId::Sink(id) => (id.as_object_id(), ObjectType::Sink),
1854 StreamingJobId::Table(_, id) => (id.as_object_id(), ObjectType::Table),
1855 StreamingJobId::Index(idx) => (idx.as_object_id(), ObjectType::Index),
1856 };
1857
1858 let job_status = self
1859 .metadata_manager
1860 .catalog_controller
1861 .get_streaming_job_status(job_id.id())
1862 .await?;
1863 let version = match job_status {
1864 JobStatus::Initial => {
1865 let abort_result = self
1866 .metadata_manager
1867 .catalog_controller
1868 .try_abort_creating_streaming_job(job_id.id(), true)
1869 .await?;
1870 self.iceberg_compaction_manager
1871 .clear_maintenance_for_aborted_job(&abort_result);
1872 IGNORED_NOTIFICATION_VERSION
1873 }
1874 JobStatus::Creating => {
1875 self.stream_manager
1876 .cancel_streaming_jobs(vec![job_id.id()])
1877 .await?;
1878 IGNORED_NOTIFICATION_VERSION
1879 }
1880 JobStatus::Created => self.drop_object(object_type, object_id, drop_mode).await?,
1881 };
1882
1883 Ok(version)
1884 }
1885
1886 #[await_tree::instrument]
1892 pub(crate) async fn build_stream_job(
1893 &self,
1894 stream_ctx: StreamContext,
1895 mut stream_job: StreamingJob,
1896 fragment_graph: StreamFragmentGraph,
1897 resource_type: streaming_job_resource_type::ResourceType,
1898 streaming_job_model: streaming_job::Model,
1899 since_timestamp_epoch: Option<u64>,
1900 ) -> MetaResult<(CreateStreamingJobContext, StreamJobFragmentsToCreate)> {
1901 let id = stream_job.id();
1902 let max_parallelism = NonZeroUsize::new(fragment_graph.max_parallelism()).unwrap();
1903 Self::validate_specified_parallelism(
1904 fragment_graph.specified_parallelism(),
1905 fragment_graph.specified_backfill_parallelism(),
1906 max_parallelism,
1907 )?;
1908
1909 let fragment_backfill_ordering = fragment_graph.create_fragment_backfill_ordering();
1911
1912 let (snapshot_backfill_info, cross_db_snapshot_backfill_info) =
1916 fragment_graph.collect_snapshot_backfill_info()?;
1917 assert!(
1918 snapshot_backfill_info
1919 .iter()
1920 .chain([&cross_db_snapshot_backfill_info])
1921 .flat_map(|info| info.upstream_mv_table_id_to_backfill_epoch.values())
1922 .all(|backfill_epoch| backfill_epoch.is_none()),
1923 "should not set backfill epoch when initially build the job: {:?} {:?}",
1924 snapshot_backfill_info,
1925 cross_db_snapshot_backfill_info
1926 );
1927
1928 let locality_fragment_state_table_mapping =
1929 fragment_graph.find_locality_provider_fragment_state_table_mapping();
1930
1931 self.metadata_manager
1933 .catalog_controller
1934 .validate_cross_db_snapshot_backfill(&cross_db_snapshot_backfill_info)
1935 .await?;
1936
1937 let upstream_table_ids = fragment_graph
1938 .dependent_table_ids()
1939 .iter()
1940 .filter(|id| {
1941 !cross_db_snapshot_backfill_info
1942 .upstream_mv_table_id_to_backfill_epoch
1943 .contains_key(*id)
1944 })
1945 .cloned()
1946 .collect();
1947
1948 let upstream_root_fragments = self
1949 .metadata_manager
1950 .get_upstream_root_fragments(&upstream_table_ids)
1951 .await?;
1952
1953 if snapshot_backfill_info.is_some() {
1954 match stream_job {
1955 StreamingJob::MaterializedView(_)
1956 | StreamingJob::Sink(_)
1957 | StreamingJob::Index(_, _) => {}
1958 StreamingJob::Table(_, _, _) | StreamingJob::Source(_) => {
1959 return Err(
1960 anyhow!("snapshot_backfill not enabled for table and source").into(),
1961 );
1962 }
1963 }
1964 }
1965
1966 let complete_graph = CompleteStreamFragmentGraph::with_upstreams(
1967 fragment_graph,
1968 FragmentGraphUpstreamContext {
1969 upstream_root_fragments,
1970 },
1971 (&stream_job).into(),
1972 )?;
1973 let database_resource_group = self
1974 .metadata_manager
1975 .get_database_resource_group(stream_job.database_id())
1976 .await?;
1977 let is_serverless_backfill = matches!(
1978 &resource_type,
1979 streaming_job_resource_type::ResourceType::ServerlessBackfill(true)
1980 );
1981
1982 let actor_graph_builder = ActorGraphBuilder::new(complete_graph)?;
1984
1985 let ActorGraphBuildResult {
1986 graph,
1987 downstream_fragment_relations,
1988 upstream_fragment_downstreams,
1989 replace_upstream,
1990 } = actor_graph_builder.generate_graph()?;
1991 assert!(replace_upstream.is_empty());
1992
1993 let stream_job_fragments =
1998 StreamJobFragments::new(id, graph, stream_ctx.clone(), max_parallelism.get());
1999
2000 if let Some(mview_fragment) = stream_job_fragments.mview_fragment() {
2001 stream_job.set_table_vnode_count(mview_fragment.vnode_count());
2002 }
2003
2004 let new_upstream_sink = if let StreamingJob::Sink(sink) = &stream_job
2005 && let Ok(table_id) = sink.get_target_table()
2006 {
2007 let tables = self
2008 .metadata_manager
2009 .get_table_catalog_by_ids(&[*table_id])
2010 .await?;
2011 let target_table = tables
2012 .first()
2013 .ok_or_else(|| MetaError::catalog_id_not_found("table", *table_id))?;
2014 let sink_fragment = stream_job_fragments
2015 .sink_fragment()
2016 .ok_or_else(|| anyhow::anyhow!("sink fragment not found for sink {}", sink.id))?;
2017 let mview_fragment_id = self
2018 .metadata_manager
2019 .catalog_controller
2020 .get_mview_fragment_by_id(table_id.as_job_id())
2021 .await?;
2022 let upstream_sink_info = build_upstream_sink_info(
2023 sink.id,
2024 sink.original_target_columns.clone(),
2025 sink_fragment.fragment_id as _,
2026 target_table,
2027 mview_fragment_id,
2028 )?;
2029 Some(upstream_sink_info)
2030 } else {
2031 None
2032 };
2033
2034 let mut cdc_table_snapshot_splits = None;
2035 if let StreamingJob::Table(None, table, TableJobType::SharedCdcSource) = &stream_job
2036 && let Some((_, stream_cdc_scan)) =
2037 parallel_cdc_table_backfill_fragment(stream_job_fragments.fragments.values())
2038 {
2039 {
2040 let splits = try_init_parallel_cdc_table_snapshot_splits(
2042 table.id,
2043 stream_cdc_scan.cdc_table_desc.as_ref().unwrap(),
2044 self.env.meta_store_ref(),
2045 stream_cdc_scan.options.as_ref().unwrap(),
2046 self.env.opts.cdc_table_split_init_insert_batch_size,
2047 self.env.opts.cdc_table_split_init_sleep_interval_splits,
2048 self.env.opts.cdc_table_split_init_sleep_duration_millis,
2049 )
2050 .await?;
2051 cdc_table_snapshot_splits = Some(splits);
2052 }
2053 }
2054
2055 let ctx = CreateStreamingJobContext {
2056 upstream_fragment_downstreams,
2057 database_resource_group,
2058 definition: stream_job.definition(),
2059 create_type: stream_job.create_type(),
2060 job_type: (&stream_job).into(),
2061 streaming_job: stream_job,
2062 new_upstream_sink,
2063 option: CreateStreamingJobOption {},
2064 snapshot_backfill_info,
2065 cross_db_snapshot_backfill_info,
2066 fragment_backfill_ordering,
2067 locality_fragment_state_table_mapping,
2068 cdc_table_snapshot_splits,
2069 is_serverless_backfill,
2070 resource_type,
2071 streaming_job_model: streaming_job_model.clone(),
2072 replace_sink: None,
2073 refresh_interval_sec: streaming_job_model.refresh_interval_sec.map(|s| s as u64),
2074 since_timestamp_epoch,
2075 };
2076
2077 Ok((
2078 ctx,
2079 StreamJobFragmentsToCreate {
2080 inner: stream_job_fragments,
2081 downstreams: downstream_fragment_relations,
2082 },
2083 ))
2084 }
2085
2086 pub(crate) async fn build_replace_job(
2092 &self,
2093 stream_ctx: StreamContext,
2094 stream_job: &StreamingJob,
2095 mut fragment_graph: StreamFragmentGraph,
2096 tmp_job_id: JobId,
2097 auto_refresh_schema_sinks: Option<Vec<AutoRefreshSchemaSinkContext>>,
2098 streaming_job_model: streaming_job::Model,
2099 ) -> MetaResult<(ReplaceStreamJobContext, StreamJobFragmentsToCreate)> {
2100 match &stream_job {
2101 StreamingJob::Table(..)
2102 | StreamingJob::Source(..)
2103 | StreamingJob::MaterializedView(..) => {}
2104 StreamingJob::Sink(..) | StreamingJob::Index(..) => {
2105 bail_not_implemented!("schema change for {}", stream_job.job_type_str())
2106 }
2107 }
2108
2109 let id = stream_job.id();
2110
2111 let mut drop_table_associated_source_id = None;
2113 if let StreamingJob::Table(None, _, _) = &stream_job {
2114 drop_table_associated_source_id = self
2115 .metadata_manager
2116 .get_table_associated_source_id(id.as_mv_table_id())
2117 .await?;
2118 }
2119
2120 let old_fragments = self.metadata_manager.get_job_fragments_by_id(id).await?;
2121 let old_internal_table_ids = old_fragments.internal_table_ids();
2122
2123 let mut drop_table_connector_ctx = None;
2125 if let Some(to_remove_source_id) = drop_table_associated_source_id {
2126 debug_assert!(old_internal_table_ids.len() == 1);
2128
2129 drop_table_connector_ctx = Some(DropTableConnectorContext {
2130 to_change_streaming_job_id: id,
2133 to_remove_state_table_id: old_internal_table_ids[0], to_remove_source_id,
2135 });
2136 } else if stream_job.is_materialized_view() {
2137 let old_fragments_upstreams = self
2140 .metadata_manager
2141 .catalog_controller
2142 .upstream_fragments(old_fragments.fragment_ids())
2143 .await?;
2144
2145 let old_state_graph =
2146 state_match::Graph::from_existing(&old_fragments, &old_fragments_upstreams);
2147 let new_state_graph = state_match::Graph::from_building(&fragment_graph);
2148 let result = state_match::match_graph(&new_state_graph, &old_state_graph)
2149 .context("incompatible altering on the streaming job states")?;
2150
2151 fragment_graph.fit_internal_table_ids_with_mapping(result.table_matches);
2152 fragment_graph.fit_snapshot_backfill_epochs(result.snapshot_backfill_epochs);
2153 } else {
2154 let old_internal_tables = self
2157 .metadata_manager
2158 .get_table_catalog_by_ids(&old_internal_table_ids)
2159 .await?;
2160 fragment_graph.fit_internal_tables_trivial(old_internal_tables)?;
2161 }
2162
2163 let original_root_fragment = old_fragments
2166 .root_fragment()
2167 .expect("root fragment not found");
2168
2169 let job_type = StreamingJobType::from(stream_job);
2170
2171 let mut downstream_fragments = self.metadata_manager.get_downstream_fragments(id).await?;
2173
2174 if let Some(auto_refresh_schema_sinks) = &auto_refresh_schema_sinks {
2175 let mut remaining_fragment: HashSet<_> = auto_refresh_schema_sinks
2176 .iter()
2177 .map(|sink| sink.original_fragment.fragment_id)
2178 .collect();
2179 for (_, downstream_fragment) in &mut downstream_fragments {
2180 if let Some(sink) = auto_refresh_schema_sinks.iter().find(|sink| {
2181 sink.original_fragment.fragment_id == downstream_fragment.fragment_id
2182 }) {
2183 assert!(remaining_fragment.remove(&downstream_fragment.fragment_id));
2184 *downstream_fragment = sink.new_fragment.clone();
2187 }
2188 }
2189 assert!(remaining_fragment.is_empty());
2190 }
2191
2192 let complete_graph = match &job_type {
2194 StreamingJobType::Table(TableJobType::General) | StreamingJobType::Source => {
2195 CompleteStreamFragmentGraph::with_downstreams(
2196 fragment_graph,
2197 FragmentGraphDownstreamContext {
2198 original_root_fragment_id: original_root_fragment.fragment_id,
2199 downstream_fragments,
2200 },
2201 job_type,
2202 )?
2203 }
2204 StreamingJobType::Table(TableJobType::SharedCdcSource)
2205 | StreamingJobType::MaterializedView => {
2206 let upstream_root_fragments = self
2208 .metadata_manager
2209 .get_upstream_root_fragments(fragment_graph.dependent_table_ids())
2210 .await?;
2211
2212 CompleteStreamFragmentGraph::with_upstreams_and_downstreams(
2213 fragment_graph,
2214 FragmentGraphUpstreamContext {
2215 upstream_root_fragments,
2216 },
2217 FragmentGraphDownstreamContext {
2218 original_root_fragment_id: original_root_fragment.fragment_id,
2219 downstream_fragments,
2220 },
2221 job_type,
2222 )?
2223 }
2224 _ => unreachable!(),
2225 };
2226
2227 let resource_group = self
2228 .metadata_manager
2229 .get_database_resource_group(stream_job.database_id())
2230 .await?;
2231
2232 let actor_graph_builder = ActorGraphBuilder::new(complete_graph)?;
2233
2234 let ActorGraphBuildResult {
2235 graph,
2236 downstream_fragment_relations,
2237 upstream_fragment_downstreams,
2238 mut replace_upstream,
2239 } = actor_graph_builder.generate_graph()?;
2240
2241 if matches!(
2243 job_type,
2244 StreamingJobType::Source | StreamingJobType::Table(TableJobType::General)
2245 ) {
2246 assert!(upstream_fragment_downstreams.is_empty());
2247 }
2248
2249 let stream_job_fragments =
2253 StreamJobFragments::new(tmp_job_id, graph, stream_ctx, old_fragments.max_parallelism);
2254
2255 if let Some(sinks) = &auto_refresh_schema_sinks {
2256 for sink in sinks {
2257 replace_upstream
2258 .remove(&sink.new_fragment.fragment_id)
2259 .expect("should exist");
2260 }
2261 }
2262
2263 let ctx = ReplaceStreamJobContext {
2267 old_fragments,
2268 replace_upstream,
2269 upstream_fragment_downstreams,
2270 streaming_job: stream_job.clone(),
2271 database_resource_group: resource_group,
2272 tmp_id: tmp_job_id,
2273 drop_table_connector_ctx,
2274 auto_refresh_schema_sinks,
2275 streaming_job_model,
2276 };
2277
2278 Ok((
2279 ctx,
2280 StreamJobFragmentsToCreate {
2281 inner: stream_job_fragments,
2282 downstreams: downstream_fragment_relations,
2283 },
2284 ))
2285 }
2286
2287 async fn alter_name(
2288 &self,
2289 relation: alter_name_request::Object,
2290 new_name: &str,
2291 ) -> MetaResult<NotificationVersion> {
2292 let (obj_type, id): (ObjectType, ObjectId) = match relation {
2293 alter_name_request::Object::TableId(id) => (ObjectType::Table, id.into()),
2294 alter_name_request::Object::ViewId(id) => (ObjectType::View, id.into()),
2295 alter_name_request::Object::IndexId(id) => (ObjectType::Index, id.into()),
2296 alter_name_request::Object::SinkId(id) => (ObjectType::Sink, id.into()),
2297 alter_name_request::Object::SourceId(id) => (ObjectType::Source, id.into()),
2298 alter_name_request::Object::SchemaId(id) => (ObjectType::Schema, id.into()),
2299 alter_name_request::Object::DatabaseId(id) => (ObjectType::Database, id.into()),
2300 alter_name_request::Object::SubscriptionId(id) => (ObjectType::Subscription, id.into()),
2301 };
2302 self.metadata_manager
2303 .catalog_controller
2304 .alter_name(obj_type, id, new_name)
2305 .await
2306 }
2307
2308 async fn alter_swap_rename(
2309 &self,
2310 object: alter_swap_rename_request::Object,
2311 ) -> MetaResult<NotificationVersion> {
2312 let (obj_type, src_id, dst_id) = match object {
2313 alter_swap_rename_request::Object::Schema(_) => unimplemented!("schema swap"),
2314 alter_swap_rename_request::Object::Table(objs) => {
2315 let (src_id, dst_id) = (objs.src_object_id, objs.dst_object_id);
2316 (ObjectType::Table, src_id, dst_id)
2317 }
2318 alter_swap_rename_request::Object::View(objs) => {
2319 let (src_id, dst_id) = (objs.src_object_id, objs.dst_object_id);
2320 (ObjectType::View, src_id, dst_id)
2321 }
2322 alter_swap_rename_request::Object::Source(objs) => {
2323 let (src_id, dst_id) = (objs.src_object_id, objs.dst_object_id);
2324 (ObjectType::Source, src_id, dst_id)
2325 }
2326 alter_swap_rename_request::Object::Sink(objs) => {
2327 let (src_id, dst_id) = (objs.src_object_id, objs.dst_object_id);
2328 (ObjectType::Sink, src_id, dst_id)
2329 }
2330 alter_swap_rename_request::Object::Subscription(objs) => {
2331 let (src_id, dst_id) = (objs.src_object_id, objs.dst_object_id);
2332 (ObjectType::Subscription, src_id, dst_id)
2333 }
2334 };
2335
2336 self.metadata_manager
2337 .catalog_controller
2338 .alter_swap_rename(obj_type, src_id, dst_id)
2339 .await
2340 }
2341
2342 async fn alter_owner(
2343 &self,
2344 object: Object,
2345 owner_id: UserId,
2346 ) -> MetaResult<NotificationVersion> {
2347 let (obj_type, id): (ObjectType, ObjectId) = match object {
2348 Object::TableId(id) => (ObjectType::Table, id.into()),
2349 Object::ViewId(id) => (ObjectType::View, id.into()),
2350 Object::SourceId(id) => (ObjectType::Source, id.into()),
2351 Object::SinkId(id) => (ObjectType::Sink, id.into()),
2352 Object::SchemaId(id) => (ObjectType::Schema, id.into()),
2353 Object::DatabaseId(id) => (ObjectType::Database, id.into()),
2354 Object::SubscriptionId(id) => (ObjectType::Subscription, id.into()),
2355 Object::ConnectionId(id) => (ObjectType::Connection, id.into()),
2356 Object::FunctionId(id) => (ObjectType::Function, id.into()),
2357 Object::SecretId(id) => (ObjectType::Secret, id.into()),
2358 };
2359 self.metadata_manager
2360 .catalog_controller
2361 .alter_owner(obj_type, id, owner_id as _)
2362 .await
2363 }
2364
2365 async fn alter_set_schema(
2366 &self,
2367 object: alter_set_schema_request::Object,
2368 new_schema_id: SchemaId,
2369 ) -> MetaResult<NotificationVersion> {
2370 let (obj_type, id): (ObjectType, ObjectId) = match object {
2371 alter_set_schema_request::Object::TableId(id) => (ObjectType::Table, id.into()),
2372 alter_set_schema_request::Object::ViewId(id) => (ObjectType::View, id.into()),
2373 alter_set_schema_request::Object::SourceId(id) => (ObjectType::Source, id.into()),
2374 alter_set_schema_request::Object::SinkId(id) => (ObjectType::Sink, id.into()),
2375 alter_set_schema_request::Object::FunctionId(id) => (ObjectType::Function, id.into()),
2376 alter_set_schema_request::Object::ConnectionId(id) => {
2377 (ObjectType::Connection, id.into())
2378 }
2379 alter_set_schema_request::Object::SubscriptionId(id) => {
2380 (ObjectType::Subscription, id.into())
2381 }
2382 };
2383 self.metadata_manager
2384 .catalog_controller
2385 .alter_schema(obj_type, id, new_schema_id)
2386 .await
2387 }
2388
2389 pub async fn wait(&self, job_id: Option<JobId>) -> MetaResult<WaitVersion> {
2390 if let Some(job_id) = job_id {
2391 let database_id = self
2392 .metadata_manager
2393 .catalog_controller
2394 .get_object_database_id(job_id)
2395 .await?;
2396 let catalog_version = self
2397 .metadata_manager
2398 .wait_streaming_job_finished(database_id, job_id)
2399 .await?;
2400 let hummock_version_id = self.barrier_manager.get_hummock_version_id().await;
2401 return Ok(WaitVersion {
2402 catalog_version,
2403 hummock_version_id,
2404 });
2405 }
2406
2407 let timeout_ms = 2 * 60 * 60 * 1000;
2408 let poll_interval = Duration::from_millis(100);
2409 for _ in 0..(timeout_ms / poll_interval.as_millis() as usize) {
2410 let background_jobs = self
2411 .metadata_manager
2412 .catalog_controller
2413 .list_background_creating_jobs(true, None)
2414 .await?;
2415 if background_jobs.is_empty() {
2416 let catalog_version = self
2417 .metadata_manager
2418 .catalog_controller
2419 .notify_frontend_trivial()
2420 .await;
2421 let hummock_version_id = self.barrier_manager.get_hummock_version_id().await;
2422 return Ok(WaitVersion {
2423 catalog_version,
2424 hummock_version_id,
2425 });
2426 }
2427
2428 sleep(poll_interval).await;
2429 }
2430 Err(MetaError::cancelled(format!(
2431 "timeout after {timeout_ms}ms"
2432 )))
2433 }
2434
2435 async fn comment_on(&self, comment: Comment) -> MetaResult<NotificationVersion> {
2436 self.metadata_manager
2437 .catalog_controller
2438 .comment_on(comment)
2439 .await
2440 }
2441
2442 async fn alter_streaming_job_config(
2443 &self,
2444 job_id: JobId,
2445 entries_to_add: HashMap<String, String>,
2446 keys_to_remove: Vec<String>,
2447 ) -> MetaResult<NotificationVersion> {
2448 self.metadata_manager
2449 .catalog_controller
2450 .alter_streaming_job_config(job_id, entries_to_add, keys_to_remove)
2451 .await
2452 }
2453}
2454
2455fn report_create_object(
2456 job_id: JobId,
2457 event_name: &str,
2458 obj_type: PbTelemetryDatabaseObject,
2459 connector_name: Option<String>,
2460 attr_info: Option<jsonbb::Value>,
2461) {
2462 report_event(
2463 PbTelemetryEventStage::CreateStreamJob,
2464 event_name,
2465 job_id.as_raw_id() as _,
2466 connector_name,
2467 Some(obj_type),
2468 attr_info,
2469 );
2470}
2471
2472pub fn build_upstream_sink_info(
2473 sink_id: SinkId,
2474 original_target_columns: Vec<PbColumnCatalog>,
2475 sink_fragment_id: FragmentId,
2476 target_table: &PbTable,
2477 target_fragment_id: FragmentId,
2478) -> MetaResult<UpstreamSinkInfo> {
2479 let sink_columns = if !original_target_columns.is_empty() {
2480 original_target_columns.clone()
2481 } else {
2482 target_table.columns.clone()
2487 };
2488
2489 let sink_output_fields = sink_columns
2490 .iter()
2491 .map(|col| Field::from(col.column_desc.as_ref().unwrap()).to_prost())
2492 .collect_vec();
2493 let output_indices = (0..sink_output_fields.len())
2494 .map(|i| i as u32)
2495 .collect_vec();
2496
2497 let dist_key_indices: anyhow::Result<Vec<u32>> = try {
2498 let sink_idx_by_col_id = sink_columns
2499 .iter()
2500 .enumerate()
2501 .map(|(idx, col)| {
2502 let column_id = col.column_desc.as_ref().unwrap().column_id;
2503 (column_id, idx as u32)
2504 })
2505 .collect::<HashMap<_, _>>();
2506 target_table
2507 .distribution_key
2508 .iter()
2509 .map(|dist_idx| {
2510 let column_id = target_table.columns[*dist_idx as usize]
2511 .column_desc
2512 .as_ref()
2513 .unwrap()
2514 .column_id;
2515 let sink_idx = sink_idx_by_col_id
2516 .get(&column_id)
2517 .ok_or_else(|| anyhow::anyhow!("column id {} not found in sink", column_id))?;
2518 Ok(*sink_idx)
2519 })
2520 .collect::<anyhow::Result<Vec<_>>>()?
2521 };
2522 let dist_key_indices =
2523 dist_key_indices.map_err(|e| e.context("failed to get distribution key indices"))?;
2524 let downstream_fragment_id = target_fragment_id as _;
2525 let new_downstream_relation = DownstreamFragmentRelation {
2526 downstream_fragment_id,
2527 dispatcher_type: DispatcherType::Hash,
2528 dist_key_indices,
2529 output_mapping: PbDispatchOutputMapping::simple(output_indices),
2530 };
2531 let current_target_columns = target_table.get_columns();
2532 let project_exprs = build_select_node_list(&sink_columns, current_target_columns)?;
2533 Ok(UpstreamSinkInfo {
2534 sink_id,
2535 sink_fragment_id: sink_fragment_id as _,
2536 sink_output_fields,
2537 sink_original_target_columns: original_target_columns,
2538 project_exprs,
2539 new_sink_downstream: new_downstream_relation,
2540 })
2541}
2542
2543pub fn refill_upstream_sink_union_in_table(
2544 union_fragment_root: &mut PbStreamNode,
2545 upstream_sink_infos: &Vec<UpstreamSinkInfo>,
2546) {
2547 visit_stream_node_cont_mut(union_fragment_root, |node| {
2548 if let Some(NodeBody::UpstreamSinkUnion(upstream_sink_union)) = &mut node.node_body {
2549 let init_upstreams = upstream_sink_infos
2550 .iter()
2551 .map(|info| PbUpstreamSinkInfo {
2552 upstream_fragment_id: info.sink_fragment_id,
2553 sink_output_schema: info.sink_output_fields.clone(),
2554 project_exprs: info.project_exprs.clone(),
2555 })
2556 .collect();
2557 upstream_sink_union.init_upstreams = init_upstreams;
2558 false
2559 } else {
2560 true
2561 }
2562 });
2563}
2564
2565#[cfg(test)]
2566mod tests {
2567 use std::num::NonZeroUsize;
2568
2569 use super::*;
2570
2571 #[test]
2572 fn test_validate_specified_parallelism_accepts_within_max() {
2573 DdlController::validate_specified_parallelism(
2574 Some(NonZeroUsize::new(4).unwrap()),
2575 Some(NonZeroUsize::new(8).unwrap()),
2576 NonZeroUsize::new(8).unwrap(),
2577 )
2578 .unwrap();
2579 }
2580
2581 #[test]
2582 fn test_validate_specified_parallelism_rejects_parallelism_over_max() {
2583 let result = DdlController::validate_specified_parallelism(
2584 Some(NonZeroUsize::new(9).unwrap()),
2585 None,
2586 NonZeroUsize::new(8).unwrap(),
2587 );
2588 assert!(matches!(
2589 result,
2590 Err(ref e) if matches!(e.inner(), MetaErrorInner::InvalidParameter(_))
2591 ));
2592 }
2593
2594 #[test]
2595 fn test_validate_specified_parallelism_rejects_backfill_parallelism_over_max() {
2596 let result = DdlController::validate_specified_parallelism(
2597 None,
2598 Some(NonZeroUsize::new(9).unwrap()),
2599 NonZeroUsize::new(8).unwrap(),
2600 );
2601 assert!(matches!(
2602 result,
2603 Err(ref e) if matches!(e.inner(), MetaErrorInner::InvalidParameter(_))
2604 ));
2605 }
2606}