Skip to main content

risingwave_connector/sink/
kinesis.rs

1// Copyright 2023 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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        // Kinesis requires partition key. There is no builtin support for round-robin as in kafka/pulsar.
95        // https://docs.aws.amazon.com/kinesis/latest/APIReference/API_PutRecord.html#Streams-PutRecord-request-PartitionKey
96        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        // Check for formatter constructor error, before it is too late for error reporting.
102        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        // check reachability
113        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            // For reference to the behavior of `put_records`
234            // https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/kinesis/client/put_records.html
235
236            async move {
237                // From the doc of `put_records`:
238                // Each PutRecords request can support up to 500 records. Each record in the request can be as large as 1 MiB,
239                // up to a limit of 5 MiB for the entire request, including partition keys. Each shard can support writes up to
240                // 1,000 records per second, up to a maximum data write total of 1 MiB per second.
241
242                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                // Allow at most 3 times of retry when not making any progress to avoid endless retry
246                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                    // 1. Prepare the records to be sent
256
257                    // The maximum possible number of records that can be sent in this iteration.
258                    // Can be smaller than this number when the total payload size exceeds `MAX_TOTAL_RECORD_PAYLOAD_SIZE`
259                    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                        // at least include one record even if its size exceed `MAX_TOTAL_RECORD_PAYLOAD_SIZE`
280                        records.push(self.entries[start_idx].0.clone());
281                    }
282
283                    // 2. send the records and handle the result
284                    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                            // From the doc of `put_records`:
298                            // A single record failure does not stop the processing of subsequent records. As a result,
299                            // PutRecords doesn’t guarantee the ordering of records. If you need to read records in the same
300                            // order they are written to the stream, use PutRecord instead of PutRecords, and write to the same shard.
301
302                            // Therefore, to ensure at least once and eventual consistency, we figure out the first failed entry, and retry
303                            // all the following entries even if the following entries may have been successfully processed.
304                            if let Some((first_failed_idx, result_entry)) = Self::first_failed_entry(output) {
305                                // first_failed_idx is also the number of successful entries
306                                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                                    // reset retry count when having progress
317                                    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                                    // From the doc of `put_records`:
320                                    // The ErrorCode parameter reflects the type of error and can be one of the following values:
321                                    // ProvisionedThroughputExceededException or InternalFailure. ErrorMessage provides more detailed
322                                    // information about the ProvisionedThroughputExceededException exception including the account ID,
323                                    // stream name, and shard ID of the record that was throttled.
324                                    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                                    // no progress due to some internal error
329                                    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                                // reset retry count when having progress
353                                remaining_no_progress_retry_count = MAX_NO_PROGRESS_RETRY_COUNT;
354                                // reset throttle delay when records can be fully sent.
355                                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        // From the doc of `put_records`:
388        // A successfully processed record includes ShardId and SequenceNumber values. The ShardId parameter
389        // identifies the shard in the stream where the record is stored. The SequenceNumber parameter is an
390        // identifier assigned to the put record, unique to all records in the stream.
391        //
392        // An unsuccessfully processed record includes ErrorCode and ErrorMessage values. ErrorCode reflects
393        // the type of error and can be one of the following values: ProvisionedThroughputExceededException or
394        // InternalFailure. ErrorMessage provides more detailed information about the ProvisionedThroughputExceededException
395        // exception including the account ID, stream name, and shard ID of the record that was throttled.
396        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}