risingwave_stream/executor/source/batch_source/
batch_posix_fs_fetch.rs1use std::collections::VecDeque;
16use std::io::BufRead;
17use std::path::Path;
18
19use either::Either;
20use futures::TryStreamExt;
21use futures::stream::{self, StreamExt};
22use futures_async_stream::try_stream;
23use risingwave_common::id::TableId;
24use risingwave_common::types::{JsonbVal, ScalarRef};
25use risingwave_common_rate_limit::RateLimiter;
26use risingwave_connector::parser::{ByteStreamSourceParserImpl, CommonParserConfig, ParserConfig};
27use risingwave_connector::source::filesystem::OpendalFsSplit;
28use risingwave_connector::source::filesystem::opendal_source::OpendalPosixFs;
29use risingwave_connector::source::{
30 ConnectorProperties, SourceChunkStream, SourceContext, SourceCtrlOpts, SourceMessage,
31 SourceMessageEvent, SourceMeta, SourceReaderEvent, SplitMetaData,
32};
33use risingwave_pb::common::ThrottleType;
34use thiserror_ext::AsReport;
35use tokio::fs;
36
37use crate::common::rate_limit::limited_chunk_size;
38use crate::executor::prelude::*;
39use crate::executor::source::{StreamSourceCore, get_split_offset_col_idx, prune_additional_cols};
40use crate::executor::stream_reader::StreamReaderWithPause;
41use crate::task::LocalBarrierManager;
42
43const BATCH_SIZE: usize = 1000;
45
46fn into_data_chunk_stream(
47 stream: impl futures::Stream<Item = risingwave_connector::error::ConnectorResult<SourceReaderEvent>>
48 + Send
49 + 'static,
50) -> impl SourceChunkStream {
51 stream
52 .try_filter_map(|event| async move {
53 Ok(match event {
54 SourceReaderEvent::DataChunk(chunk) => Some(chunk),
55 SourceReaderEvent::SplitProgress(_) => None,
56 })
57 })
58 .boxed()
59}
60
61pub struct BatchPosixFsFetchExecutor<S: StateStore> {
71 actor_ctx: ActorContextRef,
72
73 stream_source_core: Option<StreamSourceCore<S>>,
75
76 upstream: Option<Executor>,
78
79 rate_limit_rps: Option<u32>,
81
82 rate_limiter: Arc<RateLimiter>,
84
85 barrier_manager: LocalBarrierManager,
87
88 file_queue: VecDeque<(String, JsonbVal)>,
91
92 associated_table_id: TableId,
94}
95
96type FileData = Option<StreamChunk>;
98
99impl<S: StateStore> BatchPosixFsFetchExecutor<S> {
100 pub fn new(
101 actor_ctx: ActorContextRef,
102 stream_source_core: StreamSourceCore<S>,
103 upstream: Executor,
104 rate_limit_rps: Option<u32>,
105 barrier_manager: LocalBarrierManager,
106 associated_table_id: Option<TableId>,
107 ) -> Self {
108 assert!(associated_table_id.is_some());
109 Self {
110 actor_ctx,
111 stream_source_core: Some(stream_source_core),
112 upstream: Some(upstream),
113 rate_limit_rps,
114 rate_limiter: Arc::new(RateLimiter::new(rate_limit_rps.into())),
115 barrier_manager,
116 file_queue: VecDeque::new(),
117 associated_table_id: associated_table_id.unwrap(),
118 }
119 }
120
121 fn replace_with_new_batch_reader<const BIASED: bool>(
124 files_in_progress: &mut usize,
125 file_queue: &mut VecDeque<(String, JsonbVal)>,
126 stream: &mut StreamReaderWithPause<BIASED, FileData>,
127 properties: ConnectorProperties,
128 parser_config: ParserConfig,
129 source_ctx: Arc<SourceContext>,
130 rate_limiter: Arc<RateLimiter>,
131 ) -> StreamExecutorResult<()> {
132 let mut batch = Vec::with_capacity(BATCH_SIZE);
134
135 for _ in 0..BATCH_SIZE {
136 if let Some((_file_path, split_json)) = file_queue.pop_front() {
137 let split = OpendalFsSplit::<OpendalPosixFs>::restore_from_json(split_json)?;
138 batch.push(split);
139 } else {
140 break;
141 }
142 }
143
144 if batch.is_empty() {
145 stream.replace_data_stream(stream::pending().boxed());
147 } else {
148 *files_in_progress += batch.len();
149 let batch_reader = Self::build_batched_stream_reader(
150 batch,
151 properties,
152 parser_config,
153 source_ctx,
154 rate_limiter,
155 );
156 stream.replace_data_stream(batch_reader.boxed());
157 }
158
159 Ok(())
160 }
161
162 #[try_stream(ok = FileData, error = StreamExecutorError)]
164 async fn build_batched_stream_reader(
165 batch: Vec<OpendalFsSplit<OpendalPosixFs>>,
166 properties: ConnectorProperties,
167 parser_config: ParserConfig,
168 source_ctx: Arc<SourceContext>,
169 rate_limiter: Arc<RateLimiter>,
170 ) {
171 let ConnectorProperties::BatchPosixFs(batch_posix_fs_properties) = properties else {
172 unreachable!()
173 };
174
175 let root_path = batch_posix_fs_properties.root.clone();
176
177 for split in batch {
178 let file_path = split.name.clone();
179 let full_path = Path::new(&root_path).join(&file_path);
180
181 let content = match fs::read(&full_path).await {
183 Ok(content) => content,
184 Err(e) => {
185 tracing::error!(
186 error = %e.as_report(),
187 file_path = %full_path.display(),
188 "Failed to read file"
189 );
190 continue;
191 }
192 };
193
194 if content.is_empty() {
195 yield None;
197 continue;
198 }
199
200 for line in content.lines() {
202 let line =
203 line.map_err(|e| StreamExecutorError::connector_error(anyhow::Error::from(e)))?;
204
205 let message = SourceMessage {
206 key: None,
207 payload: Some(line.as_bytes().to_vec()),
208 offset: "0".to_owned(),
209 split_id: split.id(),
210 meta: SourceMeta::Empty,
211 };
212
213 let parser =
216 ByteStreamSourceParserImpl::create(parser_config.clone(), source_ctx.clone())
217 .await?;
218
219 let chunk_stream = into_data_chunk_stream(parser.parse_stream_with_events(
220 Box::pin(futures::stream::once(async {
221 Ok(SourceMessageEvent::Data(vec![message]))
222 })),
223 ));
224
225 #[for_await]
226 for chunk in chunk_stream {
227 let chunk = chunk?;
228 rate_limiter.wait(chunk.rate_limit_permits()).await;
229 yield Some(chunk);
230 }
231 }
232
233 tracing::debug!(file_path, "Processed file");
234 yield None;
235 }
236 }
237
238 #[try_stream(ok = Message, error = StreamExecutorError)]
239 async fn into_stream(mut self) {
240 let mut upstream = self.upstream.take().unwrap().execute();
241 let barrier = expect_first_barrier(&mut upstream).await?;
242 let is_pause_on_startup = barrier.is_pause_on_startup();
243 yield Message::Barrier(barrier);
244
245 let mut core = self.stream_source_core.take().unwrap();
246
247 let source_desc_builder = core.source_desc_builder.take().unwrap();
249
250 let source_desc = source_desc_builder
251 .build()
252 .map_err(StreamExecutorError::connector_error)?;
253 let (Some(split_idx), Some(offset_idx), _) = get_split_offset_col_idx(&source_desc.columns)
254 else {
255 unreachable!("Partition and offset columns must be set.");
256 };
257
258 let properties = source_desc.source.config.clone();
259 let parser_config = ParserConfig {
260 common: CommonParserConfig {
261 rw_columns: source_desc.columns.clone(),
262 },
263 specific: source_desc.source.parser_config.clone(),
264 };
265
266 let mut files_in_progress: usize = 0;
267 let mut stream =
268 StreamReaderWithPause::<true, FileData>::new(upstream, stream::pending().boxed());
269
270 if is_pause_on_startup {
271 stream.pause_stream();
272 }
273
274 let mut list_finished = false;
278 let mut is_refreshing = false;
279 let mut file_queue = self.file_queue;
280
281 let actor_ctx = self.actor_ctx.clone();
283 let barrier_manager = self.barrier_manager.clone();
284 let rate_limit_rps = &mut self.rate_limit_rps;
285 let rate_limiter = self.rate_limiter.clone();
286
287 let make_source_ctx = |rate_limit_rps: Option<u32>| {
288 Arc::new(SourceContext::new(
289 actor_ctx.id,
290 core.source_id,
291 actor_ctx.fragment_id,
292 core.source_name.clone(),
293 source_desc.metrics.clone(),
294 SourceCtrlOpts {
295 chunk_size: limited_chunk_size(rate_limit_rps),
296 split_txn: rate_limit_rps.is_some(),
297 },
298 source_desc.source.config.clone(),
299 None,
300 ))
301 };
302 let mut source_ctx = make_source_ctx(*rate_limit_rps);
303
304 while let Some(msg) = stream.next().await {
305 match msg {
306 Err(e) => {
307 tracing::error!(error = %e.as_report(), "Fetch Error");
308 files_in_progress = 0;
309 }
310 Ok(msg) => match msg {
311 Either::Left(msg) => match msg {
313 Message::Barrier(barrier) => {
314 let need_rebuild_reader = false;
315
316 if let Some(mutation) = barrier.mutation.as_deref() {
317 match mutation {
318 Mutation::Throttle(fragment_to_apply) => {
319 if let Some(entry) =
320 fragment_to_apply.get(&actor_ctx.fragment_id)
321 && entry.throttle_type() == ThrottleType::Source
322 && entry.rate_limit != *rate_limit_rps
323 {
324 tracing::info!(
325 "updating rate limit from {:?} to {:?}",
326 *rate_limit_rps,
327 entry.rate_limit
328 );
329 *rate_limit_rps = entry.rate_limit;
330 rate_limiter.update(entry.rate_limit.into());
331 source_ctx = make_source_ctx(*rate_limit_rps);
332 }
333 }
334 Mutation::Pause => stream.pause_stream(),
335 Mutation::Resume => stream.resume_stream(),
336 Mutation::RefreshStart {
337 associated_source_id,
338 ..
339 } if associated_source_id.as_raw_id()
340 == core.source_id.as_raw_id() =>
341 {
342 tracing::info!(
343 ?barrier.epoch,
344 actor_id = %actor_ctx.id,
345 source_id = %core.source_id,
346 queue_len = file_queue.len(),
347 files_in_progress,
348 "RefreshStart: clearing state and aborting workload"
349 );
350
351 file_queue.clear();
353 files_in_progress = 0;
354 list_finished = false;
355 is_refreshing = true;
356
357 stream.replace_data_stream(stream::pending().boxed());
359 }
360 Mutation::ListFinish {
361 associated_source_id,
362 } if associated_source_id.as_raw_id()
363 == core.source_id.as_raw_id() =>
364 {
365 tracing::info!(
367 ?barrier.epoch,
368 actor_id = %actor_ctx.id,
369 source_id = %core.source_id,
370 "received ListFinish mutation"
371 );
372 list_finished = true;
373 }
374 _ => (),
375 }
376 }
377
378 let epoch = barrier.epoch;
379
380 if files_in_progress == 0
388 && file_queue.is_empty()
389 && list_finished
390 && is_refreshing
391 && barrier.is_checkpoint()
392 {
393 tracing::info!(
394 ?epoch,
395 actor_id = %actor_ctx.id,
396 source_id = %core.source_id,
397 "Reporting source load finished"
398 );
399 barrier_manager.report_source_load_finished(
400 epoch,
401 actor_ctx.id,
402 self.associated_table_id,
403 core.source_id,
404 );
405 list_finished = false;
407 is_refreshing = false;
408 }
409
410 yield Message::Barrier(barrier);
412
413 if (files_in_progress == 0 || need_rebuild_reader)
415 && *rate_limit_rps != Some(0)
416 {
417 Self::replace_with_new_batch_reader(
418 &mut files_in_progress,
419 &mut file_queue,
420 &mut stream,
421 properties.clone(),
422 parser_config.clone(),
423 source_ctx.clone(),
424 rate_limiter.clone(),
425 )?;
426 }
427 }
428 Message::Chunk(chunk) => {
431 for row in chunk.data_chunk().rows() {
432 let file_name = row.datum_at(0).unwrap().into_utf8().to_owned();
433 let split = row.datum_at(1).unwrap().into_jsonb().to_owned_scalar();
434 file_queue.push_back((file_name, split));
435 }
436
437 tracing::debug!(
438 actor_id = %actor_ctx.id,
439 queue_len = file_queue.len(),
440 "Added file assignments to queue"
441 );
442 }
443 Message::Watermark(_) => unreachable!(),
444 },
445 Either::Right(Some(chunk)) => {
447 let chunk = prune_additional_cols(
448 &chunk,
449 &[split_idx, offset_idx],
450 &source_desc.columns,
451 );
452 yield Message::Chunk(chunk);
453 }
454 Either::Right(None) => {
455 files_in_progress -= 1;
456 }
457 },
458 }
459 }
460 }
461}
462
463impl<S: StateStore> Execute for BatchPosixFsFetchExecutor<S> {
464 fn execute(self: Box<Self>) -> BoxedMessageStream {
465 self.into_stream().boxed()
466 }
467}
468
469impl<S: StateStore> Debug for BatchPosixFsFetchExecutor<S> {
470 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
471 if let Some(core) = &self.stream_source_core {
472 f.debug_struct("BatchPosixFsFetchExecutor")
473 .field("source_id", &core.source_id)
474 .field("column_ids", &core.column_ids)
475 .finish()
476 } else {
477 f.debug_struct("BatchPosixFsFetchExecutor").finish()
478 }
479 }
480}