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