1feature_gated_sink_mod!(big_query, "bigquery");
16pub mod boxed;
17pub mod catalog;
18feature_gated_sink_mod!(clickhouse, ClickHouse, "clickhouse");
19pub mod coordinate;
20pub mod decouple_checkpoint_log_sink;
21feature_gated_sink_mod!(deltalake, DeltaLake, "deltalake");
22feature_gated_sink_mod!(doris, "doris");
23#[cfg(any(feature = "sink-doris", feature = "sink-starrocks"))]
24pub mod doris_starrocks_connector;
25pub mod dynamodb;
26pub mod elasticsearch_opensearch;
27pub mod encoder;
28pub mod file_sink;
29pub mod formatter;
30feature_gated_sink_mod!(google_pubsub, GooglePubSub, "google_pubsub");
31pub mod http;
32pub mod iceberg;
33pub mod kafka;
34pub mod kinesis;
35use risingwave_common::bail;
36use risingwave_pb::stream_plan::PbSinkSchemaChange;
37pub mod jdbc_jni_client;
38pub mod log_store;
39pub mod mock_coordination_client;
40pub mod mongodb;
41pub mod mqtt;
42pub mod nats;
43pub mod postgres;
44pub mod pulsar;
45pub mod redis;
46pub mod remote;
47pub mod snowflake_redshift;
48pub mod sqlserver;
49feature_gated_sink_mod!(starrocks, "starrocks");
50pub mod test_sink;
51pub mod trivial;
52pub mod turbopuffer;
53pub mod utils;
54pub mod writer;
55pub mod prelude {
56 pub use crate::sink::{
57 Result, SINK_TYPE_APPEND_ONLY, SINK_USER_FORCE_APPEND_ONLY_OPTION,
58 SINK_USER_FORCE_COMPACTION, Sink, SinkError, SinkParam, SinkWriterParam,
59 };
60}
61
62use std::collections::BTreeMap;
63use std::future::Future;
64use std::sync::{Arc, LazyLock};
65
66use ::redis::RedisError;
67use anyhow::anyhow;
68use async_trait::async_trait;
69use chrono_tz::{Tz, UTC};
70use decouple_checkpoint_log_sink::{
71 COMMIT_CHECKPOINT_INTERVAL, DEFAULT_COMMIT_CHECKPOINT_INTERVAL_WITH_SINK_DECOUPLE,
72 DEFAULT_COMMIT_CHECKPOINT_INTERVAL_WITHOUT_SINK_DECOUPLE,
73};
74use futures::future::BoxFuture;
75use opendal::Error as OpendalError;
76use prometheus::Registry;
77use risingwave_common::array::ArrayError;
78use risingwave_common::bitmap::Bitmap;
79use risingwave_common::catalog::{ColumnDesc, Field, Schema};
80use risingwave_common::config::StreamingConfig;
81use risingwave_common::hash::ActorId;
82use risingwave_common::metrics::{
83 LabelGuardedHistogram, LabelGuardedHistogramVec, LabelGuardedIntCounter,
84 LabelGuardedIntCounterVec, LabelGuardedIntGaugeVec,
85};
86use risingwave_common::monitor::GLOBAL_METRICS_REGISTRY;
87use risingwave_common::secret::{LocalSecretManager, SecretError};
88use risingwave_common::session_config::sink_decouple::SinkDecouple;
89use risingwave_common::{
90 register_guarded_histogram_vec_with_registry, register_guarded_int_counter_vec_with_registry,
91 register_guarded_int_gauge_vec_with_registry,
92};
93use risingwave_pb::catalog::PbSinkType;
94use risingwave_pb::connector_service::{PbSinkParam, SinkMetadata, TableSchema};
95use risingwave_pb::id::ExecutorId;
96use risingwave_rpc_client::MetaClient;
97use risingwave_rpc_client::error::RpcError;
98use starrocks::STARROCKS_SINK;
99use thiserror::Error;
100use thiserror_ext::AsReport;
101use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
102pub use tracing;
103
104use self::catalog::{SinkFormatDesc, SinkType};
105use self::clickhouse::CLICKHOUSE_SINK;
106use self::deltalake::DELTALAKE_SINK;
107use self::iceberg::ICEBERG_SINK;
108use self::mock_coordination_client::{MockMetaClient, SinkCoordinationRpcClientEnum};
109use crate::WithPropertiesExt;
110use crate::connector_common::IcebergSinkCompactionUpdate;
111use crate::error::{ConnectorError, ConnectorResult};
112use crate::sink::boxed::{BoxSinglePhaseCoordinator, BoxTwoPhaseCoordinator};
113use crate::sink::catalog::desc::SinkDesc;
114use crate::sink::catalog::{SinkCatalog, SinkId};
115use crate::sink::decouple_checkpoint_log_sink::ICEBERG_DEFAULT_COMMIT_CHECKPOINT_INTERVAL;
116use crate::sink::file_sink::fs::FsSink;
117use crate::sink::log_store::{LogReader, LogStoreReadItem, LogStoreResult, TruncateOffset};
118use crate::sink::snowflake_redshift::snowflake::SNOWFLAKE_SINK_V2;
119use crate::sink::utils::feature_gated_sink_mod;
120
121const BOUNDED_CHANNEL_SIZE: usize = 16;
122#[macro_export]
123macro_rules! for_all_sinks {
124 ($macro:path $(, $arg:tt)*) => {
125 $macro! {
126 {
127 { Redis, $crate::sink::redis::RedisSink, $crate::sink::redis::RedisConfig },
128 { Kafka, $crate::sink::kafka::KafkaSink, $crate::sink::kafka::KafkaConfig },
129 { Pulsar, $crate::sink::pulsar::PulsarSink, $crate::sink::pulsar::PulsarConfig },
130 { BlackHole, $crate::sink::trivial::BlackHoleSink, () },
131 { Http, $crate::sink::http::HttpSink, $crate::sink::http::HttpConfig },
132 { Turbopuffer, $crate::sink::turbopuffer::TurbopufferSink, $crate::sink::turbopuffer::TurbopufferConfig },
133 { Kinesis, $crate::sink::kinesis::KinesisSink, $crate::sink::kinesis::KinesisSinkConfig },
134 { ClickHouse, $crate::sink::clickhouse::ClickHouseSink, $crate::sink::clickhouse::ClickHouseConfig },
135 { Iceberg, $crate::sink::iceberg::IcebergSink, $crate::sink::iceberg::IcebergConfig },
136 { Mqtt, $crate::sink::mqtt::MqttSink, $crate::sink::mqtt::MqttConfig },
137 { GooglePubSub, $crate::sink::google_pubsub::GooglePubSubSink, $crate::sink::google_pubsub::GooglePubSubConfig },
138 { Nats, $crate::sink::nats::NatsSink, $crate::sink::nats::NatsConfig },
139 { Jdbc, $crate::sink::remote::JdbcSink, () },
140 { ElasticSearch, $crate::sink::elasticsearch_opensearch::elasticsearch::ElasticSearchSink, $crate::sink::elasticsearch_opensearch::elasticsearch_opensearch_config::ElasticSearchConfig },
141 { Opensearch, $crate::sink::elasticsearch_opensearch::opensearch::OpenSearchSink, $crate::sink::elasticsearch_opensearch::elasticsearch_opensearch_config::OpenSearchConfig },
142 { Cassandra, $crate::sink::remote::CassandraSink, () },
143 { Doris, $crate::sink::doris::DorisSink, $crate::sink::doris::DorisConfig },
144 { Starrocks, $crate::sink::starrocks::StarrocksSink, $crate::sink::starrocks::StarrocksConfig },
145 { S3, $crate::sink::file_sink::opendal_sink::FileSink<$crate::sink::file_sink::s3::S3Sink>, $crate::sink::file_sink::s3::S3Config },
146
147 { Gcs, $crate::sink::file_sink::opendal_sink::FileSink<$crate::sink::file_sink::gcs::GcsSink>, $crate::sink::file_sink::gcs::GcsConfig },
148 { Azblob, $crate::sink::file_sink::opendal_sink::FileSink<$crate::sink::file_sink::azblob::AzblobSink>, $crate::sink::file_sink::azblob::AzblobConfig },
149 { Webhdfs, $crate::sink::file_sink::opendal_sink::FileSink<$crate::sink::file_sink::webhdfs::WebhdfsSink>, $crate::sink::file_sink::webhdfs::WebhdfsConfig },
150
151 { Fs, $crate::sink::file_sink::opendal_sink::FileSink<FsSink>, $crate::sink::file_sink::fs::FsConfig },
152 { SnowflakeV2, $crate::sink::snowflake_redshift::snowflake::SnowflakeV2Sink, $crate::sink::snowflake_redshift::snowflake::SnowflakeV2Config },
153 { Snowflake, $crate::sink::file_sink::opendal_sink::FileSink<$crate::sink::file_sink::s3::SnowflakeSink>, $crate::sink::file_sink::s3::SnowflakeConfig },
154 { RedShift, $crate::sink::snowflake_redshift::redshift::RedshiftSink, $crate::sink::snowflake_redshift::redshift::RedShiftConfig },
155 { DeltaLake, $crate::sink::deltalake::DeltaLakeSink, $crate::sink::deltalake::DeltaLakeConfig },
156 { BigQuery, $crate::sink::big_query::BigQuerySink, $crate::sink::big_query::BigQueryConfig },
157 { DynamoDb, $crate::sink::dynamodb::DynamoDbSink, $crate::sink::dynamodb::DynamoDbConfig },
158 { Mongodb, $crate::sink::mongodb::MongodbSink, $crate::sink::mongodb::MongodbConfig },
159 { SqlServer, $crate::sink::sqlserver::SqlServerSink, $crate::sink::sqlserver::SqlServerConfig },
160 { Postgres, $crate::sink::postgres::PostgresSink, $crate::sink::postgres::PostgresConfig },
161
162 { Test, $crate::sink::test_sink::TestSink, () },
163 { Table, $crate::sink::trivial::TableSink, () }
164 }
165 $(,$arg)*
166 }
167 };
168}
169
170#[macro_export]
171macro_rules! generate_config_use_clauses {
172 ({$({ $variant_name:ident, $sink_type:ty, $($config_type:tt)+ }), *}) => {
173 $(
174 $crate::generate_config_use_single! { $($config_type)+ }
175 )*
176 };
177}
178
179#[macro_export]
180macro_rules! generate_config_use_single {
181 (()) => {};
183
184 ($config_type:path) => {
186 #[allow(unused_imports)]
187 pub(super) use $config_type;
188 };
189}
190
191#[macro_export]
193macro_rules! use_all_sink_configs {
194 () => {
195 $crate::for_all_sinks! { $crate::generate_config_use_clauses }
196 };
197}
198
199#[macro_export]
200macro_rules! dispatch_sink {
201 ({$({$variant_name:ident, $sink_type:ty, $config_type:ty}),*}, $impl:tt, $sink:tt, $body:tt) => {{
202 use $crate::sink::SinkImpl;
203
204 match $impl {
205 $(
206 SinkImpl::$variant_name($sink) => $body,
207 )*
208 }
209 }};
210 ($impl:expr, $sink:ident, $body:expr) => {{
211 $crate::for_all_sinks! {$crate::dispatch_sink, {$impl}, $sink, {$body}}
212 }};
213}
214
215#[macro_export]
216macro_rules! match_sink_name_str {
217 ({$({$variant_name:ident, $sink_type:ty, $config_type:ty}),*}, $name_str:tt, $type_name:ident, $body:tt, $on_other_closure:tt) => {{
218 use $crate::sink::Sink;
219 match $name_str {
220 $(
221 <$sink_type>::SINK_NAME => {
222 type $type_name = $sink_type;
223 {
224 $body
225 }
226 },
227 )*
228 other => ($on_other_closure)(other),
229 }
230 }};
231 ($name_str:expr, $type_name:ident, $body:expr, $on_other_closure:expr) => {{
232 $crate::for_all_sinks! {$crate::match_sink_name_str, {$name_str}, $type_name, {$body}, {$on_other_closure}}
233 }};
234}
235
236pub const CONNECTOR_TYPE_KEY: &str = "connector";
237pub const SINK_TYPE_OPTION: &str = "type";
238pub const SINK_SNAPSHOT_OPTION: &str = "snapshot";
240pub const SINK_TYPE_APPEND_ONLY: &str = "append-only";
241pub const SINK_TYPE_DEBEZIUM: &str = "debezium";
242pub const SINK_TYPE_UPSERT: &str = "upsert";
243pub const SINK_TYPE_RETRACT: &str = "retract";
244pub const SINK_USER_IGNORE_DELETE_OPTION: &str = "ignore_delete";
246pub const SINK_USER_FORCE_APPEND_ONLY_OPTION: &str = "force_append_only";
248pub const SINK_USER_FORCE_COMPACTION: &str = "force_compaction";
249pub const SINK_USER_PRESERVE_ROW_LEVEL_CHANGES: &str = "preserve_row_level_changes";
253
254#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct SinkParam {
256 pub sink_id: SinkId,
257 pub sink_name: String,
258 pub properties: BTreeMap<String, String>,
259 pub columns: Vec<ColumnDesc>,
260 pub downstream_pk: Option<Vec<usize>>,
262 pub sink_type: SinkType,
263 pub ignore_delete: bool,
265 pub format_desc: Option<SinkFormatDesc>,
266 pub db_name: String,
267
268 pub sink_from_name: String,
274}
275
276impl SinkParam {
277 pub fn from_proto(pb_param: PbSinkParam) -> Self {
278 let ignore_delete = pb_param.ignore_delete();
279 let table_schema = pb_param.table_schema.expect("should contain table schema");
280 let format_desc = match pb_param.format_desc {
281 Some(f) => f.try_into().ok(),
282 None => {
283 let connector = pb_param.properties.get(CONNECTOR_TYPE_KEY);
284 let r#type = pb_param.properties.get(SINK_TYPE_OPTION);
285 match (connector, r#type) {
286 (Some(c), Some(t)) => SinkFormatDesc::from_legacy_type(c, t).ok().flatten(),
287 _ => None,
288 }
289 }
290 };
291 Self {
292 sink_id: SinkId::from(pb_param.sink_id),
293 sink_name: pb_param.sink_name,
294 properties: pb_param.properties,
295 columns: table_schema.columns.iter().map(ColumnDesc::from).collect(),
296 downstream_pk: if table_schema.pk_indices.is_empty() {
297 None
298 } else {
299 Some(
300 (table_schema.pk_indices.iter())
301 .map(|i| *i as usize)
302 .collect(),
303 )
304 },
305 sink_type: SinkType::from_proto(
306 PbSinkType::try_from(pb_param.sink_type).expect("should be able to convert"),
307 ),
308 ignore_delete,
309 format_desc,
310 db_name: pb_param.db_name,
311 sink_from_name: pb_param.sink_from_name,
312 }
313 }
314
315 pub fn to_proto(&self) -> PbSinkParam {
316 PbSinkParam {
317 sink_id: self.sink_id,
318 sink_name: self.sink_name.clone(),
319 properties: self.properties.clone(),
320 table_schema: Some(TableSchema {
321 columns: self.columns.iter().map(|col| col.to_protobuf()).collect(),
322 pk_indices: (self.downstream_pk.as_ref())
323 .map_or_else(Vec::new, |pk| pk.iter().map(|i| *i as u32).collect()),
324 }),
325 sink_type: self.sink_type.to_proto().into(),
326 format_desc: self.format_desc.as_ref().map(|f| f.to_proto()),
327 db_name: self.db_name.clone(),
328 sink_from_name: self.sink_from_name.clone(),
329 raw_ignore_delete: self.ignore_delete,
330 }
331 }
332
333 pub fn schema(&self) -> Schema {
334 Schema {
335 fields: self.columns.iter().map(Field::from).collect(),
336 }
337 }
338
339 pub fn downstream_pk_or_empty(&self) -> Vec<usize> {
345 self.downstream_pk.clone().unwrap_or_default()
346 }
347
348 pub fn fill_secret_for_format_desc(
351 format_desc: Option<SinkFormatDesc>,
352 ) -> Result<Option<SinkFormatDesc>> {
353 match format_desc {
354 Some(mut format_desc) => {
355 format_desc.options = LocalSecretManager::global()
356 .fill_secrets(format_desc.options, format_desc.secret_refs.clone())?;
357 Ok(Some(format_desc))
358 }
359 None => Ok(None),
360 }
361 }
362
363 pub fn try_from_sink_catalog(sink_catalog: SinkCatalog) -> Result<Self> {
365 let columns = sink_catalog
366 .visible_columns()
367 .map(|col| col.column_desc.clone())
368 .collect();
369 let properties_with_secret = LocalSecretManager::global()
370 .fill_secrets(sink_catalog.properties, sink_catalog.secret_refs)?;
371 let format_desc_with_secret = Self::fill_secret_for_format_desc(sink_catalog.format_desc)?;
372 Ok(Self {
373 sink_id: sink_catalog.id,
374 sink_name: sink_catalog.name,
375 properties: properties_with_secret,
376 columns,
377 downstream_pk: sink_catalog.downstream_pk,
378 sink_type: sink_catalog.sink_type,
379 ignore_delete: sink_catalog.ignore_delete,
380 format_desc: format_desc_with_secret,
381 db_name: sink_catalog.db_name,
382 sink_from_name: sink_catalog.sink_from_name,
383 })
384 }
385}
386
387pub fn enforce_secret_sink(props: &impl WithPropertiesExt) -> ConnectorResult<()> {
388 use crate::enforce_secret::EnforceSecret;
389
390 let connector = props
391 .get_connector()
392 .ok_or_else(|| anyhow!("Must specify 'connector' in WITH clause"))?;
393 let key_iter = props.key_iter();
394 match_sink_name_str!(
395 connector.as_str(),
396 PropType,
397 PropType::enforce_secret(key_iter),
398 |other| bail!("connector '{}' is not supported", other)
399 )
400}
401
402pub static GLOBAL_SINK_METRICS: LazyLock<SinkMetrics> =
403 LazyLock::new(|| SinkMetrics::new(&GLOBAL_METRICS_REGISTRY));
404
405#[derive(Clone)]
406pub struct SinkMetrics {
407 pub sink_commit_duration: LabelGuardedHistogramVec,
408 pub connector_sink_rows_received: LabelGuardedIntCounterVec,
409
410 pub log_store_first_write_epoch: LabelGuardedIntGaugeVec,
412 pub log_store_latest_write_epoch: LabelGuardedIntGaugeVec,
413 pub log_store_write_rows: LabelGuardedIntCounterVec,
414
415 pub log_store_latest_read_epoch: LabelGuardedIntGaugeVec,
417 pub log_store_read_rows: LabelGuardedIntCounterVec,
418 pub log_store_read_bytes: LabelGuardedIntCounterVec,
419 pub log_store_reader_wait_new_future_duration_ns: LabelGuardedIntCounterVec,
420
421 pub iceberg_write_qps: LabelGuardedIntCounterVec,
423 pub iceberg_write_latency: LabelGuardedHistogramVec,
424 pub iceberg_rolling_unflushed_data_file: LabelGuardedIntGaugeVec,
425 pub iceberg_position_delete_cache_num: LabelGuardedIntGaugeVec,
426 pub iceberg_partition_num: LabelGuardedIntGaugeVec,
427 pub iceberg_write_bytes: LabelGuardedIntCounterVec,
428 pub iceberg_snapshot_num: LabelGuardedIntGaugeVec,
429}
430
431impl SinkMetrics {
432 pub fn new(registry: &Registry) -> Self {
433 let sink_commit_duration = register_guarded_histogram_vec_with_registry!(
434 "sink_commit_duration",
435 "Duration of commit op in sink",
436 &["actor_id", "connector", "sink_id", "sink_name"],
437 registry
438 )
439 .unwrap();
440
441 let connector_sink_rows_received = register_guarded_int_counter_vec_with_registry!(
442 "connector_sink_rows_received",
443 "Number of rows received by sink",
444 &["actor_id", "connector_type", "sink_id", "sink_name"],
445 registry
446 )
447 .unwrap();
448
449 let log_store_first_write_epoch = register_guarded_int_gauge_vec_with_registry!(
450 "log_store_first_write_epoch",
451 "The first write epoch of log store",
452 &["actor_id", "sink_id", "sink_name"],
453 registry
454 )
455 .unwrap();
456
457 let log_store_latest_write_epoch = register_guarded_int_gauge_vec_with_registry!(
458 "log_store_latest_write_epoch",
459 "The latest write epoch of log store",
460 &["actor_id", "sink_id", "sink_name"],
461 registry
462 )
463 .unwrap();
464
465 let log_store_write_rows = register_guarded_int_counter_vec_with_registry!(
466 "log_store_write_rows",
467 "The write rate of rows",
468 &["actor_id", "sink_id", "sink_name"],
469 registry
470 )
471 .unwrap();
472
473 let log_store_latest_read_epoch = register_guarded_int_gauge_vec_with_registry!(
474 "log_store_latest_read_epoch",
475 "The latest read epoch of log store",
476 &["actor_id", "connector", "sink_id", "sink_name"],
477 registry
478 )
479 .unwrap();
480
481 let log_store_read_rows = register_guarded_int_counter_vec_with_registry!(
482 "log_store_read_rows",
483 "The read rate of rows",
484 &["actor_id", "connector", "sink_id", "sink_name"],
485 registry
486 )
487 .unwrap();
488
489 let log_store_read_bytes = register_guarded_int_counter_vec_with_registry!(
490 "log_store_read_bytes",
491 "Total size of chunks read by log reader",
492 &["actor_id", "connector", "sink_id", "sink_name"],
493 registry
494 )
495 .unwrap();
496
497 let log_store_reader_wait_new_future_duration_ns =
498 register_guarded_int_counter_vec_with_registry!(
499 "log_store_reader_wait_new_future_duration_ns",
500 "Accumulated duration of LogReader to wait for next call to create future",
501 &["actor_id", "connector", "sink_id", "sink_name"],
502 registry
503 )
504 .unwrap();
505
506 let iceberg_write_qps = register_guarded_int_counter_vec_with_registry!(
507 "iceberg_write_qps",
508 "The qps of iceberg writer",
509 &["actor_id", "sink_id", "sink_name"],
510 registry
511 )
512 .unwrap();
513
514 let iceberg_write_latency = register_guarded_histogram_vec_with_registry!(
515 "iceberg_write_latency",
516 "The latency of iceberg writer",
517 &["actor_id", "sink_id", "sink_name"],
518 registry
519 )
520 .unwrap();
521
522 let iceberg_rolling_unflushed_data_file = register_guarded_int_gauge_vec_with_registry!(
523 "iceberg_rolling_unflushed_data_file",
524 "The unflushed data file count of iceberg rolling writer",
525 &["actor_id", "sink_id", "sink_name"],
526 registry
527 )
528 .unwrap();
529
530 let iceberg_position_delete_cache_num = register_guarded_int_gauge_vec_with_registry!(
531 "iceberg_position_delete_cache_num",
532 "The delete cache num of iceberg position delete writer",
533 &["actor_id", "sink_id", "sink_name"],
534 registry
535 )
536 .unwrap();
537
538 let iceberg_partition_num = register_guarded_int_gauge_vec_with_registry!(
539 "iceberg_partition_num",
540 "The partition num of iceberg partition writer",
541 &["actor_id", "sink_id", "sink_name"],
542 registry
543 )
544 .unwrap();
545
546 let iceberg_write_bytes = register_guarded_int_counter_vec_with_registry!(
547 "iceberg_write_bytes",
548 "The write bytes of iceberg writer",
549 &["actor_id", "sink_id", "sink_name"],
550 registry
551 )
552 .unwrap();
553
554 let iceberg_snapshot_num = register_guarded_int_gauge_vec_with_registry!(
555 "iceberg_snapshot_num",
556 "The snapshot number of iceberg table",
557 &["sink_name", "catalog_name", "table_name"],
558 registry
559 )
560 .unwrap();
561
562 Self {
563 sink_commit_duration,
564 connector_sink_rows_received,
565 log_store_first_write_epoch,
566 log_store_latest_write_epoch,
567 log_store_write_rows,
568 log_store_latest_read_epoch,
569 log_store_read_rows,
570 log_store_read_bytes,
571 log_store_reader_wait_new_future_duration_ns,
572 iceberg_write_qps,
573 iceberg_write_latency,
574 iceberg_rolling_unflushed_data_file,
575 iceberg_position_delete_cache_num,
576 iceberg_partition_num,
577 iceberg_write_bytes,
578 iceberg_snapshot_num,
579 }
580 }
581}
582
583#[derive(Clone)]
584pub struct SinkWriterParam {
585 pub executor_id: ExecutorId,
587 pub vnode_bitmap: Option<Bitmap>,
588 pub meta_client: Option<SinkMetaClient>,
589 pub extra_partition_col_idx: Option<usize>,
594
595 pub actor_id: ActorId,
596 pub sink_id: SinkId,
597 pub sink_name: String,
598 pub connector: String,
599 pub streaming_config: StreamingConfig,
600 pub time_zone: Tz,
601}
602
603#[derive(Clone)]
604pub struct SinkWriterMetrics {
605 pub sink_commit_duration: LabelGuardedHistogram,
606 pub connector_sink_rows_received: LabelGuardedIntCounter,
607}
608
609impl SinkWriterMetrics {
610 pub fn new(writer_param: &SinkWriterParam) -> Self {
611 let labels = [
612 &writer_param.actor_id.to_string(),
613 writer_param.connector.as_str(),
614 &writer_param.sink_id.to_string(),
615 writer_param.sink_name.as_str(),
616 ];
617 let sink_commit_duration = GLOBAL_SINK_METRICS
618 .sink_commit_duration
619 .with_guarded_label_values(&labels);
620 let connector_sink_rows_received = GLOBAL_SINK_METRICS
621 .connector_sink_rows_received
622 .with_guarded_label_values(&labels);
623 Self {
624 sink_commit_duration,
625 connector_sink_rows_received,
626 }
627 }
628
629 #[cfg(test)]
630 pub fn for_test() -> Self {
631 Self {
632 sink_commit_duration: LabelGuardedHistogram::test_histogram::<4>(),
633 connector_sink_rows_received: LabelGuardedIntCounter::test_int_counter::<4>(),
634 }
635 }
636}
637
638#[derive(Clone)]
639pub enum SinkMetaClient {
640 MetaClient(MetaClient),
641 MockMetaClient(MockMetaClient),
642}
643
644impl SinkMetaClient {
645 pub async fn sink_coordinate_client(&self) -> SinkCoordinationRpcClientEnum {
646 match self {
647 SinkMetaClient::MetaClient(meta_client) => {
648 SinkCoordinationRpcClientEnum::SinkCoordinationRpcClient(
649 meta_client.sink_coordinate_client().await,
650 )
651 }
652 SinkMetaClient::MockMetaClient(mock_meta_client) => {
653 SinkCoordinationRpcClientEnum::MockSinkCoordinationRpcClient(
654 mock_meta_client.sink_coordinate_client(),
655 )
656 }
657 }
658 }
659
660 pub async fn add_sink_fail_evet_log(
661 &self,
662 sink_id: SinkId,
663 sink_name: String,
664 connector: String,
665 error: String,
666 ) {
667 match self {
668 SinkMetaClient::MetaClient(meta_client) => {
669 match meta_client
670 .add_sink_fail_evet(sink_id, sink_name, connector, error)
671 .await
672 {
673 Ok(_) => {}
674 Err(e) => {
675 tracing::warn!(error = %e.as_report(), %sink_id, "Failed to add sink fail event to event log.");
676 }
677 }
678 }
679 SinkMetaClient::MockMetaClient(_) => {}
680 }
681 }
682}
683
684impl SinkWriterParam {
685 pub fn for_test() -> Self {
686 SinkWriterParam {
687 executor_id: Default::default(),
688 vnode_bitmap: Default::default(),
689 meta_client: Default::default(),
690 extra_partition_col_idx: Default::default(),
691
692 actor_id: 1.into(),
693 sink_id: SinkId::new(1),
694 sink_name: "test_sink".to_owned(),
695 connector: "test_connector".to_owned(),
696 streaming_config: StreamingConfig::default(),
697 time_zone: UTC,
698 }
699 }
700}
701
702fn is_sink_support_commit_checkpoint_interval(sink_name: &str) -> bool {
703 matches!(
704 sink_name,
705 ICEBERG_SINK | CLICKHOUSE_SINK | STARROCKS_SINK | DELTALAKE_SINK | SNOWFLAKE_SINK_V2
706 )
707}
708pub trait Sink: TryFrom<SinkParam, Error = SinkError> {
709 const SINK_NAME: &'static str;
710
711 type LogSinker: LogSinker;
712
713 fn set_default_commit_checkpoint_interval(
714 desc: &mut SinkDesc,
715 user_specified: &SinkDecouple,
716 ) -> Result<()> {
717 if is_sink_support_commit_checkpoint_interval(Self::SINK_NAME) {
718 match desc.properties.get(COMMIT_CHECKPOINT_INTERVAL) {
719 Some(commit_checkpoint_interval) => {
720 let commit_checkpoint_interval = commit_checkpoint_interval
721 .parse::<u64>()
722 .map_err(|e| SinkError::Config(anyhow!(e)))?;
723 if matches!(user_specified, SinkDecouple::Disable)
724 && commit_checkpoint_interval > 1
725 {
726 return Err(SinkError::Config(anyhow!(
727 "config conflict: `commit_checkpoint_interval` larger than 1 means that sink decouple must be enabled, but session config sink_decouple is disabled"
728 )));
729 }
730 }
731 None => match user_specified {
732 SinkDecouple::Default | SinkDecouple::Enable => {
733 if matches!(Self::SINK_NAME, ICEBERG_SINK) {
734 desc.properties.insert(
735 COMMIT_CHECKPOINT_INTERVAL.to_owned(),
736 ICEBERG_DEFAULT_COMMIT_CHECKPOINT_INTERVAL.to_string(),
737 );
738 } else {
739 desc.properties.insert(
740 COMMIT_CHECKPOINT_INTERVAL.to_owned(),
741 DEFAULT_COMMIT_CHECKPOINT_INTERVAL_WITH_SINK_DECOUPLE.to_string(),
742 );
743 }
744 }
745 SinkDecouple::Disable => {
746 desc.properties.insert(
747 COMMIT_CHECKPOINT_INTERVAL.to_owned(),
748 DEFAULT_COMMIT_CHECKPOINT_INTERVAL_WITHOUT_SINK_DECOUPLE.to_string(),
749 );
750 }
751 },
752 }
753 }
754 Ok(())
755 }
756
757 fn is_sink_decouple(user_specified: &SinkDecouple) -> Result<bool> {
759 match user_specified {
760 SinkDecouple::Default | SinkDecouple::Enable => Ok(true),
761 SinkDecouple::Disable => Ok(false),
762 }
763 }
764
765 fn support_schema_change() -> bool {
766 false
767 }
768
769 fn validate_alter_config(_config: &BTreeMap<String, String>) -> Result<()> {
770 Ok(())
771 }
772
773 async fn validate(&self) -> Result<()>;
774 async fn new_log_sinker(&self, writer_param: SinkWriterParam) -> Result<Self::LogSinker>;
775
776 fn is_coordinated_sink(&self) -> bool {
777 false
778 }
779
780 async fn new_coordinator(
781 &self,
782 _iceberg_compact_stat_sender: Option<UnboundedSender<IcebergSinkCompactionUpdate>>,
783 ) -> Result<SinkCommitCoordinator> {
784 Err(SinkError::Coordinator(anyhow!("no coordinator")))
785 }
786}
787
788pub trait SinkLogReader: Send {
789 fn start_from(
790 &mut self,
791 start_offset: Option<u64>,
792 ) -> impl Future<Output = LogStoreResult<()>> + Send + '_;
793 fn next_item(
797 &mut self,
798 ) -> impl Future<Output = LogStoreResult<(u64, LogStoreReadItem)>> + Send + '_;
799
800 fn truncate(&mut self, offset: TruncateOffset) -> LogStoreResult<()>;
803}
804
805impl<R: LogReader> SinkLogReader for &mut R {
806 fn next_item(
807 &mut self,
808 ) -> impl Future<Output = LogStoreResult<(u64, LogStoreReadItem)>> + Send + '_ {
809 <R as LogReader>::next_item(*self)
810 }
811
812 fn truncate(&mut self, offset: TruncateOffset) -> LogStoreResult<()> {
813 <R as LogReader>::truncate(*self, offset)
814 }
815
816 fn start_from(
817 &mut self,
818 start_offset: Option<u64>,
819 ) -> impl Future<Output = LogStoreResult<()>> + Send + '_ {
820 <R as LogReader>::start_from(*self, start_offset)
821 }
822}
823
824#[async_trait]
825pub trait LogSinker: 'static + Send {
826 async fn consume_log_and_sink(self, log_reader: impl SinkLogReader) -> Result<!>;
828}
829pub type SinkCommittedEpochSubscriber = Arc<
830 dyn Fn(SinkId) -> BoxFuture<'static, Result<(u64, UnboundedReceiver<u64>)>>
831 + Send
832 + Sync
833 + 'static,
834>;
835
836pub enum SinkCommitCoordinator {
837 SinglePhase(BoxSinglePhaseCoordinator),
838 TwoPhase(BoxTwoPhaseCoordinator),
839}
840
841#[async_trait]
842pub trait SinglePhaseCommitCoordinator {
843 async fn init(&mut self) -> Result<()>;
845
846 async fn commit_data(&mut self, epoch: u64, metadata: Vec<SinkMetadata>) -> Result<()>;
848
849 async fn commit_schema_change(
852 &mut self,
853 _epoch: u64,
854 _schema_change: PbSinkSchemaChange,
855 ) -> Result<()> {
856 Err(SinkError::Coordinator(anyhow!(
857 "Schema change is not implemented for single-phase commit coordinator {}",
858 std::any::type_name::<Self>()
859 )))
860 }
861}
862
863#[async_trait]
864pub trait TwoPhaseCommitCoordinator {
865 async fn init(&mut self) -> Result<()>;
867
868 async fn pre_commit(
870 &mut self,
871 epoch: u64,
872 metadata: Vec<SinkMetadata>,
873 schema_change: Option<PbSinkSchemaChange>,
874 ) -> Result<Option<Vec<u8>>>;
875
876 async fn commit_data(&mut self, epoch: u64, commit_metadata: Vec<u8>) -> Result<()>;
878
879 async fn commit_schema_change(
882 &mut self,
883 _epoch: u64,
884 _schema_change: PbSinkSchemaChange,
885 ) -> Result<()> {
886 Err(SinkError::Coordinator(anyhow!(
887 "Schema change is not implemented for two-phase commit coordinator {}",
888 std::any::type_name::<Self>()
889 )))
890 }
891
892 async fn abort(&mut self, epoch: u64, commit_metadata: Vec<u8>);
894}
895
896impl SinkImpl {
897 pub fn new(mut param: SinkParam) -> Result<Self> {
898 const PRIVATE_LINK_TARGET_KEY: &str = "privatelink.targets";
899
900 param.properties.remove(PRIVATE_LINK_TARGET_KEY);
902
903 let sink_type = param
904 .properties
905 .get(CONNECTOR_TYPE_KEY)
906 .ok_or_else(|| SinkError::Config(anyhow!("missing config: {}", CONNECTOR_TYPE_KEY)))?;
907
908 let sink_type = sink_type.to_lowercase();
909 match_sink_name_str!(
910 sink_type.as_str(),
911 SinkType,
912 Ok(SinkType::try_from(param)?.into()),
913 |other| {
914 Err(SinkError::Config(anyhow!(
915 "unsupported sink connector {}",
916 other
917 )))
918 }
919 )
920 }
921
922 pub fn is_sink_into_table(&self) -> bool {
923 matches!(self, SinkImpl::Table(_))
924 }
925
926 pub fn is_blackhole(&self) -> bool {
927 matches!(self, SinkImpl::BlackHole(_))
928 }
929
930 pub fn is_coordinated_sink(&self) -> bool {
931 dispatch_sink!(self, sink, sink.is_coordinated_sink())
932 }
933}
934
935pub fn build_sink(param: SinkParam) -> Result<SinkImpl> {
936 SinkImpl::new(param)
937}
938
939macro_rules! def_sink_impl {
940 () => {
941 $crate::for_all_sinks! { def_sink_impl }
942 };
943 ({ $({ $variant_name:ident, $sink_type:ty, $config_type:ty }),* }) => {
944 #[derive(Debug)]
945 pub enum SinkImpl {
946 $(
947 $variant_name(Box<$sink_type>),
948 )*
949 }
950
951 $(
952 impl From<$sink_type> for SinkImpl {
953 fn from(sink: $sink_type) -> SinkImpl {
954 SinkImpl::$variant_name(Box::new(sink))
955 }
956 }
957 )*
958 };
959}
960
961def_sink_impl!();
962
963pub type Result<T> = std::result::Result<T, SinkError>;
964
965#[derive(Error, Debug)]
966pub enum SinkError {
967 #[error("Kafka error: {0}")]
968 Kafka(#[from] rdkafka::error::KafkaError),
969 #[error("Kinesis error: {0}")]
970 Kinesis(
971 #[source]
972 #[backtrace]
973 anyhow::Error,
974 ),
975 #[error("Remote sink error: {0}")]
976 Remote(
977 #[source]
978 #[backtrace]
979 anyhow::Error,
980 ),
981 #[error("Encode error: {0}")]
982 Encode(String),
983 #[error("Avro error: {0}")]
984 Avro(#[from] apache_avro::Error),
985 #[error("Iceberg error: {0}")]
986 Iceberg(
987 #[source]
988 #[backtrace]
989 anyhow::Error,
990 ),
991 #[error("config error: {0}")]
992 Config(
993 #[source]
994 #[backtrace]
995 anyhow::Error,
996 ),
997 #[error("coordinator error: {0}")]
998 Coordinator(
999 #[source]
1000 #[backtrace]
1001 anyhow::Error,
1002 ),
1003 #[error("ClickHouse error: {0}")]
1004 ClickHouse(String),
1005 #[error("Redis error: {0}")]
1006 Redis(String),
1007 #[error("Http error: {0}")]
1008 Http(
1009 #[source]
1010 #[backtrace]
1011 anyhow::Error,
1012 ),
1013 #[error("Mqtt error: {0}")]
1014 Mqtt(
1015 #[source]
1016 #[backtrace]
1017 anyhow::Error,
1018 ),
1019 #[error("Nats error: {0}")]
1020 Nats(
1021 #[source]
1022 #[backtrace]
1023 anyhow::Error,
1024 ),
1025 #[error("Google Pub/Sub error: {0}")]
1026 GooglePubSub(
1027 #[source]
1028 #[backtrace]
1029 anyhow::Error,
1030 ),
1031 #[error("Doris/Starrocks connect error: {0}")]
1032 DorisStarrocksConnect(
1033 #[source]
1034 #[backtrace]
1035 anyhow::Error,
1036 ),
1037 #[error("Doris error: {0}")]
1038 Doris(String),
1039 #[error("DeltaLake error: {0}")]
1040 DeltaLake(
1041 #[source]
1042 #[backtrace]
1043 anyhow::Error,
1044 ),
1045 #[error("ElasticSearch/OpenSearch error: {0}")]
1046 ElasticSearchOpenSearch(
1047 #[source]
1048 #[backtrace]
1049 anyhow::Error,
1050 ),
1051 #[error("Starrocks error: {0}")]
1052 Starrocks(String),
1053 #[error("File error: {0}")]
1054 File(String),
1055 #[error("Pulsar error: {0}")]
1056 Pulsar(
1057 #[source]
1058 #[backtrace]
1059 anyhow::Error,
1060 ),
1061 #[error(transparent)]
1062 Internal(
1063 #[from]
1064 #[backtrace]
1065 anyhow::Error,
1066 ),
1067 #[error("BigQuery error: {0}")]
1068 BigQuery(
1069 #[source]
1070 #[backtrace]
1071 anyhow::Error,
1072 ),
1073 #[error("DynamoDB error: {0}")]
1074 DynamoDb(
1075 #[source]
1076 #[backtrace]
1077 anyhow::Error,
1078 ),
1079 #[error("SQL Server error: {0}")]
1080 SqlServer(
1081 #[source]
1082 #[backtrace]
1083 anyhow::Error,
1084 ),
1085 #[error("Postgres error: {0}")]
1086 Postgres(
1087 #[source]
1088 #[backtrace]
1089 anyhow::Error,
1090 ),
1091 #[error(transparent)]
1092 Connector(
1093 #[from]
1094 #[backtrace]
1095 ConnectorError,
1096 ),
1097 #[error("Secret error: {0}")]
1098 Secret(
1099 #[from]
1100 #[backtrace]
1101 SecretError,
1102 ),
1103 #[error("Mongodb error: {0}")]
1104 Mongodb(
1105 #[source]
1106 #[backtrace]
1107 anyhow::Error,
1108 ),
1109 #[error("Redshift error: {0}")]
1110 Redshift(
1111 #[source]
1112 #[backtrace]
1113 anyhow::Error,
1114 ),
1115}
1116
1117#[allow(clippy::disallowed_types)]
1118impl From<::iceberg::Error> for SinkError {
1119 fn from(err: ::iceberg::Error) -> Self {
1120 SinkError::Iceberg(anyhow!(err))
1121 }
1122}
1123
1124impl From<sea_orm::DbErr> for SinkError {
1125 fn from(err: sea_orm::DbErr) -> Self {
1126 SinkError::Iceberg(anyhow!(err))
1127 }
1128}
1129
1130impl From<OpendalError> for SinkError {
1131 fn from(error: OpendalError) -> Self {
1132 SinkError::File(error.to_report_string())
1133 }
1134}
1135
1136impl From<parquet::errors::ParquetError> for SinkError {
1137 fn from(error: parquet::errors::ParquetError) -> Self {
1138 SinkError::File(error.to_report_string())
1139 }
1140}
1141
1142impl From<ArrayError> for SinkError {
1143 fn from(error: ArrayError) -> Self {
1144 SinkError::File(error.to_report_string())
1145 }
1146}
1147
1148impl From<RpcError> for SinkError {
1149 fn from(value: RpcError) -> Self {
1150 SinkError::Remote(anyhow!(value))
1151 }
1152}
1153
1154impl From<RedisError> for SinkError {
1155 fn from(value: RedisError) -> Self {
1156 SinkError::Redis(value.to_report_string())
1157 }
1158}
1159
1160impl From<tiberius::error::Error> for SinkError {
1161 fn from(err: tiberius::error::Error) -> Self {
1162 SinkError::SqlServer(anyhow!(err))
1163 }
1164}
1165
1166impl From<::elasticsearch::Error> for SinkError {
1167 fn from(err: ::elasticsearch::Error) -> Self {
1168 SinkError::ElasticSearchOpenSearch(anyhow!(err))
1169 }
1170}
1171
1172impl From<::opensearch::Error> for SinkError {
1173 fn from(err: ::opensearch::Error) -> Self {
1174 SinkError::ElasticSearchOpenSearch(anyhow!(err))
1175 }
1176}
1177
1178impl From<tokio_postgres::Error> for SinkError {
1179 fn from(err: tokio_postgres::Error) -> Self {
1180 SinkError::Postgres(anyhow!(err))
1181 }
1182}