risingwave_stream/executor/source/iceberg_update/
fetch.rs1use std::ops::Bound;
19use std::time::Instant;
20
21use anyhow::Context;
22use either::Either;
23use futures::stream;
24use risingwave_common::array::Op;
25use risingwave_common::catalog::ColumnCatalog;
26use risingwave_common::config::StreamingConfig;
27use risingwave_common::hash::VnodeBitmapExt;
28use risingwave_common::{bail, ensure};
29use risingwave_common_estimate_size::EstimateSize;
30use risingwave_connector::sink::iceberg::PositionDeleteReader;
31use risingwave_connector::source::iceberg::update_reader::read_file_updates;
32use risingwave_connector::source::iceberg::update_state::{
33 IcebergUpdateAssignment, IcebergUpdateFetchState,
34};
35use risingwave_connector::source::iceberg::{
36 GLOBAL_ICEBERG_SCAN_METRICS, IcebergFileScanMetrics, IcebergProperties,
37 IcebergScanMetricsLabels,
38};
39use risingwave_storage::store::PrefetchOptions;
40use thiserror_ext::AsReport;
41
42use super::{decode, properties, scan_metrics, task_pk, task_row};
43use crate::executor::prelude::*;
44use crate::executor::source::{SourceStateTableHandler, StreamSourceCore};
45use crate::executor::stream_reader::StreamReaderWithPause;
46
47pub struct IcebergUpdateFetchExecutor<S: StateStore> {
48 actor: ActorContextRef,
49 core: StreamSourceCore<S>,
50 upstream: Executor,
51 streaming_config: Arc<StreamingConfig>,
52 output_columns: Vec<ColumnCatalog>,
53 key_indices: Vec<usize>,
54}
55
56impl<S: StateStore> IcebergUpdateFetchExecutor<S> {
57 pub fn new(
58 actor: ActorContextRef,
59 core: StreamSourceCore<S>,
60 upstream: Executor,
61 streaming_config: Arc<StreamingConfig>,
62 output_columns: Vec<ColumnCatalog>,
63 key_indices: Vec<usize>,
64 ) -> Self {
65 Self {
66 actor,
67 core,
68 upstream,
69 streaming_config,
70 output_columns,
71 key_indices,
72 }
73 }
74
75 #[try_stream(ok = Message, error = StreamExecutorError)]
76 async fn into_stream(mut self) {
77 let developer = &self.streaming_config.developer;
78 let batch_size = developer.iceberg_fetch_batch_size as usize;
79 let chunk_size = developer.chunk_size;
80 ensure!(batch_size > 0, "Iceberg fetch batch size must be positive");
81 let properties = properties(&mut self.core)?;
82 let output_types = self
83 .output_columns
84 .iter()
85 .map(|column| column.data_type().clone())
86 .collect::<Vec<_>>();
87 let ctx = FetchContext {
88 job_id: self
89 .core
90 .split_state_store
91 .state_table()
92 .table_id()
93 .as_raw_id(),
94 batch_size,
95 chunk_size,
96 output_columns: self.output_columns,
97 key_indices: self.key_indices,
98 scan_metrics: scan_metrics(&self.core, &properties),
99 file_scan_metrics: IcebergFileScanMetrics::new(
100 &GLOBAL_ICEBERG_SCAN_METRICS,
101 properties.table.table_name(),
102 ),
103 properties,
104 };
105
106 let mut state_table = self.core.split_state_store;
107 let mut upstream = self.upstream.execute();
108 let first = expect_first_barrier(&mut upstream).await?;
109 let first_epoch = first.epoch;
110 let paused = first.is_pause_on_startup();
111 yield Message::Barrier(first);
112 state_table.init_epoch(first_epoch).await?;
113 let mut stream = FetchStream::new(upstream, stream::pending());
114 if paused {
115 stream.pause_stream();
116 }
117 let mut inflight = ctx.install_next_batch(&state_table, &mut stream).await?;
118 let mut busy = inflight > 0;
120 while let Some(message) = stream.next().await {
121 match message? {
122 Either::Left(Message::Barrier(barrier)) => {
123 match barrier.mutation.as_deref() {
124 Some(Mutation::Pause) => stream.pause_stream(),
125 Some(Mutation::Resume) => stream.resume_stream(),
126 _ => {}
127 }
128 barrier.assume_no_update_vnode_bitmap(self.actor.id)?;
130 state_table.commit(barrier.epoch).await?;
131 yield Message::Barrier(barrier);
132 if !busy {
133 inflight = ctx.install_next_batch(&state_table, &mut stream).await?;
134 busy = inflight > 0;
135 }
136 }
137 Either::Left(Message::Chunk(chunk)) => {
138 ctx.accept_assignments(&mut state_table, &chunk).await?;
139 if !busy {
140 inflight = ctx.install_next_batch(&state_table, &mut stream).await?;
141 busy = inflight > 0;
142 }
143 }
144 Either::Left(_) => bail!("unexpected Iceberg Fetch watermark"),
145 Either::Right(Err(e)) => {
146 tracing::error!(
147 error = %e.as_report(),
148 "Iceberg update Fetch errored, retrying"
149 );
150 ctx.scan_metrics.record_fetch_error();
151 inflight = 0;
154 ctx.scan_metrics.set_inflight_file_count(0);
155 busy = false;
156 stream.replace_data_stream(stream::pending());
157 }
158 Either::Right(Ok(FetchResult::Batch { state, chunk })) => {
159 ensure!(
160 chunk
161 .columns()
162 .iter()
163 .map(|column| column.data_type())
164 .collect::<Vec<_>>()
165 == output_types,
166 "Iceberg update output schema differs from its plan"
167 );
168 let table = state_table.state_table_mut();
169 let old = table
170 .get_row(task_pk(&state.assignment))
171 .await?
172 .context("missing Iceberg task state")?;
173 table.update(old, task_row(&state.assignment, &state)?);
174 if chunk.cardinality() != 0 {
175 yield Message::Chunk(chunk);
176 }
177 }
178 Either::Right(Ok(FetchResult::Finished(state))) => {
179 let table = state_table.state_table_mut();
182 if let Some(old) = table.get_row(task_pk(&state.assignment)).await? {
183 table.delete(old);
184 }
185 inflight = inflight.saturating_sub(1);
186 ctx.scan_metrics.set_inflight_file_count(inflight);
187 }
188 Either::Right(Ok(FetchResult::BatchFinished)) => {
189 inflight = ctx.install_next_batch(&state_table, &mut stream).await?;
190 busy = inflight > 0;
191 }
192 }
193 }
194 }
195}
196
197impl<S: StateStore> Execute for IcebergUpdateFetchExecutor<S> {
198 fn execute(self: Box<Self>) -> BoxedMessageStream {
199 self.into_stream().boxed()
200 }
201}
202
203impl<S: StateStore> Debug for IcebergUpdateFetchExecutor<S> {
204 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
205 f.debug_struct("IcebergUpdateFetchExecutor")
206 .finish_non_exhaustive()
207 }
208}
209
210type FetchStream = StreamReaderWithPause<true, StreamExecutorResult<FetchResult>>;
211
212struct FetchContext {
214 properties: IcebergProperties,
215 job_id: u32,
216 batch_size: usize,
217 chunk_size: usize,
218 output_columns: Vec<ColumnCatalog>,
219 key_indices: Vec<usize>,
220 scan_metrics: IcebergScanMetricsLabels,
221 file_scan_metrics: IcebergFileScanMetrics,
222}
223
224impl FetchContext {
225 async fn accept_assignments<S: StateStore>(
227 &self,
228 state_table: &mut SourceStateTableHandler<S>,
229 chunk: &StreamChunk,
230 ) -> StreamExecutorResult<()> {
231 let table = state_table.state_table_mut();
232 for (op, row) in chunk.rows() {
233 let assignment: IcebergUpdateAssignment = decode(row, |assignment| assignment)?;
234 ensure!(
235 assignment.job_id == self.job_id,
236 "Iceberg assignment belongs to another job"
237 );
238 assignment
239 .task
240 .validate_key(&self.output_columns, &self.key_indices)?;
241 match (op, table.get_row(task_pk(&assignment)).await?) {
242 (Op::Insert, None) => {
243 let state = IcebergUpdateFetchState::new(assignment);
244 table.insert(task_row(&state.assignment, &state)?);
245 }
246 (Op::Insert, Some(old)) => {
247 ensure!(
248 decode(&old, |state: &IcebergUpdateFetchState| &state.assignment)?
249 .assignment
250 == assignment,
251 "conflicting duplicate Iceberg assignment"
252 );
253 }
254 _ => bail!("invalid Iceberg assignment operation"),
255 }
256 }
257 state_table.try_flush().await
258 }
259
260 async fn install_next_batch<S: StateStore>(
264 &self,
265 state_table: &SourceStateTableHandler<S>,
266 reader: &mut FetchStream,
267 ) -> StreamExecutorResult<usize> {
268 let tasks = pending_tasks(
269 state_table,
270 self.job_id,
271 self.batch_size,
272 &self.output_columns,
273 &self.key_indices,
274 )
275 .await?;
276 let count = tasks.len();
277 if tasks.is_empty() {
278 reader.replace_data_stream(stream::pending());
279 } else {
280 reader.replace_data_stream(
281 fetch_work(
282 self.properties.clone(),
283 tasks,
284 self.chunk_size,
285 self.file_scan_metrics.clone(),
286 )
287 .map(Ok),
288 );
289 }
290 self.scan_metrics.set_inflight_file_count(count);
291 Ok(count)
292 }
293}
294
295enum FetchResult {
296 Batch {
297 state: IcebergUpdateFetchState,
298 chunk: StreamChunk,
299 },
300 Finished(IcebergUpdateFetchState),
301 BatchFinished,
302}
303
304#[try_stream(boxed, ok = FetchResult, error = StreamExecutorError)]
306async fn fetch_work(
307 properties: IcebergProperties,
308 tasks: Vec<IcebergUpdateFetchState>,
309 chunk_size: usize,
310 metrics: IcebergFileScanMetrics,
311) {
312 let table = properties.load_table().await?;
313 let mut reader = PositionDeleteReader::new(table.file_io());
314 for mut state in tasks {
315 let started = Instant::now();
316 let mut rows = 0;
317 let (scan, mode) = state.assignment.task.prepare_read(&table)?;
318 let mut batches = read_file_updates(
319 table.clone(),
320 &mut reader,
321 scan,
322 mode,
323 chunk_size,
324 state.next_position,
325 );
326 while let Some(batch) = batches.next().await {
327 let batch = batch?;
328 state.advance(batch.next_position, false)?;
329 rows += batch.chunk.cardinality() as u64;
330 metrics.record_read_bytes(batch.chunk.estimated_heap_size() as u64);
331 yield FetchResult::Batch {
332 state: state.clone(),
333 chunk: batch.chunk,
334 };
335 }
336 metrics.record_file_read_duration(started.elapsed().as_secs_f64());
337 if rows > 0 {
338 metrics.record_rows_read(rows);
339 }
340 metrics.record_file_read();
341 state.advance(state.assignment.task.record_count(), true)?;
342 yield FetchResult::Finished(state);
343 }
344 yield FetchResult::BatchFinished;
345}
346
347pub(super) async fn pending_tasks<S: StateStore>(
348 state: &SourceStateTableHandler<S>,
349 job_id: u32,
350 limit: usize,
351 columns: &[ColumnCatalog],
352 key_indices: &[usize],
353) -> StreamExecutorResult<Vec<IcebergUpdateFetchState>> {
354 ensure!(limit > 0, "Iceberg fetch batch size must be positive");
355 let table = state.state_table();
356 let mut tasks = vec![];
357 'vnodes: for vnode in table.vnodes().iter_vnodes() {
358 let iter = table
359 .iter_with_vnode(
360 vnode,
361 &(Bound::<OwnedRow>::Unbounded, Bound::<OwnedRow>::Unbounded),
362 PrefetchOptions::prefetch_for_small_range_scan(),
363 )
364 .await?;
365 pin_mut!(iter);
366 while let Some(row) = iter.next().await {
367 let task = decode(&row?, |task: &IcebergUpdateFetchState| &task.assignment)?;
368 ensure!(
369 task.assignment.job_id == job_id,
370 "Iceberg task belongs to another ingestion job"
371 );
372 task.assignment.task.validate_key(columns, key_indices)?;
373 ensure!(
374 !task.finished && task.next_position <= task.assignment.task.record_count(),
375 "invalid persisted Iceberg task cursor"
376 );
377 tasks.push(task);
378 if tasks.len() == limit {
379 break 'vnodes;
380 }
381 }
382 }
383 Ok(tasks)
384}