risingwave_stream/executor/iceberg_with_pk_index/
writer.rs1use anyhow::Context;
16use iceberg::writer::PositionDeleteInput;
17use risingwave_common::array::DataChunk;
18use risingwave_common::array::stream_record::Record;
19use risingwave_common::id::SinkId;
20use risingwave_common::row::{Project, RowExt};
21use risingwave_common::util::chunk_coalesce::DataChunkBuilder;
22use risingwave_common::util::iter_util::ZipEqFast;
23use risingwave_pb::connector_service::SinkMetadata;
24use risingwave_pb::stream_service::PbIcebergPkIndexSinkRole;
25use risingwave_storage::StateStore;
26
27use crate::common::change_buffer::output_kind;
28use crate::common::compact_chunk::{InconsistencyBehavior, compact_chunk_inline};
29use crate::executor::prelude::*;
30use crate::task::LocalBarrierManager;
31
32type PkRow<'a> = Project<'a, RowRef<'a>>;
33
34fn new_chunk_builder(chunk_size: usize) -> DataChunkBuilder {
35 DataChunkBuilder::new(vec![DataType::Varchar, DataType::Int64], chunk_size)
36}
37
38fn append_row(builder: &mut DataChunkBuilder, file_path: &str, position: i64) -> Option<DataChunk> {
39 builder.append_one_row([
40 Some(ScalarRefImpl::Utf8(file_path)),
41 Some(ScalarRefImpl::Int64(position)),
42 ])
43}
44
45#[async_trait::async_trait]
50pub trait IcebergWriter: Send + 'static {
51 async fn write_chunk(
53 &mut self,
54 chunk: DataChunk,
55 ) -> StreamExecutorResult<Vec<PositionDeleteInput>>;
56
57 async fn flush(&mut self) -> StreamExecutorResult<Option<SinkMetadata>>;
60}
61
62pub struct WriterExecutor<S, W>
76where
77 S: StateStore,
78 W: IcebergWriter,
79{
80 ctx: ActorContextRef,
81 input: Option<Executor>,
82 pk_indices: Vec<usize>,
84 pk_index_state_table: StateTable<S>,
87 writer: W,
89 delete_position_buffer: Option<DataChunkBuilder>,
91 chunk_size: usize,
92 sink_id: SinkId,
93 local_barrier_manager: LocalBarrierManager,
94}
95
96impl<S, W> WriterExecutor<S, W>
97where
98 S: StateStore,
99 W: IcebergWriter,
100{
101 #[allow(clippy::too_many_arguments)]
102 pub fn new(
103 ctx: ActorContextRef,
104 input: Executor,
105 pk_indices: Vec<usize>,
106 pk_index_state_table: StateTable<S>,
107 writer: W,
108 chunk_size: usize,
109 sink_id: SinkId,
110 local_barrier_manager: LocalBarrierManager,
111 ) -> Self {
112 Self {
113 ctx,
114 input: Some(input),
115 pk_indices,
116 pk_index_state_table,
117 writer,
118 delete_position_buffer: None,
119 chunk_size,
120 sink_id,
121 local_barrier_manager,
122 }
123 }
124
125 async fn delete_existing_row(
126 &mut self,
127 pk_row: PkRow<'_>,
128 delete_position_buffer: &mut DataChunkBuilder,
129 ) -> StreamExecutorResult<Option<DataChunk>> {
130 let Some(index_row) = self.pk_index_state_table.get_row(pk_row).await? else {
131 return Ok(None);
132 };
133
134 let num_cols = index_row.len();
135 let file_path = index_row
136 .datum_at(num_cols - 2)
137 .context("file_path should not be null")?
138 .into_utf8();
139 let position = index_row
140 .datum_at(num_cols - 1)
141 .context("position should not be null")?
142 .into_int64();
143 let chunk = append_row(delete_position_buffer, file_path, position);
144 self.pk_index_state_table.delete(index_row);
145 Ok(chunk)
146 }
147
148 #[try_stream(ok = DataChunk, error = StreamExecutorError)]
163 async fn process_chunk(&mut self, chunk: StreamChunk) {
164 let chunk = compact_chunk_inline::<{ output_kind::RETRACT }>(
165 chunk,
166 &self.pk_indices,
167 InconsistencyBehavior::Panic,
168 );
169
170 let mut delete_position_buffer = self
171 .delete_position_buffer
172 .take()
173 .unwrap_or_else(|| new_chunk_builder(self.chunk_size));
174 let pk_indices = self.pk_indices.clone();
175
176 let mut insert_chunk =
184 DataChunkBuilder::new(chunk.data_chunk().data_types(), chunk.capacity() + 1);
185 let mut insert_pks: Vec<PkRow<'_>> = Vec::new();
186
187 for record in chunk.records() {
188 match record {
189 Record::Insert { new_row } => {
190 let overflow = insert_chunk.append_one_row(new_row);
191 debug_assert!(overflow.is_none(), "insert chunk exceeds capacity");
192 insert_pks.push(new_row.project(&pk_indices));
193 }
194 Record::Delete { old_row } => {
195 let pk_row = old_row.project(&pk_indices);
196 if let Some(chunk) = self
197 .delete_existing_row(pk_row, &mut delete_position_buffer)
198 .await?
199 {
200 yield chunk;
201 }
202 }
203 Record::Update { new_row, .. } => {
204 let pk_row = new_row.project(&pk_indices);
206 if let Some(chunk) = self
207 .delete_existing_row(pk_row, &mut delete_position_buffer)
208 .await?
209 {
210 yield chunk;
211 }
212 let overflow = insert_chunk.append_one_row(new_row);
213 debug_assert!(overflow.is_none(), "insert chunk exceeds capacity");
214 insert_pks.push(pk_row);
215 }
216 }
217 }
218
219 if !insert_pks.is_empty() {
220 let write_chunk = insert_chunk.finish();
221 let positions = self.writer.write_chunk(write_chunk).await?;
222
223 for (pk, pos) in insert_pks.into_iter().zip_eq_fast(positions) {
224 let mut index_row_data = Vec::with_capacity(pk_indices.len() + 2);
225 for datum in pk.iter() {
226 index_row_data.push(datum);
227 }
228 index_row_data.push(Some(ScalarRefImpl::Utf8(&pos.path)));
229 index_row_data.push(Some(ScalarRefImpl::Int64(pos.pos)));
230 self.pk_index_state_table.insert(index_row_data.as_slice());
231 }
232 }
233
234 self.delete_position_buffer = Some(delete_position_buffer);
235 self.pk_index_state_table.try_flush().await?;
236 }
237
238 #[try_stream(ok = Message, error = StreamExecutorError)]
239 async fn execute_inner(mut self) {
240 let mut input = self.input.take().unwrap().execute();
241
242 let barrier = expect_first_barrier(&mut input).await?;
244 let first_epoch = barrier.epoch;
245
246 yield Message::Barrier(barrier);
247 self.pk_index_state_table.init_epoch(first_epoch).await?;
248
249 #[for_await]
250 for msg in input {
251 match msg? {
252 Message::Chunk(chunk) =>
253 {
254 #[for_await]
255 for data_chunk in self.process_chunk(chunk) {
256 yield Message::Chunk(data_chunk?.into());
257 }
258 }
259 Message::Barrier(barrier) => {
260 barrier.assume_no_update_vnode_bitmap(self.ctx.id)?;
261
262 let mut metadata = None;
263 if barrier.is_checkpoint() {
264 if let Some(chunk) = self
265 .delete_position_buffer
266 .take()
267 .and_then(|mut b| b.consume_all())
268 {
269 yield Message::Chunk(chunk.into());
270 }
271 metadata = self.writer.flush().await?;
272 }
273
274 self.pk_index_state_table
275 .commit_assert_no_update_vnode_bitmap(barrier.epoch)
276 .await?;
277
278 if let Some(metadata) = metadata
279 && metadata.metadata.is_some()
280 {
281 self.local_barrier_manager
282 .report_iceberg_pk_index_sink_metadata(
283 barrier.epoch,
284 self.sink_id,
285 self.ctx.id,
286 PbIcebergPkIndexSinkRole::Writer,
287 Some(metadata),
288 );
289 }
290
291 yield Message::Barrier(barrier);
292 }
293 Message::Watermark(w) => {
294 yield Message::Watermark(w);
295 }
296 }
297 }
298 }
299}
300
301impl<S, W> Execute for WriterExecutor<S, W>
302where
303 S: StateStore,
304 W: IcebergWriter,
305{
306 fn execute(self: Box<Self>) -> BoxedMessageStream {
307 self.execute_inner().boxed()
308 }
309}
310
311#[cfg(test)]
312#[path = "writer_test.rs"]
313mod tests;