Skip to main content

risingwave_connector/sink/
writer.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::future::{Future, Ready};
16use std::pin::pin;
17use std::time::Instant;
18
19use async_trait::async_trait;
20use await_tree::InstrumentAwait;
21use futures::TryFuture;
22use futures::future::{Either, select};
23use risingwave_common::array::StreamChunk;
24use rw_futures_util::drop_either_future;
25
26use crate::sink::encoder::SerTo;
27use crate::sink::formatter::SinkFormatter;
28use crate::sink::log_store::{
29    DeliveryFutureManager, DeliveryFutureManagerAddFuture, LogStoreReadItem, TruncateOffset,
30};
31use crate::sink::{LogSinker, Result, SinkError, SinkLogReader, SinkWriterMetrics};
32
33#[async_trait]
34pub trait SinkWriter: Send + 'static {
35    type CommitMetadata: Send = ();
36    /// Begin a new epoch
37    async fn begin_epoch(&mut self, epoch: u64) -> Result<()>;
38
39    /// Write a stream chunk to sink
40    async fn write_batch(&mut self, chunk: StreamChunk) -> Result<()>;
41
42    /// Receive a barrier and mark the end of current epoch. When `is_checkpoint` is true, the sink
43    /// writer should commit the current epoch.
44    async fn barrier(&mut self, is_checkpoint: bool) -> Result<Self::CommitMetadata>;
45
46    /// Clean up
47    async fn abort(&mut self) -> Result<()> {
48        Ok(())
49    }
50}
51
52pub type DummyDeliveryFuture = Ready<std::result::Result<(), SinkError>>;
53
54pub trait AsyncTruncateSinkWriter: Send + 'static {
55    type DeliveryFuture: TryFuture<Ok = (), Error = SinkError> + Unpin + Send + 'static =
56        DummyDeliveryFuture;
57
58    fn write_chunk<'a>(
59        &'a mut self,
60        chunk: StreamChunk,
61        add_future: DeliveryFutureManagerAddFuture<'a, Self::DeliveryFuture>,
62    ) -> impl Future<Output = Result<()>> + Send + 'a;
63
64    fn barrier(&mut self, _is_checkpoint: bool) -> impl Future<Output = Result<()>> + Send + '_ {
65        async { Ok(()) }
66    }
67}
68
69/// A free-form sink that may output in multiple formats and encodings. Examples include kafka,
70/// kinesis, nats and redis.
71///
72/// The implementor specifies required key & value type (likely string or bytes), as well as how to
73/// write a single pair. The provided `write_chunk` method would handle the interaction with a
74/// `SinkFormatter`.
75///
76/// Currently kafka takes `&mut self` while kinesis takes `&self`. So we use `&mut self` in trait
77/// but implement it for `&Kinesis`. This allows us to hold `&mut &Kinesis` and `&Kinesis`
78/// simultaneously, preventing the schema clone issue propagating from kafka to kinesis.
79pub trait FormattedSink {
80    type K;
81    type V;
82    async fn write_one(&mut self, k: Option<Self::K>, v: Option<Self::V>) -> Result<()>;
83
84    async fn write_chunk<F: SinkFormatter>(
85        &mut self,
86        chunk: StreamChunk,
87        formatter: &F,
88    ) -> Result<()>
89    where
90        F::K: SerTo<Self::K>,
91        F::V: SerTo<Self::V>,
92    {
93        for r in formatter.format_chunk(&chunk) {
94            let (event_key_object, event_object) = r?;
95
96            self.write_one(
97                event_key_object.map(SerTo::ser_to).transpose()?,
98                event_object.map(SerTo::ser_to).transpose()?,
99            )
100            .await?;
101        }
102
103        Ok(())
104    }
105}
106
107pub struct LogSinkerOf<W> {
108    writer: W,
109    sink_writer_metrics: SinkWriterMetrics,
110}
111
112impl<W> LogSinkerOf<W> {
113    pub fn new(writer: W, sink_writer_metrics: SinkWriterMetrics) -> Self {
114        LogSinkerOf {
115            writer,
116            sink_writer_metrics,
117        }
118    }
119}
120
121#[async_trait]
122impl<W: SinkWriter<CommitMetadata = ()>> LogSinker for LogSinkerOf<W> {
123    async fn consume_log_and_sink(self, mut log_reader: impl SinkLogReader) -> Result<!> {
124        log_reader.start_from(None).await?;
125        let mut sink_writer = self.writer;
126        let metrics = self.sink_writer_metrics;
127        #[derive(Debug)]
128        enum LogConsumerState {
129            /// Mark that the log consumer is not initialized yet
130            Uninitialized,
131
132            /// Mark that a new epoch has begun.
133            EpochBegun { curr_epoch: u64 },
134
135            /// Mark that the consumer has just received a barrier
136            BarrierReceived { prev_epoch: u64 },
137        }
138
139        let mut state = LogConsumerState::Uninitialized;
140
141        loop {
142            let (epoch, item): (u64, LogStoreReadItem) = log_reader.next_item().await?;
143            // begin_epoch when not previously began
144            state = match state {
145                LogConsumerState::Uninitialized => {
146                    sink_writer
147                        .begin_epoch(epoch)
148                        .instrument_await(await_tree::span!("sink_begin_epoch epoch={epoch}"))
149                        .await?;
150                    LogConsumerState::EpochBegun { curr_epoch: epoch }
151                }
152                LogConsumerState::EpochBegun { curr_epoch } => {
153                    assert!(
154                        epoch >= curr_epoch,
155                        "new epoch {} should not be below the current epoch {}",
156                        epoch,
157                        curr_epoch
158                    );
159                    LogConsumerState::EpochBegun { curr_epoch: epoch }
160                }
161                LogConsumerState::BarrierReceived { prev_epoch } => {
162                    assert!(
163                        epoch > prev_epoch,
164                        "new epoch {} should be greater than prev epoch {}",
165                        epoch,
166                        prev_epoch
167                    );
168                    sink_writer
169                        .begin_epoch(epoch)
170                        .instrument_await(await_tree::span!("sink_begin_epoch epoch={epoch}"))
171                        .await?;
172                    LogConsumerState::EpochBegun { curr_epoch: epoch }
173                }
174            };
175            match item {
176                LogStoreReadItem::StreamChunk { chunk, .. } => {
177                    if let Err(e) = sink_writer
178                        .write_batch(chunk)
179                        .instrument_await(await_tree::span!("sink_write_batch").verbose())
180                        .await
181                    {
182                        sink_writer.abort().instrument_await("sink_abort").await?;
183                        return Err(e);
184                    }
185                }
186                LogStoreReadItem::Barrier {
187                    is_checkpoint,
188                    new_vnode_bitmap,
189                    ..
190                } => {
191                    let prev_epoch = match state {
192                        LogConsumerState::EpochBegun { curr_epoch } => curr_epoch,
193                        _ => unreachable!("epoch must have begun before handling barrier"),
194                    };
195                    if is_checkpoint {
196                        let start_time = Instant::now();
197                        sink_writer
198                            .barrier(true)
199                            .instrument_await(await_tree::span!(
200                                "sink_barrier checkpoint=true epoch={epoch}"
201                            ))
202                            .await?;
203                        metrics
204                            .sink_commit_duration
205                            .observe(start_time.elapsed().as_secs_f64());
206                        log_reader.truncate(TruncateOffset::Barrier { epoch })?;
207                    } else {
208                        assert!(new_vnode_bitmap.is_none());
209                        sink_writer
210                            .barrier(false)
211                            .instrument_await(await_tree::span!(
212                                "sink_barrier checkpoint=false epoch={epoch}"
213                            ))
214                            .await?;
215                    }
216                    state = LogConsumerState::BarrierReceived { prev_epoch }
217                }
218            }
219        }
220    }
221}
222
223#[easy_ext::ext(SinkWriterExt)]
224impl<T> T
225where
226    T: SinkWriter<CommitMetadata = ()> + Sized,
227{
228    pub fn into_log_sinker(self, sink_writer_metrics: SinkWriterMetrics) -> LogSinkerOf<Self> {
229        LogSinkerOf {
230            writer: self,
231            sink_writer_metrics,
232        }
233    }
234}
235
236pub struct AsyncTruncateLogSinkerOf<W: AsyncTruncateSinkWriter> {
237    writer: W,
238    future_manager: DeliveryFutureManager<W::DeliveryFuture>,
239}
240
241impl<W: AsyncTruncateSinkWriter> AsyncTruncateLogSinkerOf<W> {
242    pub fn new(writer: W, max_future_count: usize) -> Self {
243        AsyncTruncateLogSinkerOf {
244            writer,
245            future_manager: DeliveryFutureManager::new(max_future_count),
246        }
247    }
248}
249
250#[async_trait]
251impl<W: AsyncTruncateSinkWriter> LogSinker for AsyncTruncateLogSinkerOf<W> {
252    async fn consume_log_and_sink(mut self, mut log_reader: impl SinkLogReader) -> Result<!> {
253        log_reader.start_from(None).await?;
254        loop {
255            let next_truncate_offset = self
256                .future_manager
257                .next_truncate_offset()
258                .instrument_await("sink_wait_delivery");
259            let select_result = drop_either_future(
260                select(pin!(log_reader.next_item()), pin!(next_truncate_offset)).await,
261            );
262            match select_result {
263                Either::Left(item_result) => {
264                    let (epoch, item) = item_result?;
265                    match item {
266                        LogStoreReadItem::StreamChunk { chunk_id, chunk } => {
267                            let add_future = self.future_manager.start_write_chunk(epoch, chunk_id);
268                            self.writer
269                                .write_chunk(chunk, add_future)
270                                .instrument_await(await_tree::span!("sink_write_batch").verbose())
271                                .await?;
272                        }
273                        LogStoreReadItem::Barrier { is_checkpoint, .. } => {
274                            self.writer
275                                .barrier(is_checkpoint)
276                                .instrument_await(await_tree::span!(
277                                    "sink_barrier checkpoint={is_checkpoint} epoch={epoch}"
278                                ))
279                                .await?;
280                            self.future_manager.add_barrier(epoch);
281                        }
282                    }
283                }
284                Either::Right(offset_result) => {
285                    let offset = offset_result?;
286                    log_reader.truncate(offset)?;
287                }
288            }
289        }
290    }
291}
292
293#[easy_ext::ext(AsyncTruncateSinkWriterExt)]
294impl<T> T
295where
296    T: AsyncTruncateSinkWriter + Sized,
297{
298    pub fn into_log_sinker(self, max_future_count: usize) -> AsyncTruncateLogSinkerOf<Self> {
299        AsyncTruncateLogSinkerOf::new(self, max_future_count)
300    }
301}