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