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