1feature_gated_sink_mod!(big_query, "bigquery");
16pub mod batching_log_sink;
17pub mod boxed;
18pub mod catalog;
19feature_gated_sink_mod!(clickhouse, ClickHouse, "clickhouse");
20pub mod coordinate;
21pub mod decouple_checkpoint_log_sink;
22feature_gated_sink_mod!(deltalake, DeltaLake, "deltalake");
23feature_gated_sink_mod!(doris, "doris");
24#[cfg(any(feature = "sink-doris", feature = "sink-starrocks"))]
25pub mod doris_starrocks_connector;
26pub mod dynamodb;
27pub mod elasticsearch_opensearch;
28pub mod encoder;
29pub mod file_sink;
30pub mod formatter;
31feature_gated_sink_mod!(google_pubsub, GooglePubSub, "google_pubsub");
32feature_gated_sink_mod!(lancedb, LanceDb, "lancedb");
33pub mod http;
34pub mod iceberg;
35pub mod kafka;
36pub mod kinesis;
37use risingwave_common::bail;
38use risingwave_pb::stream_plan::PbSinkSchemaChange;
39pub mod jdbc_jni_client;
40pub mod log_store;
41pub mod mock_coordination_client;
42pub mod mongodb;
43pub mod mqtt;
44pub mod nats;
45pub mod postgres;
46pub mod pulsar;
47pub mod redis;
48pub mod remote;
49pub mod snowflake_redshift;
50pub mod sqlserver;
51feature_gated_sink_mod!(starrocks, "starrocks");
52pub mod test_sink;
53pub mod trivial;
54pub mod turbopuffer;
55pub mod utils;
56pub mod writer;
57pub mod prelude {
58 pub use crate::sink::{
59 Result, SINK_TYPE_APPEND_ONLY, SINK_USER_FORCE_APPEND_ONLY_OPTION,
60 SINK_USER_FORCE_COMPACTION, Sink, SinkError, SinkParam, SinkWriterParam,
61 };
62}
63
64use std::collections::{BTreeMap, HashMap};
65use std::future::Future;
66use std::sync::{Arc, LazyLock};
67
68use ::redis::RedisError;
69use anyhow::anyhow;
70use async_trait::async_trait;
71use chrono_tz::{Tz, UTC};
72use decouple_checkpoint_log_sink::{
73 COMMIT_CHECKPOINT_INTERVAL, DEFAULT_COMMIT_CHECKPOINT_INTERVAL_WITH_SINK_DECOUPLE,
74 DEFAULT_COMMIT_CHECKPOINT_INTERVAL_WITHOUT_SINK_DECOUPLE,
75};
76use futures::future::BoxFuture;
77use opendal::Error as OpendalError;
78use prometheus::Registry;
79use risingwave_common::array::ArrayError;
80use risingwave_common::bitmap::Bitmap;
81use risingwave_common::catalog::{ColumnDesc, Field, Schema};
82use risingwave_common::config::{MetricLevel, StreamingConfig};
83use risingwave_common::hash::{ActorId, VnodeBitmapExt};
84use risingwave_common::metrics::{
85 GaugeAggregation, IntGaugeVecRelabelExt, LabelGuardedHistogram, LabelGuardedIntCounter,
86 LabelGuardedIntGaugeVec, MetricVecRelabelExt, RelabeledAggregatedIntGaugeVec,
87 RelabeledGuardedHistogramVec, RelabeledGuardedIntCounterVec,
88};
89use risingwave_common::monitor::GLOBAL_METRICS_REGISTRY;
90use risingwave_common::secret::{LocalSecretManager, SecretError};
91use risingwave_common::session_config::sink_decouple::SinkDecouple;
92use risingwave_common::{
93 register_guarded_histogram_vec_with_registry, register_guarded_int_counter_vec_with_registry,
94 register_guarded_int_gauge_vec_with_registry,
95};
96use risingwave_pb::catalog::PbSinkType;
97use risingwave_pb::connector_service::{PbSinkParam, SinkMetadata, TableSchema};
98use risingwave_pb::id::ExecutorId;
99use risingwave_rpc_client::MetaClient;
100use risingwave_rpc_client::error::RpcError;
101use starrocks::STARROCKS_SINK;
102use thiserror::Error;
103use thiserror_ext::AsReport;
104use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
105pub use tracing;
106
107use self::catalog::{SinkFormatDesc, SinkType};
108use self::clickhouse::CLICKHOUSE_SINK;
109use self::deltalake::DELTALAKE_SINK;
110use self::iceberg::ICEBERG_SINK;
111use self::lancedb::LANCEDB_SINK;
112use self::mock_coordination_client::{MockMetaClient, SinkCoordinationRpcClientEnum};
113use crate::connector_common::IcebergSinkCompactionUpdate;
114use crate::error::{ConnectorError, ConnectorResult};
115use crate::sink::boxed::{BoxSinglePhaseCoordinator, BoxTwoPhaseCoordinator};
116use crate::sink::catalog::desc::SinkDesc;
117use crate::sink::catalog::{SinkCatalog, SinkId};
118use crate::sink::decouple_checkpoint_log_sink::ICEBERG_DEFAULT_COMMIT_CHECKPOINT_INTERVAL;
119use crate::sink::file_sink::fs::FsSink;
120use crate::sink::log_store::{LogReader, LogStoreReadItem, LogStoreResult, TruncateOffset};
121use crate::sink::snowflake_redshift::snowflake::SNOWFLAKE_SINK_V2;
122use crate::sink::utils::feature_gated_sink_mod;
123use crate::{WithPropertiesExt, connector_metrics_level};
124
125const BOUNDED_CHANNEL_SIZE: usize = 16;
126#[macro_export]
127macro_rules! for_all_sinks {
128 ($macro:path $(, $arg:tt)*) => {
129 $macro! {
130 {
131 { Redis, $crate::sink::redis::RedisSink, $crate::sink::redis::RedisConfig },
132 { Kafka, $crate::sink::kafka::KafkaSink, $crate::sink::kafka::KafkaConfig },
133 { Pulsar, $crate::sink::pulsar::PulsarSink, $crate::sink::pulsar::PulsarConfig },
134 { BlackHole, $crate::sink::trivial::BlackHoleSink, () },
135 { Http, $crate::sink::http::HttpSink, $crate::sink::http::HttpConfig },
136 { Turbopuffer, $crate::sink::turbopuffer::TurbopufferSink, $crate::sink::turbopuffer::TurbopufferConfig },
137 { Kinesis, $crate::sink::kinesis::KinesisSink, $crate::sink::kinesis::KinesisSinkConfig },
138 { ClickHouse, $crate::sink::clickhouse::ClickHouseSink, $crate::sink::clickhouse::ClickHouseConfig },
139 { Iceberg, $crate::sink::iceberg::IcebergSink, $crate::sink::iceberg::IcebergConfig },
140 { Mqtt, $crate::sink::mqtt::MqttSink, $crate::sink::mqtt::MqttConfig },
141 { GooglePubSub, $crate::sink::google_pubsub::GooglePubSubSink, $crate::sink::google_pubsub::GooglePubSubConfig },
142 { Nats, $crate::sink::nats::NatsSink, $crate::sink::nats::NatsConfig },
143 { Jdbc, $crate::sink::remote::JdbcSink, () },
144 { ElasticSearch, $crate::sink::elasticsearch_opensearch::elasticsearch::ElasticSearchSink, $crate::sink::elasticsearch_opensearch::elasticsearch_opensearch_config::ElasticSearchConfig },
145 { Opensearch, $crate::sink::elasticsearch_opensearch::opensearch::OpenSearchSink, $crate::sink::elasticsearch_opensearch::elasticsearch_opensearch_config::OpenSearchConfig },
146 { Cassandra, $crate::sink::remote::CassandraSink, () },
147 { Doris, $crate::sink::doris::DorisSink, $crate::sink::doris::DorisConfig },
148 { Starrocks, $crate::sink::starrocks::StarrocksSink, $crate::sink::starrocks::StarrocksConfig },
149 { S3, $crate::sink::file_sink::opendal_sink::FileSink<$crate::sink::file_sink::s3::S3Sink>, $crate::sink::file_sink::s3::S3Config },
150
151 { Gcs, $crate::sink::file_sink::opendal_sink::FileSink<$crate::sink::file_sink::gcs::GcsSink>, $crate::sink::file_sink::gcs::GcsConfig },
152 { Azblob, $crate::sink::file_sink::opendal_sink::FileSink<$crate::sink::file_sink::azblob::AzblobSink>, $crate::sink::file_sink::azblob::AzblobConfig },
153 { Webhdfs, $crate::sink::file_sink::opendal_sink::FileSink<$crate::sink::file_sink::webhdfs::WebhdfsSink>, $crate::sink::file_sink::webhdfs::WebhdfsConfig },
154
155 { Fs, $crate::sink::file_sink::opendal_sink::FileSink<FsSink>, $crate::sink::file_sink::fs::FsConfig },
156 { SnowflakeV2, $crate::sink::snowflake_redshift::snowflake::SnowflakeV2Sink, $crate::sink::snowflake_redshift::snowflake::SnowflakeV2Config },
157 { Snowflake, $crate::sink::file_sink::opendal_sink::FileSink<$crate::sink::file_sink::s3::SnowflakeSink>, $crate::sink::file_sink::s3::SnowflakeConfig },
158 { RedShift, $crate::sink::snowflake_redshift::redshift::RedshiftSink, $crate::sink::snowflake_redshift::redshift::RedShiftConfig },
159 { DeltaLake, $crate::sink::deltalake::DeltaLakeSink, $crate::sink::deltalake::DeltaLakeConfig },
160 { LanceDb, $crate::sink::lancedb::LanceDbSink, $crate::sink::lancedb::LanceDbConfig },
161 { BigQuery, $crate::sink::big_query::BigQuerySink, $crate::sink::big_query::BigQueryConfig },
162 { DynamoDb, $crate::sink::dynamodb::DynamoDbSink, $crate::sink::dynamodb::DynamoDbConfig },
163 { Mongodb, $crate::sink::mongodb::MongodbSink, $crate::sink::mongodb::MongodbConfig },
164 { SqlServer, $crate::sink::sqlserver::SqlServerSink, $crate::sink::sqlserver::SqlServerConfig },
165 { Postgres, $crate::sink::postgres::PostgresSink, $crate::sink::postgres::PostgresConfig },
166
167 { Test, $crate::sink::test_sink::TestSink, () },
168 { Table, $crate::sink::trivial::TableSink, () }
169 }
170 $(,$arg)*
171 }
172 };
173}
174
175#[macro_export]
176macro_rules! generate_config_use_clauses {
177 ({$({ $variant_name:ident, $sink_type:ty, $($config_type:tt)+ }), *}) => {
178 $(
179 $crate::generate_config_use_single! { $($config_type)+ }
180 )*
181 };
182}
183
184#[macro_export]
185macro_rules! generate_config_use_single {
186 (()) => {};
188
189 ($config_type:path) => {
191 #[allow(unused_imports)]
192 pub(super) use $config_type;
193 };
194}
195
196#[macro_export]
198macro_rules! use_all_sink_configs {
199 () => {
200 $crate::for_all_sinks! { $crate::generate_config_use_clauses }
201 };
202}
203
204#[macro_export]
205macro_rules! dispatch_sink {
206 ({$({$variant_name:ident, $sink_type:ty, $config_type:ty}),*}, $impl:tt, $sink:tt, $body:tt) => {{
207 use $crate::sink::SinkImpl;
208
209 match $impl {
210 $(
211 SinkImpl::$variant_name($sink) => $body,
212 )*
213 }
214 }};
215 ($impl:expr, $sink:ident, $body:expr) => {{
216 $crate::for_all_sinks! {$crate::dispatch_sink, {$impl}, $sink, {$body}}
217 }};
218}
219
220#[macro_export]
221macro_rules! match_sink_name_str {
222 ({$({$variant_name:ident, $sink_type:ty, $config_type:ty}),*}, $name_str:tt, $type_name:ident, $body:tt, $on_other_closure:tt) => {{
223 use $crate::sink::Sink;
224 match $name_str {
225 $(
226 <$sink_type>::SINK_NAME => {
227 type $type_name = $sink_type;
228 {
229 $body
230 }
231 },
232 )*
233 other => ($on_other_closure)(other),
234 }
235 }};
236 ($name_str:expr, $type_name:ident, $body:expr, $on_other_closure:expr) => {{
237 $crate::for_all_sinks! {$crate::match_sink_name_str, {$name_str}, $type_name, {$body}, {$on_other_closure}}
238 }};
239}
240
241pub const CONNECTOR_TYPE_KEY: &str = "connector";
242pub const SINK_TYPE_OPTION: &str = "type";
243pub const SINK_SNAPSHOT_OPTION: &str = "snapshot";
245pub const SINK_TYPE_APPEND_ONLY: &str = "append-only";
246pub const SINK_TYPE_DEBEZIUM: &str = "debezium";
247pub const SINK_TYPE_UPSERT: &str = "upsert";
248pub const SINK_TYPE_RETRACT: &str = "retract";
249pub const SINK_USER_IGNORE_DELETE_OPTION: &str = "ignore_delete";
251pub const SINK_USER_FORCE_APPEND_ONLY_OPTION: &str = "force_append_only";
253pub const SINK_USER_FORCE_COMPACTION: &str = "force_compaction";
254pub const SINK_USER_PRESERVE_ROW_LEVEL_CHANGES: &str = "preserve_row_level_changes";
258
259pub fn sink_is_exactly_once(properties: &BTreeMap<String, String>) -> Result<bool> {
264 let sink_type = properties
265 .get(CONNECTOR_TYPE_KEY)
266 .ok_or_else(|| SinkError::Config(anyhow!("missing config: {}", CONNECTOR_TYPE_KEY)))?
267 .to_lowercase();
268
269 match_sink_name_str!(
270 sink_type.as_str(),
271 SinkType,
272 SinkType::is_exactly_once(properties),
273 |other| Err(SinkError::Config(anyhow!(
274 "unsupported sink connector {}",
275 other
276 )))
277 )
278}
279
280pub trait UnknownFields {
281 fn unknown_fields(&self) -> HashMap<String, String>;
283}
284
285impl UnknownFields for () {
286 fn unknown_fields(&self) -> HashMap<String, String> {
287 HashMap::new()
288 }
289}
290
291impl UnknownFields for HashMap<String, String> {
292 fn unknown_fields(&self) -> HashMap<String, String> {
293 self.clone()
294 }
295}
296
297#[macro_export]
298macro_rules! impl_sink_unknown_fields {
299 ($config_type:ty) => {
300 impl $crate::sink::UnknownFields for $config_type {
301 fn unknown_fields(&self) -> std::collections::HashMap<String, String> {
302 self.unknown_fields.clone()
303 }
304 }
305 };
306}
307
308#[macro_export]
309macro_rules! impl_validate_sink_unknown_fields {
310 () => {
311 fn validate_unknown_fields(&self) -> $crate::sink::Result<()> {
312 $crate::sink::validate_sink_unknown_fields(&self.config)
313 }
314 };
315}
316
317pub fn validate_sink_unknown_fields(config: &impl UnknownFields) -> Result<()> {
318 let mut unknown_fields = config.unknown_fields();
319 for key in [
322 CONNECTOR_TYPE_KEY,
323 SINK_TYPE_OPTION,
324 SINK_SNAPSHOT_OPTION,
325 SINK_USER_IGNORE_DELETE_OPTION,
326 SINK_USER_FORCE_APPEND_ONLY_OPTION,
327 SINK_USER_FORCE_COMPACTION,
328 SINK_USER_PRESERVE_ROW_LEVEL_CHANGES,
329 "backfill_rate_limit",
330 "primary_key",
331 "sink_rate_limit",
332 ] {
333 unknown_fields.remove(key);
334 }
335 if unknown_fields.is_empty() {
336 Ok(())
337 } else {
338 Err(SinkError::Config(anyhow!(
339 "Unknown fields in the WITH clause: {:?}",
340 unknown_fields
341 )))
342 }
343}
344
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct SinkParam {
347 pub sink_id: SinkId,
348 pub sink_name: String,
349 pub properties: BTreeMap<String, String>,
350 pub columns: Vec<ColumnDesc>,
351 pub downstream_pk: Option<Vec<usize>>,
353 pub sink_type: SinkType,
354 pub ignore_delete: bool,
356 pub format_desc: Option<SinkFormatDesc>,
357 pub db_name: String,
358
359 pub sink_from_name: String,
365}
366
367impl SinkParam {
368 pub fn from_proto(pb_param: PbSinkParam) -> Self {
369 let ignore_delete = pb_param.ignore_delete();
370 let table_schema = pb_param.table_schema.expect("should contain table schema");
371 let format_desc = match pb_param.format_desc {
372 Some(f) => f.try_into().ok(),
373 None => {
374 let connector = pb_param.properties.get(CONNECTOR_TYPE_KEY);
375 let r#type = pb_param.properties.get(SINK_TYPE_OPTION);
376 match (connector, r#type) {
377 (Some(c), Some(t)) => SinkFormatDesc::from_legacy_type(c, t).ok().flatten(),
378 _ => None,
379 }
380 }
381 };
382 Self {
383 sink_id: SinkId::from(pb_param.sink_id),
384 sink_name: pb_param.sink_name,
385 properties: pb_param.properties,
386 columns: table_schema.columns.iter().map(ColumnDesc::from).collect(),
387 downstream_pk: if table_schema.pk_indices.is_empty() {
388 None
389 } else {
390 Some(
391 (table_schema.pk_indices.iter())
392 .map(|i| *i as usize)
393 .collect(),
394 )
395 },
396 sink_type: SinkType::from_proto(
397 PbSinkType::try_from(pb_param.sink_type).expect("should be able to convert"),
398 ),
399 ignore_delete,
400 format_desc,
401 db_name: pb_param.db_name,
402 sink_from_name: pb_param.sink_from_name,
403 }
404 }
405
406 pub fn to_proto(&self) -> PbSinkParam {
407 PbSinkParam {
408 sink_id: self.sink_id,
409 sink_name: self.sink_name.clone(),
410 properties: self.properties.clone(),
411 table_schema: Some(TableSchema {
412 columns: self.columns.iter().map(|col| col.to_protobuf()).collect(),
413 pk_indices: (self.downstream_pk.as_ref())
414 .map_or_else(Vec::new, |pk| pk.iter().map(|i| *i as u32).collect()),
415 }),
416 sink_type: self.sink_type.to_proto().into(),
417 format_desc: self.format_desc.as_ref().map(|f| f.to_proto()),
418 db_name: self.db_name.clone(),
419 sink_from_name: self.sink_from_name.clone(),
420 raw_ignore_delete: self.ignore_delete,
421 }
422 }
423
424 pub fn schema(&self) -> Schema {
425 Schema {
426 fields: self.columns.iter().map(Field::from).collect(),
427 }
428 }
429
430 pub fn downstream_pk_or_empty(&self) -> Vec<usize> {
436 self.downstream_pk.clone().unwrap_or_default()
437 }
438
439 pub fn fill_secret_for_format_desc(
442 format_desc: Option<SinkFormatDesc>,
443 ) -> Result<Option<SinkFormatDesc>> {
444 match format_desc {
445 Some(mut format_desc) => {
446 format_desc.options = LocalSecretManager::global()
447 .fill_secrets(format_desc.options, format_desc.secret_refs.clone())?;
448 Ok(Some(format_desc))
449 }
450 None => Ok(None),
451 }
452 }
453
454 pub fn try_from_sink_catalog(sink_catalog: SinkCatalog) -> Result<Self> {
456 let columns = sink_catalog
457 .visible_columns()
458 .map(|col| col.column_desc.clone())
459 .collect();
460 let properties_with_secret = LocalSecretManager::global()
461 .fill_secrets(sink_catalog.properties, sink_catalog.secret_refs)?;
462 let format_desc_with_secret = Self::fill_secret_for_format_desc(sink_catalog.format_desc)?;
463 Ok(Self {
464 sink_id: sink_catalog.id,
465 sink_name: sink_catalog.name,
466 properties: properties_with_secret,
467 columns,
468 downstream_pk: sink_catalog.downstream_pk,
469 sink_type: sink_catalog.sink_type,
470 ignore_delete: sink_catalog.ignore_delete,
471 format_desc: format_desc_with_secret,
472 db_name: sink_catalog.db_name,
473 sink_from_name: sink_catalog.sink_from_name,
474 })
475 }
476}
477
478pub fn enforce_secret_sink(props: &impl WithPropertiesExt) -> ConnectorResult<()> {
479 use crate::enforce_secret::EnforceSecret;
480
481 let connector = props
482 .get_connector()
483 .ok_or_else(|| anyhow!("Must specify 'connector' in WITH clause"))?;
484 let key_iter = props.key_iter();
485 match_sink_name_str!(
486 connector.as_str(),
487 PropType,
488 PropType::enforce_secret(key_iter),
489 |other| bail!("connector '{}' is not supported", other)
490 )
491}
492
493pub static GLOBAL_SINK_METRICS: LazyLock<SinkMetrics> =
494 LazyLock::new(|| SinkMetrics::new(&GLOBAL_METRICS_REGISTRY, connector_metrics_level()));
495
496#[derive(Clone)]
497pub struct SinkMetrics {
498 pub sink_commit_duration: RelabeledGuardedHistogramVec,
499 pub connector_sink_rows_received: RelabeledGuardedIntCounterVec,
500
501 pub log_store_first_write_epoch: RelabeledAggregatedIntGaugeVec,
503 pub log_store_latest_write_epoch: RelabeledAggregatedIntGaugeVec,
504 pub log_store_write_rows: RelabeledGuardedIntCounterVec,
505
506 pub log_store_latest_read_epoch: RelabeledAggregatedIntGaugeVec,
508 pub log_store_read_rows: RelabeledGuardedIntCounterVec,
509 pub log_store_read_bytes: RelabeledGuardedIntCounterVec,
510 pub log_store_reader_wait_new_future_duration_ns: RelabeledGuardedIntCounterVec,
511
512 pub iceberg_write_qps: RelabeledGuardedIntCounterVec,
514 pub iceberg_write_latency: RelabeledGuardedHistogramVec,
515 pub iceberg_rolling_unflushed_data_file: RelabeledAggregatedIntGaugeVec,
516 pub iceberg_position_delete_cache_num: RelabeledAggregatedIntGaugeVec,
517 pub iceberg_partition_num: RelabeledAggregatedIntGaugeVec,
518 pub iceberg_write_bytes: RelabeledGuardedIntCounterVec,
519 pub iceberg_snapshot_num: LabelGuardedIntGaugeVec,
520}
521
522impl SinkMetrics {
523 pub fn new(registry: &Registry, metric_level: MetricLevel) -> Self {
524 let sink_commit_duration = register_guarded_histogram_vec_with_registry!(
525 "sink_commit_duration",
526 "Duration of commit op in sink",
527 &["actor_id", "connector", "sink_id", "sink_name"],
528 registry
529 )
530 .unwrap()
531 .relabel_debug_1(metric_level);
532
533 let connector_sink_rows_received = register_guarded_int_counter_vec_with_registry!(
534 "connector_sink_rows_received",
535 "Number of rows received by sink",
536 &["actor_id", "connector_type", "sink_id", "sink_name"],
537 registry
538 )
539 .unwrap()
540 .relabel_debug_1(metric_level);
541
542 let log_store_first_write_epoch = register_guarded_int_gauge_vec_with_registry!(
543 "log_store_first_write_epoch",
544 "The first write epoch of log store",
545 &["actor_id", "sink_id", "sink_name"],
546 registry
547 )
548 .unwrap()
549 .relabel_debug_1_with_aggregation(metric_level, GaugeAggregation::Min);
550
551 let log_store_latest_write_epoch = register_guarded_int_gauge_vec_with_registry!(
552 "log_store_latest_write_epoch",
553 "The latest write epoch of log store",
554 &["actor_id", "sink_id", "sink_name"],
555 registry
556 )
557 .unwrap()
558 .relabel_debug_1_with_aggregation(metric_level, GaugeAggregation::Min);
559
560 let log_store_write_rows = register_guarded_int_counter_vec_with_registry!(
561 "log_store_write_rows",
562 "The write rate of rows",
563 &["actor_id", "sink_id", "sink_name"],
564 registry
565 )
566 .unwrap()
567 .relabel_debug_1(metric_level);
568
569 let log_store_latest_read_epoch = register_guarded_int_gauge_vec_with_registry!(
570 "log_store_latest_read_epoch",
571 "The latest read epoch of log store",
572 &["actor_id", "connector", "sink_id", "sink_name"],
573 registry
574 )
575 .unwrap()
576 .relabel_debug_1_with_aggregation(metric_level, GaugeAggregation::Min);
577
578 let log_store_read_rows = register_guarded_int_counter_vec_with_registry!(
579 "log_store_read_rows",
580 "The read rate of rows",
581 &["actor_id", "connector", "sink_id", "sink_name"],
582 registry
583 )
584 .unwrap()
585 .relabel_debug_1(metric_level);
586
587 let log_store_read_bytes = register_guarded_int_counter_vec_with_registry!(
588 "log_store_read_bytes",
589 "Total size of chunks read by log reader",
590 &["actor_id", "connector", "sink_id", "sink_name"],
591 registry
592 )
593 .unwrap()
594 .relabel_debug_1(metric_level);
595
596 let log_store_reader_wait_new_future_duration_ns =
597 register_guarded_int_counter_vec_with_registry!(
598 "log_store_reader_wait_new_future_duration_ns",
599 "Accumulated duration of LogReader to wait for next call to create future",
600 &["actor_id", "connector", "sink_id", "sink_name"],
601 registry
602 )
603 .unwrap()
604 .relabel_debug_1(metric_level);
605
606 let iceberg_write_qps = register_guarded_int_counter_vec_with_registry!(
607 "iceberg_write_qps",
608 "The qps of iceberg writer",
609 &["actor_id", "sink_id", "sink_name"],
610 registry
611 )
612 .unwrap()
613 .relabel_debug_1(metric_level);
614
615 let iceberg_write_latency = register_guarded_histogram_vec_with_registry!(
616 "iceberg_write_latency",
617 "The latency of iceberg writer",
618 &["actor_id", "sink_id", "sink_name"],
619 registry
620 )
621 .unwrap()
622 .relabel_debug_1(metric_level);
623
624 let iceberg_rolling_unflushed_data_file = register_guarded_int_gauge_vec_with_registry!(
625 "iceberg_rolling_unflushed_data_file",
626 "The unflushed data file count of iceberg rolling writer",
627 &["actor_id", "sink_id", "sink_name"],
628 registry
629 )
630 .unwrap()
631 .relabel_debug_1_with_aggregation(metric_level, GaugeAggregation::Sum);
632
633 let iceberg_position_delete_cache_num = register_guarded_int_gauge_vec_with_registry!(
634 "iceberg_position_delete_cache_num",
635 "The delete cache num of iceberg position delete writer",
636 &["actor_id", "sink_id", "sink_name"],
637 registry
638 )
639 .unwrap()
640 .relabel_debug_1_with_aggregation(metric_level, GaugeAggregation::Sum);
641
642 let iceberg_partition_num = register_guarded_int_gauge_vec_with_registry!(
643 "iceberg_partition_num",
644 "The partition num of iceberg partition writer",
645 &["actor_id", "sink_id", "sink_name"],
646 registry
647 )
648 .unwrap()
649 .relabel_debug_1_with_aggregation(metric_level, GaugeAggregation::Sum);
650
651 let iceberg_write_bytes = register_guarded_int_counter_vec_with_registry!(
652 "iceberg_write_bytes",
653 "The write bytes of iceberg writer",
654 &["actor_id", "sink_id", "sink_name"],
655 registry
656 )
657 .unwrap()
658 .relabel_debug_1(metric_level);
659
660 let iceberg_snapshot_num = register_guarded_int_gauge_vec_with_registry!(
661 "iceberg_snapshot_num",
662 "The snapshot number of iceberg table",
663 &["sink_name", "catalog_name", "table_name"],
664 registry
665 )
666 .unwrap();
667
668 Self {
669 sink_commit_duration,
670 connector_sink_rows_received,
671 log_store_first_write_epoch,
672 log_store_latest_write_epoch,
673 log_store_write_rows,
674 log_store_latest_read_epoch,
675 log_store_read_rows,
676 log_store_read_bytes,
677 log_store_reader_wait_new_future_duration_ns,
678 iceberg_write_qps,
679 iceberg_write_latency,
680 iceberg_rolling_unflushed_data_file,
681 iceberg_position_delete_cache_num,
682 iceberg_partition_num,
683 iceberg_write_bytes,
684 iceberg_snapshot_num,
685 }
686 }
687}
688
689#[derive(Clone)]
690pub struct SinkWriterParam {
691 pub executor_id: ExecutorId,
693 pub vnode_bitmap: Bitmap,
694 pub meta_client: Option<SinkMetaClient>,
695 pub extra_partition_col_idx: Option<usize>,
700
701 pub actor_id: ActorId,
702 pub sink_id: SinkId,
703 pub sink_name: String,
704 pub connector: String,
705 pub streaming_config: StreamingConfig,
706 pub time_zone: Tz,
707}
708
709#[derive(Clone)]
710pub struct SinkWriterMetrics {
711 pub sink_commit_duration: LabelGuardedHistogram,
712 pub connector_sink_rows_received: LabelGuardedIntCounter,
713}
714
715impl SinkWriterMetrics {
716 pub fn new(writer_param: &SinkWriterParam) -> Self {
717 let labels = [
718 &writer_param.actor_id.to_string(),
719 writer_param.connector.as_str(),
720 &writer_param.sink_id.to_string(),
721 writer_param.sink_name.as_str(),
722 ];
723 let sink_commit_duration = GLOBAL_SINK_METRICS
724 .sink_commit_duration
725 .with_guarded_label_values(&labels);
726 let connector_sink_rows_received = GLOBAL_SINK_METRICS
727 .connector_sink_rows_received
728 .with_guarded_label_values(&labels);
729 Self {
730 sink_commit_duration,
731 connector_sink_rows_received,
732 }
733 }
734
735 #[cfg(test)]
736 pub fn for_test() -> Self {
737 Self {
738 sink_commit_duration: LabelGuardedHistogram::test_histogram::<4>(),
739 connector_sink_rows_received: LabelGuardedIntCounter::test_int_counter::<4>(),
740 }
741 }
742}
743
744#[derive(Clone)]
745pub enum SinkMetaClient {
746 MetaClient(MetaClient),
747 MockMetaClient(MockMetaClient),
748}
749
750impl SinkMetaClient {
751 pub async fn sink_coordinate_client(&self) -> SinkCoordinationRpcClientEnum {
752 match self {
753 SinkMetaClient::MetaClient(meta_client) => {
754 SinkCoordinationRpcClientEnum::SinkCoordinationRpcClient(
755 meta_client.sink_coordinate_client().await,
756 )
757 }
758 SinkMetaClient::MockMetaClient(mock_meta_client) => {
759 SinkCoordinationRpcClientEnum::MockSinkCoordinationRpcClient(
760 mock_meta_client.sink_coordinate_client(),
761 )
762 }
763 }
764 }
765
766 pub async fn add_sink_fail_evet_log(
767 &self,
768 sink_id: SinkId,
769 sink_name: String,
770 connector: String,
771 error: String,
772 ) {
773 match self {
774 SinkMetaClient::MetaClient(meta_client) => {
775 match meta_client
776 .add_sink_fail_evet(sink_id, sink_name, connector, error)
777 .await
778 {
779 Ok(_) => {}
780 Err(e) => {
781 tracing::warn!(
782 error = %e.as_report(),
783 %sink_id,
784 "failed to add the sink failure event to the event log",
785 );
786 }
787 }
788 }
789 SinkMetaClient::MockMetaClient(_) => {}
790 }
791 }
792}
793
794impl SinkWriterParam {
795 pub fn for_test() -> Self {
796 SinkWriterParam {
797 executor_id: Default::default(),
798 vnode_bitmap: Bitmap::singleton().clone(),
799 meta_client: Default::default(),
800 extra_partition_col_idx: Default::default(),
801
802 actor_id: 1.into(),
803 sink_id: SinkId::new(1),
804 sink_name: "test_sink".to_owned(),
805 connector: "test_connector".to_owned(),
806 streaming_config: StreamingConfig::default(),
807 time_zone: UTC,
808 }
809 }
810}
811
812fn is_sink_support_commit_checkpoint_interval(sink_name: &str) -> bool {
813 matches!(
814 sink_name,
815 ICEBERG_SINK
816 | CLICKHOUSE_SINK
817 | STARROCKS_SINK
818 | DELTALAKE_SINK
819 | SNOWFLAKE_SINK_V2
820 | LANCEDB_SINK
821 )
822}
823pub trait Sink: TryFrom<SinkParam, Error = SinkError> {
824 const SINK_NAME: &'static str;
825
826 type LogSinker: LogSinker;
827
828 fn is_exactly_once(_properties: &BTreeMap<String, String>) -> Result<bool> {
830 Ok(false)
831 }
832
833 fn set_default_commit_checkpoint_interval(
834 desc: &mut SinkDesc,
835 user_specified: &SinkDecouple,
836 ) -> Result<()> {
837 if is_sink_support_commit_checkpoint_interval(Self::SINK_NAME) {
838 match desc.properties.get(COMMIT_CHECKPOINT_INTERVAL) {
839 Some(commit_checkpoint_interval) => {
840 let commit_checkpoint_interval = commit_checkpoint_interval
841 .parse::<u64>()
842 .map_err(|e| SinkError::Config(anyhow!(e)))?;
843 if matches!(user_specified, SinkDecouple::Disable)
844 && commit_checkpoint_interval > 1
845 {
846 return Err(SinkError::Config(anyhow!(
847 "config conflict: `commit_checkpoint_interval` larger than 1 means that sink decouple must be enabled, but session config sink_decouple is disabled"
848 )));
849 }
850 }
851 None => match user_specified {
852 SinkDecouple::Default | SinkDecouple::Enable => {
853 if matches!(Self::SINK_NAME, ICEBERG_SINK) {
854 desc.properties.insert(
855 COMMIT_CHECKPOINT_INTERVAL.to_owned(),
856 ICEBERG_DEFAULT_COMMIT_CHECKPOINT_INTERVAL.to_string(),
857 );
858 } else {
859 desc.properties.insert(
860 COMMIT_CHECKPOINT_INTERVAL.to_owned(),
861 DEFAULT_COMMIT_CHECKPOINT_INTERVAL_WITH_SINK_DECOUPLE.to_string(),
862 );
863 }
864 }
865 SinkDecouple::Disable => {
866 desc.properties.insert(
867 COMMIT_CHECKPOINT_INTERVAL.to_owned(),
868 DEFAULT_COMMIT_CHECKPOINT_INTERVAL_WITHOUT_SINK_DECOUPLE.to_string(),
869 );
870 }
871 },
872 }
873 }
874 Ok(())
875 }
876
877 fn is_sink_decouple(user_specified: &SinkDecouple) -> Result<bool> {
879 match user_specified {
880 SinkDecouple::Default | SinkDecouple::Enable => Ok(true),
881 SinkDecouple::Disable => Ok(false),
882 }
883 }
884
885 fn support_schema_change() -> bool {
886 false
887 }
888
889 fn validate_alter_config_change(
894 config: &BTreeMap<String, String>,
895 _alter_props: &BTreeMap<String, String>,
896 ) -> Result<()> {
897 Self::validate_alter_config(config)
898 }
899
900 fn validate_alter_config(_config: &BTreeMap<String, String>) -> Result<()> {
901 Ok(())
902 }
903
904 fn validate_unknown_fields(&self) -> Result<()> {
905 Ok(())
906 }
907
908 async fn validate(&self) -> Result<()>;
909 async fn new_log_sinker(&self, writer_param: SinkWriterParam) -> Result<Self::LogSinker>;
910
911 fn is_coordinated_sink(&self) -> bool {
912 false
913 }
914
915 async fn new_coordinator(
916 &self,
917 _iceberg_compact_stat_sender: Option<UnboundedSender<IcebergSinkCompactionUpdate>>,
918 ) -> Result<SinkCommitCoordinator> {
919 Err(SinkError::Coordinator(anyhow!("no coordinator")))
920 }
921}
922
923pub trait SinkLogReader: Send {
924 fn start_from(
925 &mut self,
926 start_offset: Option<u64>,
927 ) -> impl Future<Output = LogStoreResult<()>> + Send + '_;
928 fn next_item(
932 &mut self,
933 ) -> impl Future<Output = LogStoreResult<(u64, LogStoreReadItem)>> + Send + '_;
934
935 fn truncate(&mut self, offset: TruncateOffset) -> LogStoreResult<()>;
938}
939
940impl<R: LogReader> SinkLogReader for &mut R {
941 fn next_item(
942 &mut self,
943 ) -> impl Future<Output = LogStoreResult<(u64, LogStoreReadItem)>> + Send + '_ {
944 <R as LogReader>::next_item(*self)
945 }
946
947 fn truncate(&mut self, offset: TruncateOffset) -> LogStoreResult<()> {
948 <R as LogReader>::truncate(*self, offset)
949 }
950
951 fn start_from(
952 &mut self,
953 start_offset: Option<u64>,
954 ) -> impl Future<Output = LogStoreResult<()>> + Send + '_ {
955 <R as LogReader>::start_from(*self, start_offset)
956 }
957}
958
959#[async_trait]
960pub trait LogSinker: 'static + Send {
961 async fn consume_log_and_sink(self, log_reader: impl SinkLogReader) -> Result<!>;
963}
964pub type SinkCommittedEpochSubscriber = Arc<
965 dyn Fn(SinkId) -> BoxFuture<'static, Result<(u64, UnboundedReceiver<u64>)>>
966 + Send
967 + Sync
968 + 'static,
969>;
970
971pub enum SinkCommitCoordinator {
972 SinglePhase(BoxSinglePhaseCoordinator),
973 TwoPhase(BoxTwoPhaseCoordinator),
974}
975
976#[async_trait]
977pub trait SinglePhaseCommitCoordinator {
978 async fn init(&mut self) -> Result<()>;
980
981 async fn commit_data(&mut self, epoch: u64, metadata: Vec<SinkMetadata>) -> Result<()>;
983
984 async fn commit_schema_change(
987 &mut self,
988 _epoch: u64,
989 _schema_change: PbSinkSchemaChange,
990 ) -> Result<()> {
991 Err(SinkError::Coordinator(anyhow!(
992 "Schema change is not implemented for single-phase commit coordinator {}",
993 std::any::type_name::<Self>()
994 )))
995 }
996}
997
998#[async_trait]
999pub trait TwoPhaseCommitCoordinator {
1000 async fn init(&mut self) -> Result<()>;
1002
1003 async fn pre_commit(
1005 &mut self,
1006 epoch: u64,
1007 metadata: Vec<SinkMetadata>,
1008 schema_change: Option<PbSinkSchemaChange>,
1009 ) -> Result<Option<Vec<u8>>>;
1010
1011 async fn commit_data(&mut self, epoch: u64, commit_metadata: Vec<u8>) -> Result<()>;
1013
1014 async fn commit_schema_change(
1017 &mut self,
1018 _epoch: u64,
1019 _schema_change: PbSinkSchemaChange,
1020 ) -> Result<()> {
1021 Err(SinkError::Coordinator(anyhow!(
1022 "Schema change is not implemented for two-phase commit coordinator {}",
1023 std::any::type_name::<Self>()
1024 )))
1025 }
1026
1027 async fn abort(&mut self, epoch: u64, commit_metadata: Vec<u8>);
1029}
1030
1031impl SinkImpl {
1032 pub fn new(mut param: SinkParam) -> Result<Self> {
1033 const PRIVATE_LINK_TARGET_KEY: &str = "privatelink.targets";
1034
1035 param.properties.remove(PRIVATE_LINK_TARGET_KEY);
1037
1038 let sink_type = param
1039 .properties
1040 .get(CONNECTOR_TYPE_KEY)
1041 .ok_or_else(|| SinkError::Config(anyhow!("missing config: {}", CONNECTOR_TYPE_KEY)))?;
1042
1043 let sink_type = sink_type.to_lowercase();
1044 match_sink_name_str!(
1045 sink_type.as_str(),
1046 SinkType,
1047 Ok(SinkType::try_from(param)?.into()),
1048 |other| {
1049 Err(SinkError::Config(anyhow!(
1050 "unsupported sink connector {}",
1051 other
1052 )))
1053 }
1054 )
1055 }
1056
1057 pub fn is_sink_into_table(&self) -> bool {
1058 matches!(self, SinkImpl::Table(_))
1059 }
1060
1061 pub fn is_blackhole(&self) -> bool {
1062 matches!(self, SinkImpl::BlackHole(_))
1063 }
1064
1065 pub fn is_coordinated_sink(&self) -> bool {
1066 dispatch_sink!(self, sink, sink.is_coordinated_sink())
1067 }
1068
1069 pub fn validate_unknown_fields(&self) -> Result<()> {
1070 dispatch_sink!(self, sink, sink.validate_unknown_fields())
1071 }
1072}
1073
1074pub fn build_sink(param: SinkParam) -> Result<SinkImpl> {
1075 SinkImpl::new(param)
1076}
1077
1078macro_rules! def_sink_impl {
1079 () => {
1080 $crate::for_all_sinks! { def_sink_impl }
1081 };
1082 ({ $({ $variant_name:ident, $sink_type:ty, $config_type:ty }),* }) => {
1083 #[derive(Debug)]
1084 pub enum SinkImpl {
1085 $(
1086 $variant_name(Box<$sink_type>),
1087 )*
1088 }
1089
1090 $(
1091 impl From<$sink_type> for SinkImpl {
1092 fn from(sink: $sink_type) -> SinkImpl {
1093 SinkImpl::$variant_name(Box::new(sink))
1094 }
1095 }
1096 )*
1097 };
1098}
1099
1100def_sink_impl!();
1101
1102pub type Result<T> = std::result::Result<T, SinkError>;
1103
1104#[derive(Error, Debug)]
1105pub enum SinkError {
1106 #[error("Kafka error: {0}")]
1107 Kafka(#[from] rdkafka::error::KafkaError),
1108 #[error("Kinesis error: {0}")]
1109 Kinesis(
1110 #[source]
1111 #[backtrace]
1112 anyhow::Error,
1113 ),
1114 #[error("Remote sink error: {0}")]
1115 Remote(
1116 #[source]
1117 #[backtrace]
1118 anyhow::Error,
1119 ),
1120 #[error("Encode error: {0}")]
1121 Encode(String),
1122 #[error("Avro error: {0}")]
1123 Avro(#[from] apache_avro::Error),
1124 #[error("Iceberg error: {0}")]
1125 Iceberg(
1126 #[source]
1127 #[backtrace]
1128 anyhow::Error,
1129 ),
1130 #[error("config error: {0}")]
1131 Config(
1132 #[source]
1133 #[backtrace]
1134 anyhow::Error,
1135 ),
1136 #[error("coordinator error: {0}")]
1137 Coordinator(
1138 #[source]
1139 #[backtrace]
1140 anyhow::Error,
1141 ),
1142 #[error("ClickHouse error: {0}")]
1143 ClickHouse(String),
1144 #[error("Redis error: {0}")]
1145 Redis(String),
1146 #[error("Http error: {0}")]
1147 Http(
1148 #[source]
1149 #[backtrace]
1150 anyhow::Error,
1151 ),
1152 #[error("Mqtt error: {0}")]
1153 Mqtt(
1154 #[source]
1155 #[backtrace]
1156 anyhow::Error,
1157 ),
1158 #[error("Nats error: {0}")]
1159 Nats(
1160 #[source]
1161 #[backtrace]
1162 anyhow::Error,
1163 ),
1164 #[error("Google Pub/Sub error: {0}")]
1165 GooglePubSub(
1166 #[source]
1167 #[backtrace]
1168 anyhow::Error,
1169 ),
1170 #[error("Doris/Starrocks connect error: {0}")]
1171 DorisStarrocksConnect(
1172 #[source]
1173 #[backtrace]
1174 anyhow::Error,
1175 ),
1176 #[error("Doris error: {0}")]
1177 Doris(String),
1178 #[error("DeltaLake error: {0}")]
1179 DeltaLake(
1180 #[source]
1181 #[backtrace]
1182 anyhow::Error,
1183 ),
1184 #[error("LanceDB error: {0}")]
1185 LanceDb(
1186 #[source]
1187 #[backtrace]
1188 anyhow::Error,
1189 ),
1190 #[error("ElasticSearch/OpenSearch error: {0}")]
1191 ElasticSearchOpenSearch(
1192 #[source]
1193 #[backtrace]
1194 anyhow::Error,
1195 ),
1196 #[error("Starrocks error: {0}")]
1197 Starrocks(String),
1198 #[error("File error: {0}")]
1199 File(String),
1200 #[error("Pulsar error: {0}")]
1201 Pulsar(
1202 #[source]
1203 #[backtrace]
1204 anyhow::Error,
1205 ),
1206 #[error(transparent)]
1207 Internal(
1208 #[from]
1209 #[backtrace]
1210 anyhow::Error,
1211 ),
1212 #[error("BigQuery error: {0}")]
1213 BigQuery(
1214 #[source]
1215 #[backtrace]
1216 anyhow::Error,
1217 ),
1218 #[error("DynamoDB error: {0}")]
1219 DynamoDb(
1220 #[source]
1221 #[backtrace]
1222 anyhow::Error,
1223 ),
1224 #[error("SQL Server error: {0}")]
1225 SqlServer(
1226 #[source]
1227 #[backtrace]
1228 anyhow::Error,
1229 ),
1230 #[error("Postgres error: {0}")]
1231 Postgres(
1232 #[source]
1233 #[backtrace]
1234 anyhow::Error,
1235 ),
1236 #[error(transparent)]
1237 Connector(
1238 #[from]
1239 #[backtrace]
1240 ConnectorError,
1241 ),
1242 #[error("Secret error: {0}")]
1243 Secret(
1244 #[from]
1245 #[backtrace]
1246 SecretError,
1247 ),
1248 #[error("Mongodb error: {0}")]
1249 Mongodb(
1250 #[source]
1251 #[backtrace]
1252 anyhow::Error,
1253 ),
1254 #[error("Redshift error: {0}")]
1255 Redshift(
1256 #[source]
1257 #[backtrace]
1258 anyhow::Error,
1259 ),
1260}
1261
1262#[allow(clippy::disallowed_types)]
1263impl From<::iceberg::Error> for SinkError {
1264 fn from(err: ::iceberg::Error) -> Self {
1265 SinkError::Iceberg(anyhow!(err))
1266 }
1267}
1268
1269impl From<sea_orm::DbErr> for SinkError {
1270 fn from(err: sea_orm::DbErr) -> Self {
1271 SinkError::Iceberg(anyhow!(err))
1272 }
1273}
1274
1275impl From<OpendalError> for SinkError {
1276 fn from(error: OpendalError) -> Self {
1277 SinkError::File(error.to_report_string())
1278 }
1279}
1280
1281impl From<parquet::errors::ParquetError> for SinkError {
1282 fn from(error: parquet::errors::ParquetError) -> Self {
1283 SinkError::File(error.to_report_string())
1284 }
1285}
1286
1287impl From<ArrayError> for SinkError {
1288 fn from(error: ArrayError) -> Self {
1289 SinkError::File(error.to_report_string())
1290 }
1291}
1292
1293impl From<RpcError> for SinkError {
1294 fn from(value: RpcError) -> Self {
1295 SinkError::Remote(anyhow!(value))
1296 }
1297}
1298
1299impl From<RedisError> for SinkError {
1300 fn from(value: RedisError) -> Self {
1301 SinkError::Redis(value.to_report_string())
1302 }
1303}
1304
1305impl From<tiberius::error::Error> for SinkError {
1306 fn from(err: tiberius::error::Error) -> Self {
1307 SinkError::SqlServer(anyhow!(err))
1308 }
1309}
1310
1311impl From<::elasticsearch::Error> for SinkError {
1312 fn from(err: ::elasticsearch::Error) -> Self {
1313 SinkError::ElasticSearchOpenSearch(anyhow!(err))
1314 }
1315}
1316
1317impl From<::opensearch::Error> for SinkError {
1318 fn from(err: ::opensearch::Error) -> Self {
1319 SinkError::ElasticSearchOpenSearch(anyhow!(err))
1320 }
1321}
1322
1323impl From<tokio_postgres::Error> for SinkError {
1324 fn from(err: tokio_postgres::Error) -> Self {
1325 SinkError::Postgres(anyhow!(err))
1326 }
1327}
1328
1329#[cfg(test)]
1330mod tests {
1331 use std::collections::BTreeMap;
1332
1333 use super::*;
1334
1335 fn assert_aggregation(
1336 gauge_vec: &RelabeledAggregatedIntGaugeVec,
1337 first_labels: &[&str],
1338 second_labels: &[&str],
1339 expected: i64,
1340 ) {
1341 let first = gauge_vec.with_guarded_label_values(first_labels);
1342 let second = gauge_vec.with_guarded_label_values(second_labels);
1343 first.set(5);
1344 second.set(7);
1345 assert_eq!(first.get(), expected);
1346 assert_eq!(second.get(), expected);
1347 }
1348
1349 fn btreemap<const N: usize>(entries: [(&str, &str); N]) -> BTreeMap<String, String> {
1350 entries
1351 .into_iter()
1352 .map(|(key, value)| (key.to_owned(), value.to_owned()))
1353 .collect()
1354 }
1355
1356 #[test]
1357 fn test_validate_sink_unknown_fields() {
1358 let config = crate::sink::redis::RedisConfig::from_btreemap(btreemap([
1359 (CONNECTOR_TYPE_KEY, "redis"),
1360 (SINK_TYPE_OPTION, SINK_TYPE_APPEND_ONLY),
1361 ("primary_key", "id"),
1362 (SINK_USER_FORCE_COMPACTION, "true"),
1363 (SINK_USER_PRESERVE_ROW_LEVEL_CHANGES, "true"),
1364 ("redis.url", "redis://127.0.0.1:6379"),
1365 ]))
1366 .unwrap();
1367 validate_sink_unknown_fields(&config).unwrap();
1368
1369 let config = crate::sink::redis::RedisConfig::from_btreemap(btreemap([
1370 (CONNECTOR_TYPE_KEY, "redis"),
1371 (SINK_TYPE_OPTION, SINK_TYPE_APPEND_ONLY),
1372 ("redis.url", "redis://127.0.0.1:6379"),
1373 ("bogus_with", "1"),
1374 ]))
1375 .unwrap();
1376 let err = validate_sink_unknown_fields(&config).unwrap_err();
1377 let report = err.to_report_string();
1378 assert!(report.contains("bogus_with"), "{report}");
1379
1380 let err = crate::sink::kafka::KafkaConfig::from_btreemap(btreemap([
1381 (CONNECTOR_TYPE_KEY, "kafka"),
1382 (SINK_TYPE_OPTION, SINK_TYPE_APPEND_ONLY),
1383 ]))
1384 .unwrap_err();
1385 assert!(err.to_report_string().contains("missing field `topic`"));
1386 }
1387
1388 #[test]
1389 fn test_sink_exactly_once_policy() {
1390 assert!(sink_is_exactly_once(&btreemap([(CONNECTOR_TYPE_KEY, ICEBERG_SINK)])).unwrap());
1391 assert!(
1392 !sink_is_exactly_once(&btreemap([
1393 (CONNECTOR_TYPE_KEY, ICEBERG_SINK),
1394 ("is_exactly_once", "false"),
1395 ]))
1396 .unwrap()
1397 );
1398
1399 #[cfg(feature = "sink-deltalake")]
1400 {
1401 assert!(
1402 !sink_is_exactly_once(&btreemap([(CONNECTOR_TYPE_KEY, DELTALAKE_SINK)])).unwrap()
1403 );
1404 assert!(
1405 sink_is_exactly_once(&btreemap([
1406 (CONNECTOR_TYPE_KEY, DELTALAKE_SINK),
1407 ("is_exactly_once", "true"),
1408 ]))
1409 .unwrap()
1410 );
1411 }
1412
1413 #[cfg(feature = "sink-lancedb")]
1414 assert!(sink_is_exactly_once(&btreemap([(CONNECTOR_TYPE_KEY, LANCEDB_SINK)])).unwrap());
1415
1416 let error = sink_is_exactly_once(&btreemap([
1417 (CONNECTOR_TYPE_KEY, ICEBERG_SINK),
1418 ("is_exactly_once", "invalid"),
1419 ]))
1420 .unwrap_err();
1421 assert!(error.to_report_string().contains("is_exactly_once"));
1422 }
1423
1424 #[test]
1425 fn actor_relabeled_gauges_use_the_assigned_reducers() {
1426 for level in [MetricLevel::Critical, MetricLevel::Info] {
1427 let registry = Registry::new();
1428 let metrics = SinkMetrics::new(®istry, level);
1429
1430 for gauge_vec in [
1431 &metrics.log_store_first_write_epoch,
1432 &metrics.log_store_latest_write_epoch,
1433 ] {
1434 assert_aggregation(gauge_vec, &["1", "sink", "name"], &["2", "sink", "name"], 5);
1435 }
1436 assert_aggregation(
1437 &metrics.log_store_latest_read_epoch,
1438 &["1", "connector", "sink", "name"],
1439 &["2", "connector", "sink", "name"],
1440 5,
1441 );
1442 for gauge_vec in [
1443 &metrics.iceberg_partition_num,
1444 &metrics.iceberg_position_delete_cache_num,
1445 &metrics.iceberg_rolling_unflushed_data_file,
1446 ] {
1447 assert_aggregation(
1448 gauge_vec,
1449 &["1", "sink", "name"],
1450 &["2", "sink", "name"],
1451 12,
1452 );
1453 }
1454 }
1455 }
1456}