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