risingwave_stream/executor/source/batch_source/
batch_opendal_fs_fetch.rs1use std::collections::VecDeque;
16use std::marker::PhantomData;
17use std::sync::Arc;
18
19use either::Either;
20use futures::TryStreamExt;
21use futures::stream::{self, StreamExt};
22use futures_async_stream::try_stream;
23use risingwave_common::catalog::ColumnId;
24use risingwave_common::id::TableId;
25use risingwave_connector::source::filesystem::OpendalFsSplit;
26use risingwave_connector::source::filesystem::opendal_source::OpendalSource;
27use risingwave_connector::source::reader::desc::SourceDesc;
28use risingwave_connector::source::{
29 BoxStreamingFileSourceChunkStream, SourceContext, SourceCtrlOpts, SplitImpl,
30};
31use thiserror_ext::AsReport;
32
33use crate::common::rate_limit::limited_chunk_size;
34use crate::executor::prelude::*;
35use crate::executor::source::{
36 StreamSourceCore, apply_rate_limit_with_for_streaming_file_source_reader,
37 get_split_offset_col_idx, prune_additional_cols, source_reader_event_to_chunk_stream,
38};
39use crate::executor::stream_reader::StreamReaderWithPause;
40use crate::task::LocalBarrierManager;
41
42const BATCH_SIZE: usize = 1000;
43
44pub struct BatchOpendalFsFetchExecutor<S: StateStore, Src: OpendalSource>
45where
46 SplitImpl: From<OpendalFsSplit<Src>>,
47{
48 actor_ctx: ActorContextRef,
49
50 stream_source_core: Option<StreamSourceCore<S>>,
52
53 upstream: Option<Executor>,
55
56 rate_limit_rps: Option<u32>,
58
59 barrier_manager: LocalBarrierManager,
61
62 associated_table_id: TableId,
63
64 _marker: PhantomData<Src>,
65}
66
67impl<S: StateStore, Src: OpendalSource> BatchOpendalFsFetchExecutor<S, Src>
68where
69 SplitImpl: From<OpendalFsSplit<Src>>,
70{
71 pub fn new(
72 actor_ctx: ActorContextRef,
73 stream_source_core: StreamSourceCore<S>,
74 upstream: Executor,
75 rate_limit_rps: Option<u32>,
76 barrier_manager: LocalBarrierManager,
77 associated_table_id: Option<TableId>,
78 ) -> Self {
79 assert!(associated_table_id.is_some());
80 Self {
81 actor_ctx,
82 stream_source_core: Some(stream_source_core),
83 upstream: Some(upstream),
84 rate_limit_rps,
85 barrier_manager,
86 associated_table_id: associated_table_id.unwrap(),
87 _marker: PhantomData,
88 }
89 }
90
91 fn build_source_ctx(
92 actor_ctx: &ActorContextRef,
93 source_desc: &SourceDesc,
94 core: &StreamSourceCore<S>,
95 rate_limit_rps: Option<u32>,
96 ) -> SourceContext {
97 SourceContext::new(
98 actor_ctx.id,
99 core.source_id,
100 actor_ctx.fragment_id,
101 core.source_name.clone(),
102 source_desc.metrics.clone(),
103 SourceCtrlOpts {
104 chunk_size: limited_chunk_size(rate_limit_rps),
105 split_txn: rate_limit_rps.is_some(),
106 },
107 source_desc.source.config.clone(),
108 None,
109 )
110 }
111
112 async fn build_single_file_stream_reader(
113 column_ids: Vec<ColumnId>,
114 source_ctx: SourceContext,
115 source_desc: &SourceDesc,
116 split: OpendalFsSplit<Src>,
117 rate_limit_rps: Option<u32>,
118 ) -> StreamExecutorResult<BoxStreamingFileSourceChunkStream> {
119 let (stream, _) = source_desc
120 .source
121 .build_stream(
122 Some(vec![SplitImpl::from(split)]),
123 column_ids,
124 Arc::new(source_ctx),
125 false,
126 )
127 .await
128 .map_err(StreamExecutorError::connector_error)?;
129 let optional_stream: BoxStreamingFileSourceChunkStream =
130 source_reader_event_to_chunk_stream(stream)
131 .boxed()
132 .map(|item| item.map(Some))
133 .chain(stream::once(async { Ok(None) }))
134 .boxed();
135 Ok(
136 apply_rate_limit_with_for_streaming_file_source_reader(optional_stream, rate_limit_rps)
137 .boxed(),
138 )
139 }
140
141 async fn replace_with_new_batch_reader<const BIASED: bool>(
142 files_in_progress: &mut usize,
143 file_queue: &mut VecDeque<OpendalFsSplit<Src>>,
144 stream: &mut StreamReaderWithPause<BIASED, Option<StreamChunk>>,
145 column_ids: Vec<ColumnId>,
146 source_ctx: SourceContext,
147 source_desc: &SourceDesc,
148 rate_limit_rps: Option<u32>,
149 ) -> StreamExecutorResult<()> {
150 let mut batch = Vec::with_capacity(BATCH_SIZE);
151 for _ in 0..BATCH_SIZE {
152 let Some(split) = file_queue.pop_front() else {
153 break;
154 };
155 batch.push(split);
156 }
157
158 if batch.is_empty() {
159 stream.replace_data_stream(stream::pending().boxed());
160 } else {
161 *files_in_progress += batch.len();
162 let mut merged_stream =
163 stream::empty::<StreamExecutorResult<Option<StreamChunk>>>().boxed();
164 for split in batch {
165 let single_file_stream = Self::build_single_file_stream_reader(
166 column_ids.clone(),
167 source_ctx.clone(),
168 source_desc,
169 split,
170 rate_limit_rps,
171 )
172 .await?
173 .map_err(StreamExecutorError::connector_error);
174 merged_stream = merged_stream.chain(single_file_stream).boxed();
175 }
176 stream.replace_data_stream(merged_stream);
177 }
178
179 Ok(())
180 }
181
182 #[try_stream(ok = Message, error = StreamExecutorError)]
183 async fn into_stream(mut self) {
184 let mut upstream = self.upstream.take().unwrap().execute();
185 let first_barrier = expect_first_barrier(&mut upstream).await?;
186 let is_pause_on_startup = first_barrier.is_pause_on_startup();
187 yield Message::Barrier(first_barrier);
188
189 let mut core = self.stream_source_core.take().unwrap();
190 let source_desc = core
191 .source_desc_builder
192 .take()
193 .unwrap()
194 .build()
195 .map_err(StreamExecutorError::connector_error)?;
196
197 let (Some(split_idx), Some(offset_idx), _) = get_split_offset_col_idx(&source_desc.columns)
198 else {
199 unreachable!("Partition and offset columns must be set.");
200 };
201
202 let mut files_in_progress = 0;
203 let mut file_queue = VecDeque::new();
204 let mut list_finished = false;
205 let mut is_refreshing = false;
206 let mut stream = StreamReaderWithPause::<true, Option<StreamChunk>>::new(
207 upstream,
208 stream::pending().boxed(),
209 );
210
211 if is_pause_on_startup {
212 stream.pause_stream();
213 }
214
215 while let Some(msg) = stream.next().await {
216 match msg {
217 Err(e) => {
218 tracing::error!(error = %e.as_report(), "Batch OpenDAL fetch error");
219 return Err(e);
220 }
221 Ok(msg) => match msg {
222 Either::Left(msg) => match msg {
223 Message::Barrier(barrier) => {
224 if let Some(mutation) = barrier.mutation.as_deref() {
225 match mutation {
226 Mutation::Pause => stream.pause_stream(),
227 Mutation::Resume => stream.resume_stream(),
228 Mutation::RefreshStart {
229 associated_source_id,
230 ..
231 } if associated_source_id == &core.source_id => {
232 tracing::info!(
233 ?barrier.epoch,
234 actor_id = %self.actor_ctx.id,
235 source_id = %core.source_id,
236 table_id = %self.associated_table_id,
237 queue_len = file_queue.len(),
238 files_in_progress,
239 "RefreshStart: clearing batch OpenDAL fetch state"
240 );
241 file_queue.clear();
242 files_in_progress = 0;
243 list_finished = false;
244 is_refreshing = true;
245 stream.replace_data_stream(stream::pending().boxed());
246 }
247 Mutation::ListFinish {
248 associated_source_id,
249 } if associated_source_id == &core.source_id => {
250 tracing::info!(
251 ?barrier.epoch,
252 actor_id = %self.actor_ctx.id,
253 source_id = %core.source_id,
254 table_id = %self.associated_table_id,
255 "received ListFinish mutation"
256 );
257 list_finished = true;
258 }
259 _ => (),
260 }
261 }
262
263 if files_in_progress == 0
264 && file_queue.is_empty()
265 && list_finished
266 && is_refreshing
267 && barrier.is_checkpoint()
268 {
269 tracing::info!(
270 ?barrier.epoch,
271 actor_id = %self.actor_ctx.id,
272 source_id = %core.source_id,
273 table_id = %self.associated_table_id,
274 "Reporting batch OpenDAL source load finished"
275 );
276 self.barrier_manager.report_source_load_finished(
277 barrier.epoch,
278 self.actor_ctx.id,
279 self.associated_table_id,
280 core.source_id,
281 );
282 list_finished = false;
283 is_refreshing = false;
284 }
285
286 yield Message::Barrier(barrier);
287
288 if files_in_progress == 0 && !file_queue.is_empty() && is_refreshing {
289 let source_ctx = Self::build_source_ctx(
290 &self.actor_ctx,
291 &source_desc,
292 &core,
293 self.rate_limit_rps,
294 );
295 Self::replace_with_new_batch_reader(
296 &mut files_in_progress,
297 &mut file_queue,
298 &mut stream,
299 core.column_ids.clone(),
300 source_ctx,
301 &source_desc,
302 self.rate_limit_rps,
303 )
304 .await?;
305 }
306 }
307 Message::Chunk(chunk) => {
308 for row in chunk.data_chunk().rows() {
309 let filename = row.datum_at(0).unwrap().into_utf8();
310 let size = row.datum_at(2).unwrap().into_int64();
311
312 if size > 0 {
313 file_queue.push_back(OpendalFsSplit::<Src>::new(
314 filename.to_owned(),
315 0,
316 size as usize,
317 ));
318 }
319 }
320
321 tracing::debug!(
322 actor_id = %self.actor_ctx.id,
323 source_id = %core.source_id,
324 queue_len = file_queue.len(),
325 "Added OpenDAL file assignments to batch fetch queue"
326 );
327 }
328 Message::Watermark(_) => unreachable!(),
329 },
330 Either::Right(optional_chunk) => match optional_chunk {
331 Some(chunk) => {
332 let chunk = prune_additional_cols(
333 &chunk,
334 &[split_idx, offset_idx],
335 &source_desc.columns,
336 );
337 yield Message::Chunk(chunk);
338 }
339 None => {
340 files_in_progress = files_in_progress.saturating_sub(1);
341 }
342 },
343 },
344 }
345 }
346 }
347}
348
349impl<S: StateStore, Src: OpendalSource> Execute for BatchOpendalFsFetchExecutor<S, Src>
350where
351 SplitImpl: From<OpendalFsSplit<Src>>,
352{
353 fn execute(self: Box<Self>) -> BoxedMessageStream {
354 self.into_stream().boxed()
355 }
356}
357
358impl<S: StateStore, Src: OpendalSource> Debug for BatchOpendalFsFetchExecutor<S, Src>
359where
360 SplitImpl: From<OpendalFsSplit<Src>>,
361{
362 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
363 if let Some(core) = &self.stream_source_core {
364 f.debug_struct("BatchOpendalFsFetchExecutor")
365 .field("source_id", &core.source_id)
366 .field("column_ids", &core.column_ids)
367 .finish()
368 } else {
369 f.debug_struct("BatchOpendalFsFetchExecutor").finish()
370 }
371 }
372}