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 thiserror_ext::AsReport;
222 use tokio::time::sleep;
223 use tokio_retry::strategy::{ExponentialBackoff, jitter};
224 use tracing::warn;
225
226 use super::*;
227 pub type KinesisSinkPayloadWriterDeliveryFuture =
228 impl TryFuture<Ok = (), Error = SinkError> + Unpin + Send + 'static;
229
230 impl KinesisSinkPayloadWriter {
231 #[define_opaque(KinesisSinkPayloadWriterDeliveryFuture)]
232 pub(super) fn finish(self) -> KinesisSinkPayloadWriterDeliveryFuture {
233 async move {
237 const MAX_RECORD_COUNT: usize = 500;
243 const MAX_SINGLE_RECORD_PAYLOAD_SIZE: usize = 1 << 20;
244 const MAX_TOTAL_RECORD_PAYLOAD_SIZE: usize = 5 * (1 << 20);
245 const MAX_NO_PROGRESS_RETRY_COUNT: usize = 3;
247
248 let mut remaining_no_progress_retry_count = MAX_NO_PROGRESS_RETRY_COUNT;
249 let total_count = self.entries.len();
250 let mut start_idx = 0;
251
252 let mut throttle_delay = None;
253
254 while start_idx < total_count {
255 let max_record_count = min(MAX_RECORD_COUNT, total_count - start_idx);
260 let mut records = Vec::with_capacity(max_record_count);
261 let mut total_payload_size = 0;
262 for i in start_idx..(start_idx + max_record_count) {
263 let (record, size) = &self.entries[i];
264 if *size >= MAX_SINGLE_RECORD_PAYLOAD_SIZE {
265 warn!(
266 size,
267 partition = record.partition_key,
268 "encounter a large single record"
269 );
270 }
271 if total_payload_size + *size < MAX_TOTAL_RECORD_PAYLOAD_SIZE {
272 total_payload_size += *size;
273 records.push(record.clone());
274 } else {
275 break;
276 }
277 }
278 if records.is_empty() {
279 records.push(self.entries[start_idx].0.clone());
281 }
282
283 let record_count = records.len();
285 match self
286 .client
287 .put_records()
288 .stream_name(&self.stream_name)
289 .set_records(Some(records))
290 .send()
291 .await
292 {
293 Ok(output) => {
294 if record_count != output.records.len() {
295 return Err(SinkError::Kinesis(anyhow!("request record count {} not match the response record count {}", record_count, output.records.len())));
296 }
297 if let Some((first_failed_idx, result_entry)) = Self::first_failed_entry(output) {
305 let partially_sent_count = first_failed_idx;
307 if partially_sent_count > 0 {
308 warn!(
309 partially_sent_count,
310 record_count,
311 "records are partially sent. code: [{}], message: [{}]",
312 result_entry.error_code.unwrap_or_default(),
313 result_entry.error_message.unwrap_or_default()
314 );
315 start_idx += partially_sent_count;
316 remaining_no_progress_retry_count = MAX_NO_PROGRESS_RETRY_COUNT;
318 } else if let Some(err_code) = &result_entry.error_code && err_code == "ProvisionedThroughputExceededException" {
319 let throttle_delay = throttle_delay.get_or_insert_with(|| ExponentialBackoff::from_millis(100).factor(2).max_delay(Duration::from_secs(2)).map(jitter)).next().expect("should not be none");
325 warn!(err_string = ?result_entry.error_message, ?throttle_delay, "throttle");
326 sleep(throttle_delay).await;
327 } else {
328 assert_eq!(first_failed_idx, 0);
330 remaining_no_progress_retry_count -= 1;
331 if remaining_no_progress_retry_count == 0 {
332 return Err(SinkError::Kinesis(anyhow!(
333 "failed to send records. sent {} out of {}, last err: code: [{}], message: [{}]",
334 start_idx,
335 total_count,
336 result_entry.error_code.unwrap_or_default(),
337 result_entry.error_message.unwrap_or_default()
338 )));
339 } else {
340 warn!(
341 remaining_no_progress_retry_count,
342 sent = start_idx,
343 total_count,
344 "failed to send records. code: [{}], message: [{}]",
345 result_entry.error_code.unwrap_or_default(),
346 result_entry.error_message.unwrap_or_default()
347 )
348 }
349 }
350 } else {
351 start_idx += record_count;
352 remaining_no_progress_retry_count = MAX_NO_PROGRESS_RETRY_COUNT;
354 throttle_delay = None;
356 }
357 }
358 Err(e) => {
359 remaining_no_progress_retry_count -= 1;
360 if remaining_no_progress_retry_count == 0 {
361 return Err(SinkError::Kinesis(anyhow!(e).context(format!(
362 "failed to send records. sent {} out of {}",
363 start_idx, total_count,
364 ))));
365 } else {
366 warn!(
367 remaining_no_progress_retry_count,
368 sent = start_idx,
369 total_count,
370 "failed to send records. err: [{:?}]",
371 e.as_report(),
372 )
373 }
374 }
375 }
376 }
377 Ok(())
378 }
379 .boxed()
380 }
381 }
382}
383pub use opaque_type::KinesisSinkPayloadWriterDeliveryFuture;
384
385impl KinesisSinkPayloadWriter {
386 fn first_failed_entry(output: PutRecordsOutput) -> Option<(usize, PutRecordsResultEntry)> {
387 output
397 .records
398 .into_iter()
399 .find_position(|entry| entry.shard_id.is_none())
400 }
401
402 fn put_record(&mut self, key: String, payload: Vec<u8>) {
403 let size = key.len() + payload.len();
404 self.entries.push((
405 PutRecordsRequestEntry::builder()
406 .partition_key(key)
407 .data(Blob::new(payload))
408 .build()
409 .expect("should not fail because we have set `data` and `partition_key`"),
410 size,
411 ))
412 }
413}
414
415impl FormattedSink for KinesisSinkPayloadWriter {
416 type K = String;
417 type V = Vec<u8>;
418
419 async fn write_one(&mut self, k: Option<Self::K>, v: Option<Self::V>) -> Result<()> {
420 self.put_record(
421 k.ok_or_else(|| SinkError::Kinesis(anyhow!("no key provided")))?,
422 v.unwrap_or_default(),
423 );
424 Ok(())
425 }
426}
427
428impl AsyncTruncateSinkWriter for KinesisSinkWriter {
429 type DeliveryFuture = KinesisSinkPayloadWriterDeliveryFuture;
430
431 async fn write_chunk<'a>(
432 &'a mut self,
433 chunk: StreamChunk,
434 mut add_future: DeliveryFutureManagerAddFuture<'a, Self::DeliveryFuture>,
435 ) -> Result<()> {
436 let mut payload_writer = self.new_payload_writer();
437 dispatch_sink_formatter_str_key_impl!(
438 &self.formatter,
439 formatter,
440 payload_writer.write_chunk(chunk, formatter).await
441 )?;
442
443 add_future
444 .add_future_may_await(payload_writer.finish())
445 .await?;
446 Ok(())
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use aws_sdk_kinesis::types::PutRecordsRequestEntry;
453 use aws_smithy_types::Blob;
454
455 #[test]
456 fn test_kinesis_entry_builder_save_unwrap() {
457 PutRecordsRequestEntry::builder()
458 .data(Blob::new(b"data"))
459 .partition_key("partition-key")
460 .build()
461 .unwrap();
462 }
463}