risingwave_connector/sink/
kinesis.rs1use std::collections::BTreeMap;
16
17use anyhow::{Context, anyhow};
18use aws_sdk_kinesis::Client as KinesisClient;
19use aws_sdk_kinesis::operation::put_records::PutRecordsOutput;
20use aws_sdk_kinesis::primitives::Blob;
21use aws_sdk_kinesis::types::{PutRecordsRequestEntry, PutRecordsResultEntry};
22use futures::{FutureExt, TryFuture};
23use itertools::Itertools;
24use risingwave_common::array::StreamChunk;
25use risingwave_common::catalog::Schema;
26use serde::Deserialize;
27use serde_with::serde_as;
28use with_options::WithOptions;
29
30use super::SinkParam;
31use super::catalog::SinkFormatDesc;
32use crate::connector_common::KinesisCommon;
33use crate::dispatch_sink_formatter_str_key_impl;
34use crate::enforce_secret::EnforceSecret;
35use crate::sink::formatter::SinkFormatterImpl;
36use crate::sink::log_store::DeliveryFutureManagerAddFuture;
37use crate::sink::writer::{
38 AsyncTruncateLogSinkerOf, AsyncTruncateSinkWriter, AsyncTruncateSinkWriterExt, FormattedSink,
39};
40use crate::sink::{Result, Sink, SinkError, SinkWriterParam};
41pub const KINESIS_SINK: &str = "kinesis";
42
43#[derive(Clone, Debug)]
44pub struct KinesisSink {
45 pub config: KinesisSinkConfig,
46 schema: Schema,
47 pk_indices: Vec<usize>,
48 format_desc: SinkFormatDesc,
49 db_name: String,
50 sink_from_name: String,
51}
52
53impl EnforceSecret for KinesisSink {
54 fn enforce_secret<'a>(
55 prop_iter: impl Iterator<Item = &'a str>,
56 ) -> crate::error::ConnectorResult<()> {
57 for prop in prop_iter {
58 KinesisSinkConfig::enforce_one(prop)?;
59 }
60 Ok(())
61 }
62}
63
64impl TryFrom<SinkParam> for KinesisSink {
65 type Error = SinkError;
66
67 fn try_from(param: SinkParam) -> std::result::Result<Self, Self::Error> {
68 let schema = param.schema();
69 let pk_indices = param.downstream_pk_or_empty();
70 let config = KinesisSinkConfig::from_btreemap(param.properties)?;
71 Ok(Self {
72 config,
73 schema,
74 pk_indices,
75 format_desc: param
76 .format_desc
77 .ok_or_else(|| SinkError::Config(anyhow!("missing FORMAT ... ENCODE ...")))?,
78 db_name: param.db_name,
79 sink_from_name: param.sink_from_name,
80 })
81 }
82}
83
84const KINESIS_SINK_MAX_PENDING_CHUNK_NUM: usize = 64;
85
86impl Sink for KinesisSink {
87 type LogSinker = AsyncTruncateLogSinkerOf<KinesisSinkWriter>;
88
89 const SINK_NAME: &'static str = KINESIS_SINK;
90
91 crate::impl_validate_sink_unknown_fields!();
92
93 async fn validate(&self) -> Result<()> {
94 if self.pk_indices.is_empty() {
97 return Err(SinkError::Config(anyhow!(
98 "kinesis sink requires partition key (please define in `primary_key` field)",
99 )));
100 }
101 SinkFormatterImpl::new(
103 &self.format_desc,
104 self.schema.clone(),
105 self.pk_indices.clone(),
106 self.db_name.clone(),
107 self.sink_from_name.clone(),
108 &self.config.common.stream_name,
109 )
110 .await?;
111
112 let client = self.config.common.build_client().await?;
114 client
115 .list_shards()
116 .stream_name(&self.config.common.stream_name)
117 .send()
118 .await
119 .context("failed to list shards")
120 .map_err(SinkError::Kinesis)?;
121 Ok(())
122 }
123
124 async fn new_log_sinker(&self, _writer_param: SinkWriterParam) -> Result<Self::LogSinker> {
125 Ok(KinesisSinkWriter::new(
126 self.config.clone(),
127 self.schema.clone(),
128 self.pk_indices.clone(),
129 &self.format_desc,
130 self.db_name.clone(),
131 self.sink_from_name.clone(),
132 )
133 .await?
134 .into_log_sinker(KINESIS_SINK_MAX_PENDING_CHUNK_NUM))
135 }
136}
137
138#[serde_as]
139#[derive(Clone, Debug, Deserialize, WithOptions)]
140pub struct KinesisSinkConfig {
141 #[serde(flatten)]
142 pub common: KinesisCommon,
143
144 #[serde(flatten)]
145 pub unknown_fields: std::collections::HashMap<String, String>,
146}
147
148crate::impl_sink_unknown_fields!(KinesisSinkConfig);
149
150impl EnforceSecret for KinesisSinkConfig {
151 fn enforce_one(prop: &str) -> crate::error::ConnectorResult<()> {
152 KinesisCommon::enforce_one(prop)?;
153 Ok(())
154 }
155}
156
157impl KinesisSinkConfig {
158 pub fn from_btreemap(properties: BTreeMap<String, String>) -> Result<Self> {
159 let config =
160 serde_json::from_value::<KinesisSinkConfig>(serde_json::to_value(properties).unwrap())
161 .map_err(|e| SinkError::Config(anyhow!(e)))?;
162 Ok(config)
163 }
164}
165
166pub struct KinesisSinkWriter {
167 pub config: KinesisSinkConfig,
168 formatter: SinkFormatterImpl,
169 client: KinesisClient,
170}
171
172struct KinesisSinkPayloadWriter {
173 client: KinesisClient,
174 entries: Vec<(PutRecordsRequestEntry, usize)>,
175 stream_name: String,
176}
177
178impl KinesisSinkWriter {
179 pub async fn new(
180 config: KinesisSinkConfig,
181 schema: Schema,
182 pk_indices: Vec<usize>,
183 format_desc: &SinkFormatDesc,
184 db_name: String,
185 sink_from_name: String,
186 ) -> Result<Self> {
187 let formatter = SinkFormatterImpl::new(
188 format_desc,
189 schema,
190 pk_indices,
191 db_name,
192 sink_from_name,
193 &config.common.stream_name,
194 )
195 .await?;
196 let client = config
197 .common
198 .build_client()
199 .await
200 .map_err(|err| SinkError::Kinesis(anyhow!(err)))?;
201 Ok(Self {
202 config: config.clone(),
203 formatter,
204 client,
205 })
206 }
207
208 fn new_payload_writer(&self) -> KinesisSinkPayloadWriter {
209 KinesisSinkPayloadWriter {
210 client: self.client.clone(),
211 entries: vec![],
212 stream_name: self.config.common.stream_name.clone(),
213 }
214 }
215}
216
217mod opaque_type {
218 use std::cmp::min;
219 use std::time::Duration;
220
221 use risingwave_common::util::retry::exponential_backoff;
222 use thiserror_ext::AsReport;
223 use tokio::time::sleep;
224 use tokio_retry::strategy::jitter;
225 use tracing::warn;
226
227 use super::*;
228 pub type KinesisSinkPayloadWriterDeliveryFuture =
229 impl TryFuture<Ok = (), Error = SinkError> + Unpin + Send + 'static;
230
231 impl KinesisSinkPayloadWriter {
232 #[define_opaque(KinesisSinkPayloadWriterDeliveryFuture)]
233 pub(super) fn finish(self) -> KinesisSinkPayloadWriterDeliveryFuture {
234 async move {
238 const MAX_RECORD_COUNT: usize = 500;
244 const MAX_SINGLE_RECORD_PAYLOAD_SIZE: usize = 1 << 20;
245 const MAX_TOTAL_RECORD_PAYLOAD_SIZE: usize = 5 * (1 << 20);
246 const MAX_NO_PROGRESS_RETRY_COUNT: usize = 3;
248
249 let mut remaining_no_progress_retry_count = MAX_NO_PROGRESS_RETRY_COUNT;
250 let total_count = self.entries.len();
251 let mut start_idx = 0;
252
253 let mut throttle_delay = None;
254
255 while start_idx < total_count {
256 let max_record_count = min(MAX_RECORD_COUNT, total_count - start_idx);
261 let mut records = Vec::with_capacity(max_record_count);
262 let mut total_payload_size = 0;
263 for i in start_idx..(start_idx + max_record_count) {
264 let (record, size) = &self.entries[i];
265 if *size >= MAX_SINGLE_RECORD_PAYLOAD_SIZE {
266 warn!(
267 size,
268 partition = record.partition_key,
269 "encounter a large single record"
270 );
271 }
272 if total_payload_size + *size < MAX_TOTAL_RECORD_PAYLOAD_SIZE {
273 total_payload_size += *size;
274 records.push(record.clone());
275 } else {
276 break;
277 }
278 }
279 if records.is_empty() {
280 records.push(self.entries[start_idx].0.clone());
282 }
283
284 let record_count = records.len();
286 match self
287 .client
288 .put_records()
289 .stream_name(&self.stream_name)
290 .set_records(Some(records))
291 .send()
292 .await
293 {
294 Ok(output) => {
295 if record_count != output.records.len() {
296 return Err(SinkError::Kinesis(anyhow!("request record count {} not match the response record count {}", record_count, output.records.len())));
297 }
298 if let Some((first_failed_idx, result_entry)) = Self::first_failed_entry(output) {
306 let partially_sent_count = first_failed_idx;
308 if partially_sent_count > 0 {
309 warn!(
310 partially_sent_count,
311 record_count,
312 "records are partially sent. code: [{}], message: [{}]",
313 result_entry.error_code.unwrap_or_default(),
314 result_entry.error_message.unwrap_or_default()
315 );
316 start_idx += partially_sent_count;
317 remaining_no_progress_retry_count = MAX_NO_PROGRESS_RETRY_COUNT;
319 } else if let Some(err_code) = &result_entry.error_code && err_code == "ProvisionedThroughputExceededException" {
320 let throttle_delay = throttle_delay.get_or_insert_with(|| exponential_backoff(Duration::from_millis(100), 2, Duration::from_secs(2)).map(jitter)).next().expect("should not be none");
326 warn!(err_string = ?result_entry.error_message, ?throttle_delay, "throttle");
327 sleep(throttle_delay).await;
328 } else {
329 assert_eq!(first_failed_idx, 0);
331 remaining_no_progress_retry_count -= 1;
332 if remaining_no_progress_retry_count == 0 {
333 return Err(SinkError::Kinesis(anyhow!(
334 "failed to send records. sent {} out of {}, last err: code: [{}], message: [{}]",
335 start_idx,
336 total_count,
337 result_entry.error_code.unwrap_or_default(),
338 result_entry.error_message.unwrap_or_default()
339 )));
340 } else {
341 warn!(
342 remaining_no_progress_retry_count,
343 sent = start_idx,
344 total_count,
345 "failed to send records. code: [{}], message: [{}]",
346 result_entry.error_code.unwrap_or_default(),
347 result_entry.error_message.unwrap_or_default()
348 )
349 }
350 }
351 } else {
352 start_idx += record_count;
353 remaining_no_progress_retry_count = MAX_NO_PROGRESS_RETRY_COUNT;
355 throttle_delay = None;
357 }
358 }
359 Err(e) => {
360 remaining_no_progress_retry_count -= 1;
361 if remaining_no_progress_retry_count == 0 {
362 return Err(SinkError::Kinesis(anyhow!(e).context(format!(
363 "failed to send records. sent {} out of {}",
364 start_idx, total_count,
365 ))));
366 } else {
367 warn!(
368 remaining_no_progress_retry_count,
369 sent = start_idx,
370 total_count,
371 "failed to send records. err: [{:?}]",
372 e.as_report(),
373 )
374 }
375 }
376 }
377 }
378 Ok(())
379 }
380 .boxed()
381 }
382 }
383}
384pub use opaque_type::KinesisSinkPayloadWriterDeliveryFuture;
385
386impl KinesisSinkPayloadWriter {
387 fn first_failed_entry(output: PutRecordsOutput) -> Option<(usize, PutRecordsResultEntry)> {
388 output
398 .records
399 .into_iter()
400 .find_position(|entry| entry.shard_id.is_none())
401 }
402
403 fn put_record(&mut self, key: String, payload: Vec<u8>) {
404 let size = key.len() + payload.len();
405 self.entries.push((
406 PutRecordsRequestEntry::builder()
407 .partition_key(key)
408 .data(Blob::new(payload))
409 .build()
410 .expect("should not fail because we have set `data` and `partition_key`"),
411 size,
412 ))
413 }
414}
415
416impl FormattedSink for KinesisSinkPayloadWriter {
417 type K = String;
418 type V = Vec<u8>;
419
420 async fn write_one(&mut self, k: Option<Self::K>, v: Option<Self::V>) -> Result<()> {
421 self.put_record(
422 k.ok_or_else(|| SinkError::Kinesis(anyhow!("no key provided")))?,
423 v.unwrap_or_default(),
424 );
425 Ok(())
426 }
427}
428
429impl AsyncTruncateSinkWriter for KinesisSinkWriter {
430 type DeliveryFuture = KinesisSinkPayloadWriterDeliveryFuture;
431
432 async fn write_chunk<'a>(
433 &'a mut self,
434 chunk: StreamChunk,
435 mut add_future: DeliveryFutureManagerAddFuture<'a, Self::DeliveryFuture>,
436 ) -> Result<()> {
437 let mut payload_writer = self.new_payload_writer();
438 dispatch_sink_formatter_str_key_impl!(
439 &self.formatter,
440 formatter,
441 payload_writer.write_chunk(chunk, formatter).await
442 )?;
443
444 add_future
445 .add_future_may_await(payload_writer.finish())
446 .await?;
447 Ok(())
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use aws_sdk_kinesis::types::PutRecordsRequestEntry;
454 use aws_smithy_types::Blob;
455
456 #[test]
457 fn test_kinesis_entry_builder_save_unwrap() {
458 PutRecordsRequestEntry::builder()
459 .data(Blob::new(b"data"))
460 .partition_key("partition-key")
461 .build()
462 .unwrap();
463 }
464}