Skip to main content

risingwave_stream/common/log_store_impl/
in_mem.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 anyhow::{Context, anyhow};
16use await_tree::InstrumentAwait;
17use futures::FutureExt;
18use futures::future::BoxFuture;
19use risingwave_common::array::StreamChunk;
20use risingwave_common::util::epoch::{EpochExt, EpochPair, INVALID_EPOCH};
21use risingwave_connector::sink::log_store::{
22    FlushCurrentEpochOptions, LogReader, LogStoreFactory, LogStoreReadItem, LogStoreResult,
23    LogWriter, LogWriterPostFlushCurrentEpoch, TruncateBarrierLogReader, TruncateOffset,
24};
25use tokio::sync::mpsc::{
26    Receiver, Sender, UnboundedReceiver, UnboundedSender, channel, unbounded_channel,
27};
28use tokio::sync::oneshot;
29
30use crate::common::log_store_impl::in_mem::LogReaderEpochProgress::{AwaitingTruncate, Consuming};
31use crate::executor::StreamExecutorResult;
32
33enum InMemLogStoreItem {
34    StreamChunk(StreamChunk),
35    Barrier {
36        next_epoch: u64,
37        options: FlushCurrentEpochOptions,
38    },
39}
40
41/// An in-memory log store that can buffer a bounded amount of stream chunk in memory via bounded
42/// mpsc channel.
43///
44/// Since it is in-memory, when `flush_current_epoch` with checkpoint epoch, it should wait for the
45/// reader to finish consuming all the data in current checkpoint epoch.
46pub struct BoundedInMemLogStoreWriter {
47    /// Current epoch. Should be `Some` after `init`
48    curr_epoch: Option<u64>,
49
50    /// Holder of oneshot channel to send the initial epoch to the associated log reader.
51    init_epoch_tx: Option<oneshot::Sender<u64>>,
52
53    /// Sending log store item to log reader
54    item_tx: Sender<InMemLogStoreItem>,
55
56    /// Receiver for the epoch consumed by log reader.
57    truncated_epoch_rx: UnboundedReceiver<u64>,
58
59    wait_init_epoch: Option<WaitInitEpochFn>,
60}
61
62#[derive(Eq, PartialEq, Debug)]
63enum LogReaderEpochProgress {
64    /// In progress of consuming data in current epoch.
65    Consuming(u64),
66    /// Finished emitting the data in checkpoint epoch, and waiting for a call on `truncate`.
67    AwaitingTruncate { sealed_epoch: u64, next_epoch: u64 },
68}
69
70const UNINITIALIZED: LogReaderEpochProgress = LogReaderEpochProgress::Consuming(INVALID_EPOCH);
71
72pub struct BoundedInMemLogStoreReader {
73    /// Current progress of log reader. Can be either consuming an epoch, or has finished consuming
74    /// an epoch and waiting to be truncated.
75    epoch_progress: LogReaderEpochProgress,
76
77    /// Holder for oneshot channel to receive the initial epoch
78    init_epoch_rx: Option<oneshot::Receiver<u64>>,
79
80    /// Receiver to fetch log store item
81    item_rx: Receiver<InMemLogStoreItem>,
82
83    /// Sender of consumed epoch to the log writer
84    truncated_epoch_tx: UnboundedSender<u64>,
85
86    /// Offset of the latest emitted item
87    latest_offset: TruncateOffset,
88
89    /// Offset of the latest truncated item
90    truncate_offset: TruncateOffset,
91}
92
93type WaitInitEpochFn =
94    Box<dyn FnOnce(EpochPair) -> BoxFuture<'static, StreamExecutorResult<()>> + Send + 'static>;
95
96pub struct BoundedInMemLogStoreFactory {
97    bound: usize,
98    wait_init_epoch: WaitInitEpochFn,
99}
100
101impl BoundedInMemLogStoreFactory {
102    pub fn new(
103        bound: usize,
104        wait_init_epoch: impl FnOnce(EpochPair) -> BoxFuture<'static, StreamExecutorResult<()>>
105        + Send
106        + 'static,
107    ) -> Self {
108        Self {
109            bound,
110            wait_init_epoch: Box::new(wait_init_epoch),
111        }
112    }
113
114    #[cfg(test)]
115    pub fn for_test(bound: usize) -> Self {
116        Self {
117            bound,
118            wait_init_epoch: Box::new(|_x| std::future::ready(Ok(())).boxed()),
119        }
120    }
121}
122
123impl LogStoreFactory for BoundedInMemLogStoreFactory {
124    type Reader = TruncateBarrierLogReader<BoundedInMemLogStoreReader>;
125    type Writer = BoundedInMemLogStoreWriter;
126
127    const ALLOW_REWIND: bool = false;
128    const REBUILD_SINK_ON_UPDATE_VNODE_BITMAP: bool = false;
129
130    async fn build(self) -> (Self::Reader, Self::Writer) {
131        let (init_epoch_tx, init_epoch_rx) = oneshot::channel();
132        let (item_tx, item_rx) = channel(self.bound);
133        let (truncated_epoch_tx, truncated_epoch_rx) = unbounded_channel();
134        let reader = BoundedInMemLogStoreReader {
135            epoch_progress: UNINITIALIZED,
136            init_epoch_rx: Some(init_epoch_rx),
137            item_rx,
138            truncated_epoch_tx,
139            latest_offset: TruncateOffset::Barrier { epoch: 0 },
140            truncate_offset: TruncateOffset::Barrier { epoch: 0 },
141        };
142        let writer = BoundedInMemLogStoreWriter {
143            curr_epoch: None,
144            init_epoch_tx: Some(init_epoch_tx),
145            item_tx,
146            truncated_epoch_rx,
147            wait_init_epoch: Some(self.wait_init_epoch),
148        };
149        (TruncateBarrierLogReader::new(reader), writer)
150    }
151}
152
153impl LogReader for BoundedInMemLogStoreReader {
154    async fn init(&mut self) -> LogStoreResult<()> {
155        let init_epoch_rx = self
156            .init_epoch_rx
157            .take()
158            .expect("should not init for twice");
159        let epoch = init_epoch_rx.await.context("unable to get init epoch")?;
160        assert_eq!(self.epoch_progress, UNINITIALIZED);
161        self.epoch_progress = LogReaderEpochProgress::Consuming(epoch);
162        self.latest_offset = TruncateOffset::Barrier {
163            epoch: epoch.prev_epoch(),
164        };
165        self.truncate_offset = TruncateOffset::Barrier {
166            epoch: epoch.prev_epoch(),
167        };
168        Ok(())
169    }
170
171    async fn next_item(&mut self) -> LogStoreResult<(u64, LogStoreReadItem)> {
172        match self.epoch_progress {
173            Consuming(current_epoch) => match self.item_rx.recv().await {
174                Some(item) => match item {
175                    InMemLogStoreItem::StreamChunk(chunk) => {
176                        let chunk_id = match self.latest_offset {
177                            TruncateOffset::Chunk { epoch, chunk_id } => {
178                                assert_eq!(epoch, current_epoch);
179                                chunk_id + 1
180                            }
181                            TruncateOffset::Barrier { epoch } => {
182                                assert!(
183                                    epoch < current_epoch,
184                                    "prev offset at barrier {} but current epoch {}",
185                                    epoch,
186                                    current_epoch
187                                );
188                                0
189                            }
190                        };
191                        self.latest_offset = TruncateOffset::Chunk {
192                            epoch: current_epoch,
193                            chunk_id,
194                        };
195                        Ok((
196                            current_epoch,
197                            LogStoreReadItem::StreamChunk { chunk, chunk_id },
198                        ))
199                    }
200                    InMemLogStoreItem::Barrier {
201                        next_epoch,
202                        options,
203                    } => {
204                        if options.is_checkpoint {
205                            self.epoch_progress = AwaitingTruncate {
206                                next_epoch,
207                                sealed_epoch: current_epoch,
208                            };
209                        } else {
210                            self.epoch_progress = Consuming(next_epoch);
211                        }
212                        self.latest_offset = TruncateOffset::Barrier {
213                            epoch: current_epoch,
214                        };
215                        Ok((
216                            current_epoch,
217                            LogStoreReadItem::Barrier {
218                                is_checkpoint: options.is_checkpoint,
219                                new_vnode_bitmap: options.new_vnode_bitmap,
220                                is_stop: options.is_stop,
221                                schema_change: options.schema_change,
222                            },
223                        ))
224                    }
225                },
226                None => Err(anyhow!("end of log stream")),
227            },
228            AwaitingTruncate { .. } => std::future::pending().await,
229        }
230    }
231
232    fn truncate(&mut self, offset: TruncateOffset) -> LogStoreResult<()> {
233        // check the truncate offset is higher than prev truncate offset
234        if self.truncate_offset >= offset {
235            return Err(anyhow!(
236                "truncate offset {:?} but prev truncate offset is {:?}",
237                offset,
238                self.truncate_offset
239            ));
240        }
241
242        // check the truncate offset does not exceed the latest possible offset
243        if offset > self.latest_offset {
244            return Err(anyhow!(
245                "truncate at {:?} but latest offset is {:?}",
246                offset,
247                self.latest_offset
248            ));
249        }
250
251        if let AwaitingTruncate {
252            sealed_epoch,
253            next_epoch,
254        } = &self.epoch_progress
255            && let TruncateOffset::Barrier { epoch } = offset
256            && epoch == *sealed_epoch
257        {
258            let sealed_epoch = *sealed_epoch;
259            self.epoch_progress = Consuming(*next_epoch);
260            self.truncated_epoch_tx
261                .send(sealed_epoch)
262                .map_err(|_| anyhow!("unable to send sealed epoch"))?;
263        }
264        self.truncate_offset = offset;
265        Ok(())
266    }
267
268    async fn rewind(&mut self) -> LogStoreResult<()> {
269        Err(anyhow!("should not call rewind on it"))
270    }
271
272    async fn start_from(&mut self, _start_offset: Option<u64>) -> LogStoreResult<()> {
273        Ok(())
274    }
275}
276
277impl LogWriter for BoundedInMemLogStoreWriter {
278    async fn init(
279        &mut self,
280        epoch: EpochPair,
281        _pause_read_on_bootstrap: bool,
282    ) -> LogStoreResult<()> {
283        let init_epoch_tx = self.init_epoch_tx.take().expect("cannot be init for twice");
284        self.wait_init_epoch
285            .take()
286            .expect("cannot be init for in-mem log store")(epoch)
287        .await?;
288        init_epoch_tx
289            .send(epoch.curr)
290            .map_err(|_| anyhow!("unable to send init epoch"))?;
291        self.curr_epoch = Some(epoch.curr);
292        Ok(())
293    }
294
295    async fn write_chunk(&mut self, chunk: StreamChunk) -> LogStoreResult<()> {
296        self.item_tx
297            .send(InMemLogStoreItem::StreamChunk(chunk))
298            .instrument_await("in_mem_send_item_chunk")
299            .await
300            .map_err(|_| anyhow!("unable to send stream chunk"))?;
301        Ok(())
302    }
303
304    async fn flush_current_epoch(
305        &mut self,
306        next_epoch: u64,
307        options: FlushCurrentEpochOptions,
308    ) -> LogStoreResult<LogWriterPostFlushCurrentEpoch<'_>> {
309        let is_checkpoint = options.is_checkpoint;
310        self.item_tx
311            .send(InMemLogStoreItem::Barrier {
312                next_epoch,
313                options,
314            })
315            .instrument_await("in_mem_send_item_barrier")
316            .await
317            .map_err(|_| anyhow!("unable to send barrier"))?;
318
319        let prev_epoch = self
320            .curr_epoch
321            .replace(next_epoch)
322            .expect("should have epoch");
323
324        if is_checkpoint {
325            let truncated_epoch = self
326                .truncated_epoch_rx
327                .recv()
328                .instrument_await("in_mem_recv_truncated_epoch")
329                .await
330                .ok_or_else(|| anyhow!("cannot get truncated epoch"))?;
331            assert_eq!(truncated_epoch, prev_epoch);
332        }
333
334        Ok(LogWriterPostFlushCurrentEpoch::new(move || {
335            async move { Ok(()) }.boxed()
336        }))
337    }
338
339    fn pause(&mut self) -> LogStoreResult<()> {
340        // no-op when decouple is not enabled
341        Ok(())
342    }
343
344    fn resume(&mut self) -> LogStoreResult<()> {
345        // no-op when decouple is not enabled
346        Ok(())
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use std::future::poll_fn;
353    use std::task::Poll;
354
355    use futures::FutureExt;
356    use risingwave_common::array::{Op, StreamChunkBuilder};
357    use risingwave_common::types::{DataType, ScalarImpl};
358    use risingwave_common::util::epoch::{EpochPair, test_epoch};
359    use risingwave_connector::sink::log_store::{
360        LogReader, LogStoreFactory, LogStoreReadItem, LogWriter, TruncateOffset,
361    };
362
363    use crate::common::log_store_impl::in_mem::BoundedInMemLogStoreFactory;
364    use crate::common::log_store_impl::kv_log_store::test_utils::LogWriterTestExt;
365
366    #[tokio::test]
367    async fn test_in_memory_log_store() {
368        let factory = BoundedInMemLogStoreFactory::for_test(4);
369        let (mut reader, mut writer) = factory.build().await;
370
371        let init_epoch = test_epoch(1);
372        let epoch1 = test_epoch(2);
373        let epoch2 = test_epoch(3);
374
375        let ops = vec![Op::Insert, Op::Delete, Op::UpdateInsert, Op::UpdateDelete];
376        let mut builder =
377            StreamChunkBuilder::unlimited(vec![DataType::Int64, DataType::Varchar], None);
378        for (i, op) in ops.into_iter().enumerate() {
379            assert!(
380                builder
381                    .append_row(
382                        op,
383                        [
384                            Some(ScalarImpl::Int64(i as i64)),
385                            Some(ScalarImpl::Utf8(format!("name_{}", i).into_boxed_str()))
386                        ]
387                    )
388                    .is_none()
389            );
390        }
391        let stream_chunk = builder.take().unwrap();
392        let stream_chunk_clone = stream_chunk.clone();
393
394        let mut join_handle = tokio::spawn(async move {
395            writer
396                .init(EpochPair::new_test_epoch(init_epoch), false)
397                .await
398                .unwrap();
399            writer
400                .write_chunk(stream_chunk_clone.clone())
401                .await
402                .unwrap();
403            writer
404                .write_chunk(stream_chunk_clone.clone())
405                .await
406                .unwrap();
407            writer
408                .flush_current_epoch_for_test(epoch1, false)
409                .await
410                .unwrap();
411            writer.write_chunk(stream_chunk_clone).await.unwrap();
412            writer
413                .flush_current_epoch_for_test(epoch2, true)
414                .await
415                .unwrap();
416        });
417
418        reader.init().await.unwrap();
419        let _chunk_id1_1 = match reader.next_item().await.unwrap() {
420            (epoch, LogStoreReadItem::StreamChunk { chunk, chunk_id }) => {
421                assert_eq!(epoch, init_epoch);
422                assert_eq!(&chunk, &stream_chunk);
423                chunk_id
424            }
425            _ => unreachable!(),
426        };
427
428        let chunk_id1_2 = match reader.next_item().await.unwrap() {
429            (epoch, LogStoreReadItem::StreamChunk { chunk, chunk_id }) => {
430                assert_eq!(epoch, init_epoch);
431                assert_eq!(&chunk, &stream_chunk);
432                chunk_id
433            }
434            _ => unreachable!(),
435        };
436
437        match reader.next_item().await.unwrap() {
438            (epoch, LogStoreReadItem::Barrier { is_checkpoint, .. }) => {
439                assert!(!is_checkpoint);
440                assert_eq!(epoch, init_epoch);
441            }
442            _ => unreachable!(),
443        }
444
445        let chunk_id2_1 = match reader.next_item().await.unwrap() {
446            (epoch, LogStoreReadItem::StreamChunk { chunk, chunk_id }) => {
447                assert_eq!(&chunk, &stream_chunk);
448                assert_eq!(epoch, epoch1);
449                chunk_id
450            }
451            _ => unreachable!(),
452        };
453
454        match reader.next_item().await.unwrap() {
455            (epoch, LogStoreReadItem::Barrier { is_checkpoint, .. }) => {
456                assert!(is_checkpoint);
457                assert_eq!(epoch, epoch1);
458            }
459            _ => unreachable!(),
460        }
461
462        reader
463            .truncate(TruncateOffset::Chunk {
464                epoch: init_epoch,
465                chunk_id: chunk_id1_2,
466            })
467            .unwrap();
468        assert!(
469            poll_fn(|cx| Poll::Ready(join_handle.poll_unpin(cx)))
470                .await
471                .is_pending()
472        );
473        reader
474            .truncate(TruncateOffset::Chunk {
475                epoch: epoch1,
476                chunk_id: chunk_id2_1,
477            })
478            .unwrap();
479        assert!(
480            poll_fn(|cx| Poll::Ready(join_handle.poll_unpin(cx)))
481                .await
482                .is_pending()
483        );
484        reader
485            .truncate(TruncateOffset::Barrier { epoch: epoch1 })
486            .unwrap();
487        join_handle.await.unwrap();
488    }
489}