Skip to main content

risingwave_stream/executor/backfill/cdc/upstream_table/
snapshot.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;
16
17use futures::{Stream, pin_mut};
18use futures_async_stream::try_stream;
19use risingwave_common::array::StreamChunk;
20use risingwave_common::catalog::{ColumnDesc, Field};
21use risingwave_common::row::OwnedRow;
22use risingwave_common::types::{Scalar, ScalarImpl, Timestamptz};
23use risingwave_common::util::chunk_coalesce::DataChunkBuilder;
24use risingwave_common_rate_limit::RateLimiter;
25use risingwave_connector::source::cdc::external::{
26    CdcOffset, ExternalTableReader, ExternalTableReaderImpl, SchemaTableName,
27};
28use risingwave_pb::plan_common::additional_column::ColumnType;
29
30use super::external::ExternalStorageTable;
31use crate::common::rate_limit::limited_chunk_size;
32use crate::executor::backfill::utils::{get_new_pos, iter_chunks};
33use crate::executor::{StreamExecutorError, StreamExecutorResult};
34
35pub trait UpstreamTableRead {
36    fn snapshot_read_full_table(
37        &self,
38        args: SnapshotReadArgs,
39        batch_size: u32,
40    ) -> impl Stream<Item = StreamExecutorResult<Option<StreamChunk>>> + Send + '_;
41
42    fn current_cdc_offset(
43        &self,
44    ) -> impl Future<Output = StreamExecutorResult<Option<CdcOffset>>> + Send + '_;
45
46    async fn disconnect(self) -> StreamExecutorResult<()>;
47
48    fn snapshot_read_table_split(
49        &self,
50        args: SplitSnapshotReadArgs,
51    ) -> impl Stream<Item = StreamExecutorResult<Option<StreamChunk>>> + Send + '_;
52}
53
54#[derive(Debug, Clone)]
55pub struct SnapshotReadArgs {
56    pub current_pos: Option<OwnedRow>,
57    pub rate_limit_rps: Option<u32>,
58    pub pk_indices: Vec<usize>,
59    pub additional_columns: Vec<ColumnDesc>,
60    pub schema_table_name: SchemaTableName,
61    pub database_name: String,
62}
63
64impl SnapshotReadArgs {
65    pub fn new(
66        current_pos: Option<OwnedRow>,
67        rate_limit_rps: Option<u32>,
68        pk_indices: Vec<usize>,
69        additional_columns: Vec<ColumnDesc>,
70        schema_table_name: SchemaTableName,
71        database_name: String,
72    ) -> Self {
73        Self {
74            current_pos,
75            rate_limit_rps,
76            pk_indices,
77            additional_columns,
78            schema_table_name,
79            database_name,
80        }
81    }
82}
83
84#[derive(Debug, Clone)]
85pub struct SplitSnapshotReadArgs {
86    pub left_bound_inclusive: OwnedRow,
87    pub right_bound_exclusive: OwnedRow,
88    pub split_columns: Vec<Field>,
89    pub rate_limit_rps: Option<u32>,
90    pub additional_columns: Vec<ColumnDesc>,
91    pub schema_table_name: SchemaTableName,
92    pub database_name: String,
93}
94
95impl SplitSnapshotReadArgs {
96    pub fn new(
97        left_bound_inclusive: OwnedRow,
98        right_bound_exclusive: OwnedRow,
99        split_columns: Vec<Field>,
100        rate_limit_rps: Option<u32>,
101        additional_columns: Vec<ColumnDesc>,
102        schema_table_name: SchemaTableName,
103        database_name: String,
104    ) -> Self {
105        Self {
106            left_bound_inclusive,
107            right_bound_exclusive,
108            split_columns,
109            rate_limit_rps,
110            additional_columns,
111            schema_table_name,
112            database_name,
113        }
114    }
115}
116
117/// A wrapper of upstream table for snapshot read
118/// because we need to customize the snapshot read for managed upstream table (e.g. mv, index)
119/// and external upstream table.
120pub struct UpstreamTableReader<T> {
121    table: T,
122    pub(crate) reader: ExternalTableReaderImpl,
123}
124
125impl<T> UpstreamTableReader<T> {
126    pub fn new(table: T, reader: ExternalTableReaderImpl) -> Self {
127        Self { table, reader }
128    }
129}
130
131/// Append additional columns with value as null to the snapshot chunk
132fn with_additional_columns(
133    snapshot_chunk: StreamChunk,
134    additional_columns: &[ColumnDesc],
135    schema_table_name: SchemaTableName,
136    database_name: String,
137) -> StreamChunk {
138    let (ops, mut columns, visibility) = snapshot_chunk.into_inner();
139    for desc in additional_columns {
140        let mut builder = desc.data_type.create_array_builder(visibility.len());
141        match *desc.additional_column.column_type.as_ref().unwrap() {
142            // set default value for timestamp
143            ColumnType::Timestamp(_) => builder.append_n(
144                visibility.len(),
145                Some(Timestamptz::default().to_scalar_value()),
146            ),
147            ColumnType::DatabaseName(_) => {
148                builder.append_n(
149                    visibility.len(),
150                    Some(ScalarImpl::from(database_name.clone())),
151                );
152            }
153            ColumnType::SchemaName(_) => {
154                builder.append_n(
155                    visibility.len(),
156                    Some(ScalarImpl::from(schema_table_name.schema_name.clone())),
157                );
158            }
159            ColumnType::TableName(_) => {
160                builder.append_n(
161                    visibility.len(),
162                    Some(ScalarImpl::from(schema_table_name.table_name.clone())),
163                );
164            }
165            // set null for other additional columns
166            _ => {
167                builder.append_n_null(visibility.len());
168            }
169        }
170        columns.push(builder.finish().into());
171    }
172    StreamChunk::with_visibility(ops, columns, visibility)
173}
174
175impl UpstreamTableRead for UpstreamTableReader<ExternalStorageTable> {
176    #[try_stream(ok = Option<StreamChunk>, error = StreamExecutorError)]
177    async fn snapshot_read_full_table(&self, args: SnapshotReadArgs, batch_size: u32) {
178        let primary_keys = self.table.pk_names();
179
180        // prepare rate limiter
181        if args.rate_limit_rps == Some(0) {
182            // If limit is 0, we should not read any data from the upstream table.
183            // Keep waiting util the stream is rebuilt.
184            let future = futures::future::pending::<()>();
185            future.await;
186            unreachable!();
187        }
188
189        let rate_limiter = RateLimiter::new(
190            args.rate_limit_rps
191                .inspect(|limit| tracing::info!(rate_limit = limit, "rate limit applied"))
192                .into(),
193        );
194
195        let mut read_args = args;
196        let schema_table_name = read_args.schema_table_name.clone();
197        let database_name = read_args.database_name.clone();
198        // loop to read all data from the table
199        loop {
200            tracing::debug!(
201                "snapshot_read primary keys: {:?}, current_pos: {:?}",
202                primary_keys,
203                read_args.current_pos
204            );
205
206            let mut read_count: usize = 0;
207            let row_stream = self.reader.snapshot_read(
208                self.table.schema_table_name(),
209                read_args.current_pos.clone(),
210                primary_keys.clone(),
211                batch_size,
212            );
213
214            pin_mut!(row_stream);
215            let mut builder = DataChunkBuilder::new(
216                self.table.schema().data_types(),
217                limited_chunk_size(read_args.rate_limit_rps),
218            );
219            let chunk_stream = iter_chunks(row_stream, &mut builder);
220            let mut current_pk_pos = read_args.current_pos.clone().unwrap_or_default();
221
222            #[for_await]
223            for chunk in chunk_stream {
224                let chunk = chunk?;
225                let chunk_size = chunk.capacity();
226                read_count += chunk.cardinality();
227                current_pk_pos = get_new_pos(&chunk, &read_args.pk_indices);
228
229                if let Some(rate_limit_rps) = read_args.rate_limit_rps
230                    && chunk_size != 0
231                {
232                    // Apply rate limit, see `risingwave_stream::executor::source::apply_rate_limit` for more.
233                    // May be should be refactored to a common function later.
234                    let limit = rate_limit_rps as usize;
235
236                    // Because we produce chunks with limited-sized data chunk builder and all rows
237                    // are `Insert`s, the chunk size should never exceed the limit.
238                    assert!(chunk_size <= limit);
239
240                    // `InsufficientCapacity` should never happen because we have check the cardinality
241                    rate_limiter.wait(chunk_size as _).await;
242                    yield Some(with_additional_columns(
243                        chunk,
244                        &read_args.additional_columns,
245                        schema_table_name.clone(),
246                        database_name.clone(),
247                    ));
248                } else {
249                    // no limit, or empty chunk
250                    yield Some(with_additional_columns(
251                        chunk,
252                        &read_args.additional_columns,
253                        schema_table_name.clone(),
254                        database_name.clone(),
255                    ));
256                    continue;
257                }
258            }
259
260            // check read_count if the snapshot batch is finished
261            if read_count < batch_size as _ {
262                tracing::debug!("finished loading of full table snapshot");
263                yield None;
264                unreachable!()
265            } else {
266                // update PK position and continue to read the table
267                read_args.current_pos = Some(current_pk_pos);
268            }
269        }
270    }
271
272    #[try_stream(ok = Option<StreamChunk>, error = StreamExecutorError)]
273    async fn snapshot_read_table_split(&self, args: SplitSnapshotReadArgs) {
274        // prepare rate limiter
275        if args.rate_limit_rps == Some(0) {
276            // If limit is 0, we should not read any data from the upstream table.
277            // Keep waiting util the stream is rebuilt.
278            let future = futures::future::pending::<()>();
279            future.await;
280            unreachable!();
281        }
282
283        let rate_limiter = RateLimiter::new(
284            args.rate_limit_rps
285                .inspect(|limit| tracing::info!(rate_limit = limit, "rate limit applied"))
286                .into(),
287        );
288
289        let read_args = args;
290        let schema_table_name = read_args.schema_table_name.clone();
291        let database_name = read_args.database_name.clone();
292        // tracing::debug!(?args, "snapshot_read",);
293
294        let row_stream = self.reader.split_snapshot_read(
295            self.table.schema_table_name(),
296            read_args.left_bound_inclusive.clone(),
297            read_args.right_bound_exclusive.clone(),
298            read_args.split_columns.clone(),
299        );
300
301        pin_mut!(row_stream);
302        let mut builder = DataChunkBuilder::new(
303            self.table.schema().data_types(),
304            limited_chunk_size(read_args.rate_limit_rps),
305        );
306        let chunk_stream = iter_chunks(row_stream, &mut builder);
307
308        #[for_await]
309        for chunk in chunk_stream {
310            let chunk = chunk?;
311            let chunk_size = chunk.capacity();
312
313            if let Some(rate_limit_rps) = read_args.rate_limit_rps
314                && chunk_size != 0
315            {
316                // Apply rate limit, see `risingwave_stream::executor::source::apply_rate_limit` for more.
317                // May be should be refactored to a common function later.
318                let limit = rate_limit_rps as usize;
319
320                // Because we produce chunks with limited-sized data chunk builder and all rows
321                // are `Insert`s, the chunk size should never exceed the limit.
322                assert!(chunk_size <= limit);
323
324                // `InsufficientCapacity` should never happen because we have check the cardinality
325                rate_limiter.wait(chunk_size as _).await;
326                yield Some(with_additional_columns(
327                    chunk,
328                    &read_args.additional_columns,
329                    schema_table_name.clone(),
330                    database_name.clone(),
331                ));
332            } else {
333                // no limit, or empty chunk
334                yield Some(with_additional_columns(
335                    chunk,
336                    &read_args.additional_columns,
337                    schema_table_name.clone(),
338                    database_name.clone(),
339                ));
340            }
341        }
342        yield None;
343    }
344
345    async fn current_cdc_offset(&self) -> StreamExecutorResult<Option<CdcOffset>> {
346        let binlog = self.reader.current_cdc_offset();
347        let binlog = binlog.await?;
348        Ok(Some(binlog))
349    }
350
351    async fn disconnect(self) -> StreamExecutorResult<()> {
352        self.reader.disconnect().await?;
353        Ok(())
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use std::collections::HashMap;
360
361    use futures::pin_mut;
362    use futures_async_stream::for_await;
363    use maplit::{convert_args, hashmap};
364    use risingwave_common::catalog::{ColumnDesc, ColumnId, Field, Schema};
365    use risingwave_common::row::OwnedRow;
366    use risingwave_common::types::{DataType, ScalarImpl};
367    use risingwave_common::util::chunk_coalesce::DataChunkBuilder;
368    use risingwave_connector::source::cdc::external::mysql::MySqlExternalTableReader;
369    use risingwave_connector::source::cdc::external::{
370        ExternalTableConfig, ExternalTableReader, SchemaTableName,
371    };
372
373    use crate::executor::backfill::utils::{get_new_pos, iter_chunks};
374
375    #[ignore]
376    #[tokio::test]
377    async fn test_mysql_table_reader() {
378        let columns = [
379            ColumnDesc::named("o_orderkey", ColumnId::new(1), DataType::Int64),
380            ColumnDesc::named("o_custkey", ColumnId::new(2), DataType::Int64),
381            ColumnDesc::named("o_orderstatus", ColumnId::new(3), DataType::Varchar),
382        ];
383        let rw_schema = Schema {
384            fields: columns.iter().map(Field::from).collect(),
385        };
386        let props: HashMap<String, String> = convert_args!(hashmap!(
387                "hostname" => "localhost",
388                "port" => "8306",
389                "username" => "root",
390                "password" => "123456",
391                "database.name" => "mydb",
392                "table.name" => "orders_rw"));
393
394        let config =
395            serde_json::from_value::<ExternalTableConfig>(serde_json::to_value(props).unwrap())
396                .unwrap();
397        let reader = MySqlExternalTableReader::new(config, rw_schema.clone(), vec![0])
398            .await
399            .unwrap();
400
401        let mut cnt: usize = 0;
402        let mut start_pk = Some(OwnedRow::new(vec![Some(ScalarImpl::Int64(0))]));
403        loop {
404            let row_stream = reader.snapshot_read(
405                SchemaTableName {
406                    schema_name: "mydb".to_owned(),
407                    table_name: "orders_rw".to_owned(),
408                },
409                start_pk.clone(),
410                vec!["o_orderkey".to_owned()],
411                1000,
412            );
413            let mut builder = DataChunkBuilder::new(rw_schema.clone().data_types(), 256);
414            let chunk_stream = iter_chunks(row_stream, &mut builder);
415            let pk_indices = vec![0];
416            pin_mut!(chunk_stream);
417            #[for_await]
418            for chunk in chunk_stream {
419                let chunk = chunk.expect("data");
420                start_pk = Some(get_new_pos(&chunk, &pk_indices));
421                cnt += chunk.capacity();
422                // println!("chunk: {:#?}", chunk);
423                println!("cnt: {}", cnt);
424            }
425            if cnt >= 1499900 {
426                println!("bye!");
427                break;
428            }
429        }
430    }
431}