risingwave_connector/source/iceberg/
update_state.rs1use anyhow::Context;
26use iceberg::table::Table;
27use risingwave_common::catalog::ColumnCatalog;
28use risingwave_common::ensure;
29use serde::{Deserialize, Serialize};
30
31use super::update_planner::{
32 IcebergUpdateBinding, IcebergUpdateCursor, IcebergUpdatePhase, IcebergUpdatePlanner,
33 IcebergUpdateTask,
34};
35use crate::error::ConnectorResult;
36
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub struct IcebergUpdateAssignment {
39 pub job_id: u32,
41 pub route: i64,
43 pub seq: i64,
45 pub task: IcebergUpdateTask,
46}
47
48impl IcebergUpdateAssignment {
49 pub const ORDERED_ROUTE: i64 = -1;
51}
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54pub struct IcebergUpdateFetchState {
55 pub assignment: IcebergUpdateAssignment,
56 pub next_position: u64,
57 pub finished: bool,
58}
59
60impl IcebergUpdateFetchState {
61 pub fn new(assignment: IcebergUpdateAssignment) -> Self {
62 Self {
63 assignment,
64 next_position: 0,
65 finished: false,
66 }
67 }
68
69 pub fn advance(&mut self, next_position: u64, finished: bool) -> ConnectorResult<()> {
70 ensure!(
71 !self.finished
72 && next_position >= self.next_position
73 && next_position <= self.assignment.task.record_count(),
74 "invalid Iceberg fetch cursor advance"
75 );
76 ensure!(
77 !finished || next_position == self.assignment.task.record_count(),
78 "Iceberg task completion before EOF"
79 );
80 self.next_position = next_position;
81 self.finished = finished;
82 Ok(())
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum IcebergUpdateFenceKind {
89 Ordered,
91 FanOut,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
98pub struct IcebergUpdateFence {
99 epoch: u64,
102 ordered: bool,
105}
106
107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
108pub struct IcebergUpdateListState {
109 pub job_id: u32,
110 binding: IcebergUpdateBinding,
111 bootstrap_complete: bool,
112 dispatched_snapshot: Option<i64>,
114 cursor: Option<IcebergUpdateCursor>,
115 next_seq: i64,
116 fence: Option<IcebergUpdateFence>,
117}
118
119impl IcebergUpdateListState {
120 pub fn new(job_id: u32, binding: IcebergUpdateBinding) -> Self {
121 Self {
122 job_id,
123 binding,
124 bootstrap_complete: false,
125 dispatched_snapshot: None,
126 cursor: None,
127 next_seq: 0,
128 fence: None,
129 }
130 }
131
132 pub fn validate_columns(&self, job_id: u32, columns: &[ColumnCatalog]) -> ConnectorResult<()> {
133 ensure!(
134 self.job_id == job_id,
135 "Iceberg List job changed on recovery"
136 );
137 self.binding.validate_columns(columns)
138 }
139
140 pub fn wait_epoch(&self) -> Option<u64> {
142 self.fence
143 .filter(|fence| !fence.ordered)
144 .map(|fence| fence.epoch)
145 }
146
147 pub async fn enumerate(
152 &mut self,
153 table: &Table,
154 batch_size: usize,
155 ordered_max_tasks: usize,
156 planner: &mut Option<IcebergUpdatePlanner>,
157 ) -> ConnectorResult<(Vec<IcebergUpdateAssignment>, Option<IcebergUpdateFenceKind>)> {
158 ensure!(batch_size > 0, "Iceberg fetch batch size must be positive");
159 ensure!(
160 self.wait_epoch().is_none(),
161 "cannot enumerate until dispatched Iceberg tasks are applied"
162 );
163 let planner =
164 planner.get_or_insert_with(|| IcebergUpdatePlanner::new(self.binding.clone()));
165 if self.cursor.is_none() {
166 self.cursor =
167 planner.start_cursor(table, self.bootstrap_complete, self.dispatched_snapshot)?;
168 if self.cursor.is_none() {
169 self.bootstrap_complete = true;
172 return Ok((vec![], None));
173 }
174 }
175 let cursor = self.cursor.clone().expect("cursor set");
176 let page = planner.plan_page(table, &cursor, batch_size).await?;
177 if page.tasks.is_empty() {
178 ensure!(page.phase_finished, "empty non-final Iceberg page");
179 self.advance_phase();
180 return Ok((vec![], None));
181 }
182 let ordered = cursor.after_path.is_none()
185 && page.phase_finished
186 && page.tasks.len() <= ordered_max_tasks;
187 if !ordered && let Some(fence) = &mut self.fence {
188 planner.unread(cursor, page.tasks);
191 fence.ordered = false;
192 return Ok((vec![], None));
193 }
194 let first_seq = self.next_seq;
195 self.next_seq = i64::try_from(page.tasks.len())
196 .ok()
197 .and_then(|len| first_seq.checked_add(len))
198 .context("Iceberg task sequence overflow")?;
199 self.cursor.as_mut().expect("cursor set").after_path = Some(
200 page.tasks
201 .last()
202 .expect("nonempty page")
203 .id
204 .data_file_path
205 .clone(),
206 );
207 let assignments = page
208 .tasks
209 .into_iter()
210 .enumerate()
211 .map(|(offset, task)| (task, first_seq + offset as i64))
212 .map(|(task, seq)| IcebergUpdateAssignment {
213 job_id: self.job_id,
214 route: if ordered {
215 IcebergUpdateAssignment::ORDERED_ROUTE
216 } else {
217 seq
218 },
219 seq,
220 task,
221 })
222 .collect();
223 let fence = if ordered {
224 Some(IcebergUpdateFenceKind::Ordered)
225 } else {
226 page.phase_finished
227 .then_some(IcebergUpdateFenceKind::FanOut)
228 };
229 if page.phase_finished {
230 self.advance_phase();
231 }
232 Ok((assignments, fence))
233 }
234
235 pub fn record_fence(
237 &mut self,
238 kind: IcebergUpdateFenceKind,
239 epoch: u64,
240 ) -> ConnectorResult<()> {
241 let ordered = kind == IcebergUpdateFenceKind::Ordered;
242 ensure!(
243 match self.fence {
244 None => true,
245 Some(fence) => ordered && fence.ordered && fence.epoch <= epoch,
246 },
247 "invalid Iceberg dispatch fence"
248 );
249 self.fence = Some(IcebergUpdateFence { epoch, ordered });
250 Ok(())
251 }
252
253 pub fn complete_fence(&mut self, committed_epoch: u64) -> ConnectorResult<()> {
255 ensure!(
256 self.wait_epoch()
257 .is_some_and(|epoch| epoch <= committed_epoch),
258 "Iceberg completion snapshot predates dispatch"
259 );
260 self.fence = None;
261 Ok(())
262 }
263
264 fn advance_phase(&mut self) {
265 let mut cursor = self.cursor.take().expect("enumerated page has a cursor");
266 if cursor.phase == IcebergUpdatePhase::Delete {
267 cursor.phase = IcebergUpdatePhase::Insert;
268 cursor.after_path = None;
269 self.cursor = Some(cursor);
270 } else {
271 self.dispatched_snapshot = Some(cursor.snapshot_id);
272 self.bootstrap_complete = true;
273 self.cursor = None;
274 }
275 }
276}