risingwave_stream/executor/source/iceberg_update/
list.rs1use std::time::Duration;
18
19use either::Either;
20use futures::{TryStreamExt, stream};
21use risingwave_common::array::Op;
22use risingwave_common::bail;
23use risingwave_common::bitmap::Bitmap;
24use risingwave_common::catalog::{ColumnCatalog, ColumnId};
25use risingwave_common::config::StreamingConfig;
26use risingwave_common::hash::VnodeCountCompat;
27use risingwave_connector::source::iceberg::IcebergProperties;
28use risingwave_connector::source::iceberg::update_planner::{
29 IcebergUpdateBinding, IcebergUpdatePlanner,
30};
31use risingwave_connector::source::iceberg::update_state::{
32 IcebergUpdateAssignment, IcebergUpdateFenceKind, IcebergUpdateListState,
33};
34use risingwave_hummock_sdk::HummockReadEpoch;
35use risingwave_pb::plan_common::StorageTableDesc;
36use risingwave_storage::store::PrefetchOptions;
37use risingwave_storage::table::batch_table::BatchTable;
38use thiserror_ext::AsReport;
39use tokio::sync::mpsc::UnboundedReceiver;
40
41use super::{decode_value, encode, properties, scan_metrics, task_row, task_row_types};
42use crate::executor::prelude::*;
43use crate::executor::source::{StreamSourceCore, barrier_to_message_stream};
44use crate::executor::stream_reader::StreamReaderWithPause;
45
46pub struct IcebergUpdateListExecutor<S: StateStore> {
47 core: StreamSourceCore<S>,
48 downstream_columns: Vec<ColumnCatalog>,
49 fetch_state_table: StorageTableDesc,
50 barriers: UnboundedReceiver<Barrier>,
51 streaming_config: Arc<StreamingConfig>,
52}
53
54impl<S: StateStore> IcebergUpdateListExecutor<S> {
55 pub fn new(
56 core: StreamSourceCore<S>,
57 downstream_columns: Vec<ColumnCatalog>,
58 fetch_state_table: StorageTableDesc,
59 barriers: UnboundedReceiver<Barrier>,
60 streaming_config: Arc<StreamingConfig>,
61 ) -> Self {
62 Self {
63 core,
64 downstream_columns,
65 fetch_state_table,
66 barriers,
67 streaming_config,
68 }
69 }
70
71 #[try_stream(ok = Message, error = StreamExecutorError)]
72 async fn into_stream(mut self) {
73 let developer = &self.streaming_config.developer;
74 let list_interval = Duration::from_secs(developer.iceberg_list_interval_sec);
75 let batch_size = developer.iceberg_fetch_batch_size as usize;
76 let ordered_max_tasks = developer.iceberg_update_ordered_max_tasks as usize;
77 let properties = properties(&mut self.core)?;
78 let scan_metrics = scan_metrics(&self.core, &properties);
79 let fetch_reader = fetch_state_reader(
80 self.core
81 .split_state_store
82 .state_table()
83 .state_store()
84 .clone(),
85 &self.fetch_state_table,
86 );
87 let ctx = Arc::new(ListContext {
88 properties,
89 downstream_columns: self.downstream_columns,
90 batch_size,
91 ordered_max_tasks,
92 job_id: self.fetch_state_table.table_id.as_raw_id(),
93 fetch_reader,
94 });
95
96 let state_table = self.core.split_state_store.state_table_mut();
97 let mut barriers = barrier_to_message_stream(self.barriers).boxed();
98 let first = expect_first_barrier(&mut barriers).await?;
99 let first_epoch = first.epoch;
100 let paused = first.is_pause_on_startup();
101 yield Message::Barrier(first);
102 state_table.init_epoch(first_epoch).await?;
103
104 let mut persisted_state = state_table.get_from_one_value_table().await?;
105 let mut state = persisted_state
106 .as_ref()
107 .map(|value| decode_value::<IcebergUpdateListState>(value.as_jsonb().clone()))
108 .transpose()?;
109 if let Some(state) = &state {
110 state.validate_columns(ctx.job_id, &ctx.downstream_columns)?;
111 }
112 let mut dirty = false;
113 let mut planner = None;
114 let mut epoch = first_epoch.curr;
115 let mut checkpoint_epoch = first_epoch.prev;
117
118 let mut stream = ListStream::new(
119 barriers,
120 list_work(
121 ctx.clone(),
122 state.clone(),
123 None,
124 checkpoint_epoch,
125 Duration::ZERO,
126 )
127 .map(Ok),
128 );
129 if paused {
130 stream.pause_stream();
131 }
132 let mut next_poll = NextPoll::Running;
133 while let Some(message) = stream.next().await {
134 match message? {
135 Either::Left(Message::Barrier(barrier)) => {
136 match barrier.mutation.as_deref() {
137 Some(Mutation::Pause) => stream.pause_stream(),
138 Some(Mutation::Resume) => stream.resume_stream(),
139 _ => {}
140 }
141 if dirty {
142 write_list_state(
143 state_table,
144 &mut persisted_state,
145 state.as_ref().expect("dirty List state"),
146 )?;
147 dirty = false;
148 }
149 state_table
150 .commit_assert_no_update_vnode_bitmap(barrier.epoch)
151 .await?;
152 epoch = barrier.epoch.curr;
153 if barrier.is_checkpoint() {
154 checkpoint_epoch = barrier.epoch.prev;
155 }
156 yield Message::Barrier(barrier);
159 if let Some(idle_delay) = next_poll.idle_delay(list_interval) {
160 stream.replace_data_stream(
161 list_work(
162 ctx.clone(),
163 state.clone(),
164 planner.take(),
165 checkpoint_epoch,
166 idle_delay,
167 )
168 .map(Ok),
169 );
170 next_poll = NextPoll::Running;
171 }
172 }
173 Either::Left(_) => bail!("unexpected input to Iceberg update List"),
174 Either::Right(Err(e)) => {
175 tracing::warn!(
176 error = %e.as_report(),
177 "Iceberg update List errored, retrying"
178 );
179 scan_metrics.record_scan_error("list_error");
180 stream.replace_data_stream(stream::pending());
183 next_poll = NextPoll::Throttled;
184 }
185 Either::Right(Ok(ListResult {
186 state: mut new_state,
187 planner: next_planner,
188 assignments,
189 fence,
190 })) => {
191 planner = next_planner;
192 if let Some(kind) = fence {
193 new_state.record_fence(kind, epoch)?;
194 }
195 let changed = state.as_ref() != Some(&new_state);
196 dirty |= changed;
197 let waiting = new_state.wait_epoch().is_some();
198 next_poll = if !assignments.is_empty() && !waiting {
199 NextPoll::Running
202 } else if assignments.is_empty() && !changed && !waiting {
203 NextPoll::Throttled
204 } else {
205 NextPoll::Now
206 };
207 state = Some(new_state);
208 if !assignments.is_empty() {
209 yield Message::Chunk(assignments_chunk(&assignments)?);
210 }
211 if next_poll == NextPoll::Running {
212 stream.replace_data_stream(
213 list_work(
214 ctx.clone(),
215 state.clone(),
216 planner.take(),
217 checkpoint_epoch,
218 Duration::ZERO,
219 )
220 .map(Ok),
221 );
222 } else {
223 stream.replace_data_stream(stream::pending());
224 }
225 }
226 }
227 }
228 }
229}
230
231impl<S: StateStore> Execute for IcebergUpdateListExecutor<S> {
232 fn execute(self: Box<Self>) -> BoxedMessageStream {
233 self.into_stream().boxed()
234 }
235}
236
237impl<S: StateStore> Debug for IcebergUpdateListExecutor<S> {
238 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
239 f.debug_struct("IcebergUpdateListExecutor")
240 .finish_non_exhaustive()
241 }
242}
243
244type ListStream = StreamReaderWithPause<true, StreamExecutorResult<ListResult>>;
245
246#[derive(Clone, Copy, PartialEq, Eq)]
248enum NextPoll {
249 Running,
251 Now,
253 Throttled,
255}
256
257impl NextPoll {
258 fn idle_delay(self, list_interval: Duration) -> Option<Duration> {
260 match self {
261 NextPoll::Running => None,
262 NextPoll::Now => Some(Duration::ZERO),
263 NextPoll::Throttled => Some(list_interval),
264 }
265 }
266}
267
268pub(super) struct ListContext<S: StateStore> {
270 pub(super) properties: IcebergProperties,
271 pub(super) downstream_columns: Vec<ColumnCatalog>,
272 pub(super) batch_size: usize,
273 pub(super) ordered_max_tasks: usize,
274 pub(super) job_id: u32,
275 pub(super) fetch_reader: BatchTable<S>,
276}
277
278pub(super) struct ListResult {
279 pub(super) state: IcebergUpdateListState,
280 pub(super) planner: Option<IcebergUpdatePlanner>,
281 pub(super) assignments: Vec<IcebergUpdateAssignment>,
282 pub(super) fence: Option<IcebergUpdateFenceKind>,
283}
284
285#[try_stream(boxed, ok = ListResult, error = StreamExecutorError)]
288pub(super) async fn list_work<S: StateStore>(
289 ctx: Arc<ListContext<S>>,
290 state: Option<IcebergUpdateListState>,
291 mut planner: Option<IcebergUpdatePlanner>,
292 checkpoint_epoch: u64,
293 idle_delay: Duration,
294) {
295 let polls_catalog = state
296 .as_ref()
297 .is_none_or(|state| state.wait_epoch().is_none());
298 if polls_catalog && !idle_delay.is_zero() {
299 tokio::time::sleep(idle_delay).await;
300 }
301 let Some(mut state) = state else {
302 let table = ctx.properties.load_table().await?;
305 let binding = IcebergUpdateBinding::bind_columns(&table, &ctx.downstream_columns)?;
306 yield ListResult {
307 state: IcebergUpdateListState::new(ctx.job_id, binding),
308 planner: None,
309 assignments: vec![],
310 fence: None,
311 };
312 return Ok(());
313 };
314 let mut assignments = vec![];
315 let mut fence = None;
316 if let Some(dispatched_epoch) = state.wait_epoch() {
317 if checkpoint_epoch >= dispatched_epoch {
318 let rows = ctx
319 .fetch_reader
320 .batch_iter(
321 HummockReadEpoch::Committed(checkpoint_epoch),
322 false,
323 PrefetchOptions::prefetch_for_small_range_scan(),
324 )
325 .await?;
326 pin_mut!(rows);
327 if rows.try_next().await?.is_none() {
328 state.complete_fence(checkpoint_epoch)?;
329 }
330 }
331 } else {
332 let table = ctx.properties.load_table().await?;
333 (assignments, fence) = state
334 .enumerate(&table, ctx.batch_size, ctx.ordered_max_tasks, &mut planner)
335 .await?;
336 }
337 yield ListResult {
338 state,
339 planner,
340 assignments,
341 fence,
342 };
343}
344
345fn fetch_state_reader<S: StateStore>(store: S, desc: &StorageTableDesc) -> BatchTable<S> {
348 BatchTable::new_partial(
349 store,
350 vec![ColumnId::new(desc.columns[0].column_id)],
351 Some(Arc::new(Bitmap::ones(desc.vnode_count()))),
352 desc,
353 )
354}
355
356fn write_list_state<S: StateStore>(
358 table: &mut StateTable<S>,
359 persisted: &mut Option<ScalarImpl>,
360 state: &IcebergUpdateListState,
361) -> StreamExecutorResult<()> {
362 let value = ScalarImpl::Jsonb(encode(state)?);
363 if let Some(previous) = persisted.replace(value.clone()) {
364 table.update([Some(previous)], [Some(value)]);
365 } else {
366 table.insert([Some(value)]);
367 }
368 Ok(())
369}
370
371fn assignments_chunk(assignments: &[IcebergUpdateAssignment]) -> StreamExecutorResult<StreamChunk> {
372 let rows = assignments
373 .iter()
374 .map(|assignment| Ok((Op::Insert, task_row(assignment, assignment)?)))
375 .collect::<StreamExecutorResult<Vec<_>>>()?;
376 Ok(StreamChunk::from_rows(&rows, &task_row_types()))
377}