risingwave_stream/executor/source/
iceberg_list_executor.rs1use std::sync::Arc;
16
17use anyhow::anyhow;
18use either::Either;
19use futures_async_stream::try_stream;
20use parking_lot::Mutex;
21use risingwave_common::array::Op;
22use risingwave_common::catalog::ColumnCatalog;
23use risingwave_common::config::StreamingConfig;
24use risingwave_common::system_param::local_manager::SystemParamsReaderRef;
25use risingwave_connector::source::ConnectorProperties;
26use risingwave_connector::source::iceberg::{
27 IcebergIncrementalScan, IcebergScanMetricsLabels, IcebergScanPlanner, IcebergScanProjection,
28 PersistedFileScanTask,
29};
30use risingwave_connector::source::reader::desc::SourceDescBuilder;
31use thiserror_ext::AsReport;
32use tokio::sync::mpsc::UnboundedReceiver;
33
34use super::{StreamSourceCore, barrier_to_message_stream};
35use crate::executor::prelude::*;
36use crate::executor::stream_reader::StreamReaderWithPause;
37
38pub struct IcebergListExecutor<S: StateStore> {
39 actor_ctx: ActorContextRef,
40
41 stream_source_core: StreamSourceCore<S>,
43
44 downstream_columns: Option<Vec<ColumnCatalog>>,
47
48 #[expect(dead_code)]
50 metrics: Arc<StreamingMetrics>,
51
52 barrier_receiver: Option<UnboundedReceiver<Barrier>>,
54
55 #[expect(dead_code)]
57 system_params: SystemParamsReaderRef,
58
59 #[expect(dead_code)]
61 rate_limit_rps: Option<u32>,
62
63 streaming_config: Arc<StreamingConfig>,
65}
66
67impl<S: StateStore> IcebergListExecutor<S> {
68 #[expect(clippy::too_many_arguments)]
69 pub fn new(
70 actor_ctx: ActorContextRef,
71 stream_source_core: StreamSourceCore<S>,
72 downstream_columns: Option<Vec<ColumnCatalog>>,
73 metrics: Arc<StreamingMetrics>,
74 barrier_receiver: UnboundedReceiver<Barrier>,
75 system_params: SystemParamsReaderRef,
76 rate_limit_rps: Option<u32>,
77 streaming_config: Arc<StreamingConfig>,
78 ) -> Self {
79 Self {
80 actor_ctx,
81 stream_source_core,
82 downstream_columns,
83 metrics,
84 barrier_receiver: Some(barrier_receiver),
85 system_params,
86 rate_limit_rps,
87 streaming_config,
88 }
89 }
90
91 #[try_stream(ok = Message, error = StreamExecutorError)]
92 async fn into_stream(mut self) {
93 let mut barrier_receiver = self.barrier_receiver.take().unwrap();
94 let first_barrier = barrier_receiver
95 .recv()
96 .instrument_await("source_recv_first_barrier")
97 .await
98 .ok_or_else(|| {
99 anyhow!(
100 "failed to receive the first barrier, actor_id: {:?}, source_id: {:?}",
101 self.actor_ctx.id,
102 self.stream_source_core.source_id
103 )
104 })?;
105 let first_epoch = first_barrier.epoch;
106
107 let source_desc_builder: SourceDescBuilder =
109 self.stream_source_core.source_desc_builder.take().unwrap();
110
111 let properties = source_desc_builder.with_properties();
112 let config = ConnectorProperties::extract(properties, false)?;
113 let ConnectorProperties::Iceberg(iceberg_properties) = config else {
114 unreachable!()
115 };
116
117 let scan_projection =
118 IcebergScanProjection::from_downstream_columns(self.downstream_columns.as_deref());
119
120 tracing::debug!("scan_projection: {:?}", scan_projection);
121
122 yield Message::Barrier(first_barrier);
123 let barrier_stream = barrier_to_message_stream(barrier_receiver).boxed();
124
125 let source_id_str = self.stream_source_core.source_id.to_string();
126 let source_name_str = self.stream_source_core.source_name.clone();
127 let table_name = iceberg_properties.table.table_name().to_owned();
128 let scan_metrics = IcebergScanMetricsLabels::new(
129 source_id_str.clone(),
130 source_name_str.clone(),
131 table_name,
132 );
133 let scan_planner = IcebergScanPlanner::new(
134 (*iceberg_properties).clone(),
135 scan_projection,
136 Some(scan_metrics.clone()),
137 );
138
139 let state_table = self.stream_source_core.split_state_store.state_table_mut();
140 state_table.init_epoch(first_epoch).await?;
141 let state_row = state_table.get_from_one_value_table().await?;
142 let mut last_snapshot: Option<i64> = state_row.map(|s| *s.as_int64());
144 let mut prev_persisted_snapshot = last_snapshot;
145
146 if last_snapshot.is_none() {
147 if let Some(snapshot_plan) = scan_planner.plan_current_snapshot().await? {
151 last_snapshot = Some(snapshot_plan.snapshot_id);
152 let mut chunk_builder = StreamChunkBuilder::new(
153 self.streaming_config.developer.chunk_size,
154 vec![DataType::Varchar, DataType::Jsonb],
155 );
156 #[for_await]
157 for scan_task in snapshot_plan.tasks {
158 let scan_task = scan_task?;
159 let data_file_path = scan_task.data_file_path.clone();
160 let persisted_task = PersistedFileScanTask::encode(scan_task)?;
161 if let Some(chunk) = chunk_builder.append_row(
162 Op::Insert,
163 &[
164 Some(ScalarImpl::Utf8(data_file_path.into())),
165 Some(ScalarImpl::Jsonb(persisted_task)),
166 ],
167 ) {
168 yield Message::Chunk(chunk);
169 }
170 }
171 if let Some(chunk) = chunk_builder.take() {
172 yield Message::Chunk(chunk);
173 }
174 }
175 }
176
177 let last_snapshot = Arc::new(Mutex::new(last_snapshot));
178 let build_incremental_stream = || {
179 incremental_scan_stream(
180 scan_planner.clone(),
181 last_snapshot.clone(),
182 self.streaming_config.developer.iceberg_list_interval_sec,
183 )
184 .map(|res| match res {
185 Ok(scan_task) => {
186 let data_file_path = scan_task.data_file_path.clone();
187 let persisted_task = PersistedFileScanTask::encode(scan_task)?;
188 let row = (
189 Op::Insert,
190 OwnedRow::new(vec![
191 Some(ScalarImpl::Utf8(data_file_path.into())),
192 Some(ScalarImpl::Jsonb(persisted_task)),
193 ]),
194 );
195 Ok(StreamChunk::from_rows(
196 &[row],
197 &[DataType::Varchar, DataType::Jsonb],
198 ))
199 }
200 Err(e) => Err(e),
201 })
202 };
203
204 let mut stream =
205 StreamReaderWithPause::<true, _>::new(barrier_stream, build_incremental_stream());
206
207 while let Some(msg) = stream.next().await {
210 match msg {
211 Err(e) => {
212 tracing::warn!(
213 error = %e.as_report(),
214 "incremental iceberg list stream errored, rebuilding"
215 );
216 scan_metrics.record_scan_error("list_error");
217 stream.replace_data_stream(build_incremental_stream());
218 }
219 Ok(msg) => match msg {
220 Either::Left(msg) => match &msg {
222 Message::Barrier(barrier) => {
223 if let Some(mutation) = barrier.mutation.as_deref() {
224 match mutation {
225 Mutation::Pause => stream.pause_stream(),
226 Mutation::Resume => stream.resume_stream(),
227 _ => (),
228 }
229 }
230 if let Some(last_snapshot) = *last_snapshot.lock() {
231 let state_row =
232 OwnedRow::new(vec![ScalarImpl::Int64(last_snapshot).into()]);
233 if let Some(prev_persisted_snapshot_id) = prev_persisted_snapshot {
234 let prev_state_row = OwnedRow::new(vec![
235 ScalarImpl::Int64(prev_persisted_snapshot_id).into(),
236 ]);
237 state_table.update(prev_state_row, state_row);
238 } else {
239 state_table.insert(state_row);
240 }
241 prev_persisted_snapshot = Some(last_snapshot);
242 }
243 state_table
244 .commit_assert_no_update_vnode_bitmap(barrier.epoch)
245 .await?;
246 yield msg;
248 }
249 _ => unreachable!(),
251 },
252 Either::Right(chunk) => {
254 yield Message::Chunk(chunk);
255 }
256 },
257 }
258 }
259 }
260}
261
262#[try_stream(
264 boxed,
265 ok = iceberg::scan::FileScanTask,
266 error = StreamExecutorError
267)]
268async fn incremental_scan_stream(
269 scan_planner: IcebergScanPlanner,
270 last_snapshot_lock: Arc<Mutex<Option<i64>>>,
271 list_interval_sec: u64,
272) {
273 let mut last_snapshot: Option<i64> = *last_snapshot_lock.lock();
274 loop {
275 tokio::time::sleep(std::time::Duration::from_secs(list_interval_sec)).await;
276
277 match scan_planner.plan_incremental(last_snapshot).await? {
278 IcebergIncrementalScan::EmptyTable | IcebergIncrementalScan::UpToDate { .. } => {}
279 IcebergIncrementalScan::Planned(plan) => {
280 #[for_await]
281 for scan_task in plan.tasks {
282 yield scan_task?;
283 }
284
285 last_snapshot = Some(plan.snapshot_id);
286 *last_snapshot_lock.lock() = last_snapshot;
287 scan_planner.record_caught_up();
288 }
289 }
290 }
291}
292
293impl<S: StateStore> Execute for IcebergListExecutor<S> {
294 fn execute(self: Box<Self>) -> BoxedMessageStream {
295 self.into_stream().boxed()
296 }
297}
298
299impl<S: StateStore> Debug for IcebergListExecutor<S> {
300 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
301 f.debug_struct("IcebergListExecutor")
302 .field("source_id", &self.stream_source_core.source_id)
303 .field("column_ids", &self.stream_source_core.column_ids)
304 .finish()
305 }
306}