Skip to main content

risingwave_stream/executor/
sink.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::{BTreeMap, HashMap};
16use std::{assert_matches, mem};
17
18use anyhow::anyhow;
19use futures::stream::select;
20use futures::{FutureExt, TryFutureExt, TryStreamExt};
21use itertools::Itertools;
22use risingwave_common::array::Op;
23use risingwave_common::array::stream_chunk::StreamChunkMut;
24use risingwave_common::bitmap::Bitmap;
25use risingwave_common::catalog::{ColumnCatalog, Field};
26use risingwave_common::metrics::{GLOBAL_ERROR_METRICS, LabelGuardedIntGauge};
27use risingwave_common_estimate_size::EstimateSize;
28use risingwave_common_estimate_size::collections::EstimatedVec;
29use risingwave_common_rate_limit::RateLimit;
30use risingwave_connector::dispatch_sink;
31use risingwave_connector::sink::catalog::{SinkId, SinkType};
32use risingwave_connector::sink::log_store::{
33    FlushCurrentEpochOptions, LogReader, LogReaderExt, LogReaderMetrics, LogStoreFactory,
34    LogWriter, LogWriterExt, LogWriterMetrics,
35};
36use risingwave_connector::sink::{
37    GLOBAL_SINK_METRICS, LogSinker, SINK_USER_FORCE_COMPACTION,
38    SINK_USER_PRESERVE_ROW_LEVEL_CHANGES, Sink, SinkImpl, SinkParam, SinkWriterParam,
39};
40use risingwave_pb::common::ThrottleType;
41use risingwave_pb::id::FragmentId;
42use risingwave_pb::stream_plan::stream_node::StreamKind;
43use thiserror_ext::AsReport;
44use tokio::select;
45use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
46use tokio::sync::oneshot;
47
48use crate::common::change_buffer::{OutputKind, output_kind};
49use crate::common::compact_chunk::{
50    InconsistencyBehavior, StreamChunkCompactor, compact_chunk_inline,
51};
52use crate::executor::prelude::*;
53pub struct SinkExecutor<F: LogStoreFactory> {
54    actor_context: ActorContextRef,
55    info: ExecutorInfo,
56    input: Executor,
57    sink: SinkImpl,
58    input_columns: Vec<ColumnCatalog>,
59    sink_param: SinkParam,
60    log_store_factory: F,
61    sink_writer_param: SinkWriterParam,
62    chunk_size: usize,
63    input_data_types: Vec<DataType>,
64    non_append_only_behavior: Option<NonAppendOnlyBehavior>,
65    rate_limit: Option<u32>,
66}
67
68// Drop all the DELETE messages in this chunk and convert UPDATE INSERT into INSERT.
69fn force_append_only(c: StreamChunk) -> StreamChunk {
70    let mut c: StreamChunkMut = c.into();
71    for (_, mut r) in c.to_rows_mut() {
72        match r.op() {
73            Op::Insert => {}
74            Op::Delete | Op::UpdateDelete => r.set_vis(false),
75            Op::UpdateInsert => r.set_op(Op::Insert),
76        }
77    }
78    c.into()
79}
80
81// Drop all the INSERT messages in this chunk and convert UPDATE DELETE into DELETE.
82fn force_delete_only(c: StreamChunk) -> StreamChunk {
83    let mut c: StreamChunkMut = c.into();
84    for (_, mut r) in c.to_rows_mut() {
85        match r.op() {
86            Op::Delete => {}
87            Op::Insert | Op::UpdateInsert => r.set_vis(false),
88            Op::UpdateDelete => r.set_op(Op::Delete),
89        }
90    }
91    c.into()
92}
93
94/// When the sink is non-append-only, i.e. upsert or retract, we need to do some extra work for
95/// correctness and performance.
96#[derive(Clone, Copy, Debug)]
97struct NonAppendOnlyBehavior {
98    /// Whether the user specifies a primary key for the sink, and it matches the derived stream
99    /// key of the stream.
100    ///
101    /// By matching, we mean that stream key is a subset of the downstream pk.
102    pk_specified_and_matched: bool,
103    /// Whether the user forces buffering all chunks between two barriers.
104    force_compaction: bool,
105}
106
107impl NonAppendOnlyBehavior {
108    /// NOTE(kwannoel):
109    ///
110    /// After the optimization in <https://github.com/risingwavelabs/risingwave/pull/12250>,
111    /// `DELETE`s will be sequenced before `INSERT`s in JDBC sinks and PG rust sink.
112    /// There's a risk that adjacent chunks with `DELETE`s on the same PK will get
113    /// merged into a single chunk, since the logstore format doesn't preserve chunk
114    /// boundaries. Then we will have double `DELETE`s followed by unspecified sequence
115    /// of `INSERT`s, and lead to inconsistent data downstream.
116    ///
117    /// We only need to do the compaction for non-append-only sinks, when the upstream and
118    /// downstream PKs are matched. When the upstream and downstream PKs are not matched,
119    /// we will buffer the chunks between two barriers, so the compaction is not needed,
120    /// since the barriers will preserve chunk boundaries.
121    ///
122    /// When `force_compaction` is true, we also skip compaction here, since the buffering
123    /// will also make compaction.
124    ///
125    /// When the sink is an append-only sink, it is either `force_append_only` or
126    /// `append_only`, we should only append to downstream, so there should not be any
127    /// overlapping keys.
128    fn should_compact_in_log_reader(self) -> bool {
129        self.pk_specified_and_matched && !self.force_compaction
130    }
131
132    /// When stream key is different from the user defined primary key columns for sinks.
133    /// The operations could be out of order.
134    ///
135    /// For example, we have a stream with derived stream key `a, b` and user-specified sink
136    /// primary key `a`. Assume that we have `(1, 1)` in the table. Then, we perform two `UPDATE`
137    /// operations:
138    ///
139    /// ```text
140    /// UPDATE SET b = 2 WHERE a = 1 ... which issues:
141    ///   - (1, 1)
142    ///   + (1, 2)
143    ///
144    /// UPDATE SET b = 3 WHERE a = 1 ... which issues:
145    ///   - (1, 2)
146    ///   + (1, 3)
147    /// ```
148    ///
149    /// When these changes go into streaming pipeline, they could be shuffled to different parallelism
150    /// (actor), given that they are under different stream keys.
151    ///
152    /// ```text
153    /// Actor 1:
154    /// - (1, 1)
155    ///
156    /// Actor 2:
157    /// + (1, 2)
158    /// - (1, 2)
159    ///
160    /// Actor 3:
161    /// + (1, 3)
162    /// ```
163    ///
164    /// When these records are merged back into sink actor, we may get the records from different
165    /// parallelism in arbitrary order, like:
166    ///
167    /// ```text
168    /// + (1, 2) -- Actor 2, first row
169    /// + (1, 3) -- Actor 3
170    /// - (1, 1) -- Actor 1
171    /// - (1, 2) -- Actor 2, second row
172    /// ```
173    ///
174    /// Note that in terms of stream key (`a, b`), the operations in the order above are completely
175    /// correct, because we are operating on 3 different rows. However, in terms of user defined sink
176    /// primary key `a`, we're violating the unique constraint all the time.
177    ///
178    /// Therefore, in this case, we have to do additional reordering in the sink executor per barrier.
179    /// Specifically, we need to:
180    ///
181    /// First, compact all the changes with the stream key, so we have:
182    /// ```text
183    /// + (1, 3)
184    /// - (1, 1)
185    /// ```
186    ///
187    /// Then, sink all the delete events before sinking all insert events, so we have:
188    /// ```text
189    /// - (1, 1)
190    /// + (1, 3)
191    /// ```
192    /// Since we've compacted the chunk with the stream key, the `DELETE` records survived must be to
193    /// delete an existing row, so we can safely move them to the front. After the deletion is done,
194    /// we can then safely sink the insert events with uniqueness guarantee.
195    ///
196    /// When `force_compaction` is true, we also perform additional reordering to gain the
197    /// benefits of compaction:
198    /// - reduce the number of output messages;
199    /// - emit at most one update per key within a barrier interval, simplifying downstream logic.
200    fn should_reorder_records(self) -> bool {
201        !self.pk_specified_and_matched || self.force_compaction
202    }
203}
204
205/// Get the output kind for chunk compaction based on the given sink type.
206fn compact_output_kind(sink_type: SinkType) -> OutputKind {
207    match sink_type {
208        SinkType::Upsert => output_kind::UPSERT,
209        SinkType::Retract => output_kind::RETRACT,
210        // There won't be any `Update` or `Delete` in the chunk, so it doesn't matter.
211        SinkType::AppendOnly => output_kind::RETRACT,
212    }
213}
214
215/// Dispatch the code block to different output kinds for chunk compaction based on sink type.
216macro_rules! dispatch_output_kind {
217    ($sink_type:expr, $KIND:ident, $body:tt) => {
218        #[allow(unused_braces)]
219        match compact_output_kind($sink_type) {
220            output_kind::UPSERT => {
221                const KIND: OutputKind = output_kind::UPSERT;
222                $body
223            }
224            output_kind::RETRACT => {
225                const KIND: OutputKind = output_kind::RETRACT;
226                $body
227            }
228        }
229    };
230}
231
232impl<F: LogStoreFactory> SinkExecutor<F> {
233    #[expect(clippy::too_many_arguments)]
234    pub fn new(
235        actor_context: ActorContextRef,
236        info: ExecutorInfo,
237        input: Executor,
238        sink_writer_param: SinkWriterParam,
239        sink: SinkImpl,
240        sink_param: SinkParam,
241        columns: Vec<ColumnCatalog>,
242        log_store_factory: F,
243        chunk_size: usize,
244        input_data_types: Vec<DataType>,
245        rate_limit: Option<u32>,
246    ) -> StreamExecutorResult<Self> {
247        let sink_input_schema: Schema = columns
248            .iter()
249            .map(|column| Field::from(&column.column_desc))
250            .collect();
251
252        if let Some(col_dix) = sink_writer_param.extra_partition_col_idx {
253            // Remove the partition column from the schema.
254            assert_eq!(sink_input_schema.data_types(), {
255                let mut data_type = info.schema.data_types();
256                data_type.remove(col_dix);
257                data_type
258            });
259        } else {
260            assert_eq!(sink_input_schema.data_types(), info.schema.data_types());
261        }
262
263        let non_append_only_behavior = if !sink_param.sink_type.is_append_only() {
264            let stream_key = &info.stream_key;
265            let pk_specified_and_matched = (sink_param.downstream_pk.as_ref())
266                .is_some_and(|downstream_pk| stream_key.iter().all(|i| downstream_pk.contains(i)));
267            let force_compaction = sink_param
268                .properties
269                .get(SINK_USER_FORCE_COMPACTION)
270                .map(|v| v.eq_ignore_ascii_case("true"))
271                .unwrap_or(false);
272            Some(NonAppendOnlyBehavior {
273                pk_specified_and_matched,
274                force_compaction,
275            })
276        } else {
277            None
278        };
279
280        tracing::info!(
281            sink_id = %sink_param.sink_id,
282            actor_id = %actor_context.id,
283            ?non_append_only_behavior,
284            "Sink executor info"
285        );
286
287        Ok(Self {
288            actor_context,
289            info,
290            input,
291            sink,
292            input_columns: columns,
293            sink_param,
294            log_store_factory,
295            sink_writer_param,
296            chunk_size,
297            input_data_types,
298            non_append_only_behavior,
299            rate_limit,
300        })
301    }
302
303    fn execute_inner(self) -> BoxedMessageStream {
304        let sink_id = self.sink_param.sink_id;
305        let actor_id = self.actor_context.id;
306        let fragment_id = self.actor_context.fragment_id;
307
308        let stream_key = self.info.stream_key.clone();
309        let metrics = self.actor_context.streaming_metrics.new_sink_exec_metrics(
310            sink_id,
311            actor_id,
312            fragment_id,
313        );
314
315        // When processing upsert stream, we need to tolerate the inconsistency (mismatched `DELETE`
316        // and `INSERT` pairs) when compacting input chunks with derived stream key.
317        let input_compact_ib = if self.input.stream_kind() == StreamKind::Upsert {
318            InconsistencyBehavior::Tolerate
319        } else {
320            InconsistencyBehavior::Panic
321        };
322
323        let input = self.input.execute();
324
325        let input = input.inspect_ok(move |msg| {
326            if let Message::Chunk(c) = msg {
327                metrics.sink_input_row_count.inc_by(c.capacity() as u64);
328                metrics.sink_input_bytes.inc_by(c.estimated_size() as u64);
329            }
330        });
331
332        let processed_input = Self::process_msg(
333            input,
334            self.sink_param.sink_type,
335            stream_key,
336            self.chunk_size,
337            self.input_data_types,
338            input_compact_ib,
339            self.sink_param.downstream_pk.clone(),
340            self.non_append_only_behavior,
341            metrics.sink_chunk_buffer_size,
342            self.sink_param
343                .properties
344                .get(SINK_USER_PRESERVE_ROW_LEVEL_CHANGES)
345                .is_some_and(|v| v.eq_ignore_ascii_case("true")),
346            self.sink.is_blackhole(), // skip compact for blackhole for better benchmark results
347        );
348
349        let processed_input = if self.sink_param.ignore_delete {
350            // Drop UPDATE/DELETE messages if specified `ignore_delete` (formerly `force_append_only`).
351            processed_input
352                .map_ok(|msg| match msg {
353                    Message::Chunk(chunk) => Message::Chunk(force_append_only(chunk)),
354                    other => other,
355                })
356                .left_stream()
357        } else {
358            processed_input.right_stream()
359        };
360
361        if self.sink.is_sink_into_table() {
362            // TODO(hzxa21): support rate limit?
363            processed_input.boxed()
364        } else {
365            let labels = [
366                &actor_id.to_string(),
367                &sink_id.to_string(),
368                self.sink_param.sink_name.as_str(),
369            ];
370            let log_store_first_write_epoch = GLOBAL_SINK_METRICS
371                .log_store_first_write_epoch
372                .with_guarded_label_values(&labels);
373            let log_store_latest_write_epoch = GLOBAL_SINK_METRICS
374                .log_store_latest_write_epoch
375                .with_guarded_label_values(&labels);
376            let log_store_write_rows = GLOBAL_SINK_METRICS
377                .log_store_write_rows
378                .with_guarded_label_values(&labels);
379            let log_writer_metrics = LogWriterMetrics {
380                log_store_first_write_epoch,
381                log_store_latest_write_epoch,
382                log_store_write_rows,
383            };
384
385            let (rate_limit_tx, rate_limit_rx) = unbounded_channel();
386            // Init the rate limit
387            rate_limit_tx.send(self.rate_limit.into()).unwrap();
388
389            let (rebuild_sink_tx, rebuild_sink_rx) = unbounded_channel();
390
391            self.log_store_factory
392                .build()
393                .map(move |(log_reader, log_writer)| {
394                    let write_log_stream = Self::execute_write_log(
395                        processed_input,
396                        log_writer.monitored(log_writer_metrics),
397                        actor_id,
398                        fragment_id,
399                        sink_id,
400                        rate_limit_tx,
401                        rebuild_sink_tx,
402                    );
403
404                    let consume_log_stream_future = dispatch_sink!(self.sink, sink, {
405                        let consume_log_stream = Self::execute_consume_log(
406                            *sink,
407                            log_reader,
408                            self.input_columns,
409                            self.sink_param,
410                            self.sink_writer_param,
411                            self.non_append_only_behavior,
412                            self.actor_context,
413                            rate_limit_rx,
414                            rebuild_sink_rx,
415                        )
416                        .instrument_await(
417                            await_tree::span!("consume_log (sink_id {sink_id})").long_running(),
418                        )
419                        .map_ok(|never| never); // unify return type to `Message`
420
421                        consume_log_stream.boxed()
422                    });
423                    select(consume_log_stream_future.into_stream(), write_log_stream)
424                })
425                .into_stream()
426                .flatten()
427                .boxed()
428        }
429    }
430
431    #[try_stream(ok = Message, error = StreamExecutorError)]
432    async fn execute_write_log<W: LogWriter>(
433        input: impl MessageStream,
434        mut log_writer: W,
435        actor_id: ActorId,
436        fragment_id: FragmentId,
437        sink_id: SinkId,
438        rate_limit_tx: UnboundedSender<RateLimit>,
439        rebuild_sink_tx: UnboundedSender<RebuildSinkMessage>,
440    ) {
441        pin_mut!(input);
442        let barrier = expect_first_barrier(&mut input).await?;
443        let epoch_pair = barrier.epoch;
444        let is_pause_on_startup = barrier.is_pause_on_startup();
445        // Propagate the first barrier
446        yield Message::Barrier(barrier);
447
448        log_writer.init(epoch_pair, is_pause_on_startup).await?;
449
450        let mut is_paused = false;
451
452        #[for_await]
453        for msg in input {
454            match msg? {
455                Message::Watermark(w) => yield Message::Watermark(w),
456                Message::Chunk(chunk) => {
457                    assert!(
458                        !is_paused,
459                        "Actor {actor_id} should not receive any data after pause"
460                    );
461                    log_writer.write_chunk(chunk.clone()).await?;
462                    yield Message::Chunk(chunk);
463                }
464                Message::Barrier(barrier) => {
465                    let update_vnode_bitmap = barrier.as_update_vnode_bitmap(actor_id);
466                    let schema_change = barrier.as_sink_schema_change(sink_id);
467                    let wait_log_store_flush = barrier.should_flush_sink_log_store(sink_id);
468                    if let Some(schema_change) = &schema_change {
469                        info!(?schema_change, %sink_id, "sink receive schema change");
470                    }
471                    if wait_log_store_flush {
472                        info!(%sink_id, "sink wait for log store flush");
473                    }
474                    let post_flush = log_writer
475                        .flush_current_epoch(
476                            barrier.epoch.curr,
477                            FlushCurrentEpochOptions {
478                                is_checkpoint: barrier.kind.is_checkpoint(),
479                                new_vnode_bitmap: update_vnode_bitmap.clone(),
480                                is_stop: barrier.is_stop(actor_id),
481                                schema_change,
482                                wait_log_store_flush,
483                            },
484                        )
485                        .await?;
486
487                    let mutation = barrier.mutation.clone();
488                    yield Message::Barrier(barrier);
489                    if F::REBUILD_SINK_ON_UPDATE_VNODE_BITMAP
490                        && let Some(new_vnode_bitmap) = update_vnode_bitmap.clone()
491                    {
492                        let (tx, rx) = oneshot::channel();
493                        rebuild_sink_tx
494                            .send(RebuildSinkMessage::RebuildSink(new_vnode_bitmap, tx))
495                            .map_err(|_| anyhow!("fail to send rebuild sink to reader"))?;
496                        rx.await
497                            .map_err(|_| anyhow!("fail to wait rebuild sink finish"))?;
498                    }
499                    post_flush.post_yield_barrier().await?;
500
501                    if let Some(mutation) = mutation.as_deref() {
502                        match mutation {
503                            Mutation::Pause => {
504                                log_writer.pause()?;
505                                is_paused = true;
506                            }
507                            Mutation::Resume => {
508                                log_writer.resume()?;
509                                is_paused = false;
510                            }
511                            Mutation::Throttle(fragment_to_apply) => {
512                                if let Some(entry) = fragment_to_apply.get(&fragment_id)
513                                    && entry.throttle_type() == ThrottleType::Sink
514                                {
515                                    tracing::info!(
516                                        rate_limit = entry.rate_limit,
517                                        "received sink rate limit on actor {actor_id}"
518                                    );
519                                    if let Err(e) = rate_limit_tx.send(entry.rate_limit.into()) {
520                                        error!(
521                                            error = %e.as_report(),
522                                            "fail to send sink rate limit update"
523                                        );
524                                        return Err(StreamExecutorError::from(
525                                            e.to_report_string(),
526                                        ));
527                                    }
528                                }
529                            }
530                            Mutation::ConnectorPropsChange(config) => {
531                                if let Some(map) = config.get(&sink_id.as_raw_id())
532                                    && let Err(e) = rebuild_sink_tx
533                                        .send(RebuildSinkMessage::UpdateConfig(map.clone()))
534                                {
535                                    error!(
536                                        error = %e.as_report(),
537                                        "fail to send sink alter props"
538                                    );
539                                    return Err(StreamExecutorError::from(e.to_report_string()));
540                                }
541                            }
542                            _ => (),
543                        }
544                    }
545                }
546            }
547        }
548    }
549
550    #[expect(clippy::too_many_arguments)]
551    #[try_stream(ok = Message, error = StreamExecutorError)]
552    async fn process_msg(
553        input: impl MessageStream,
554        sink_type: SinkType,
555        stream_key: StreamKey,
556        chunk_size: usize,
557        input_data_types: Vec<DataType>,
558        input_compact_ib: InconsistencyBehavior,
559        downstream_pk: Option<Vec<usize>>,
560        non_append_only_behavior: Option<NonAppendOnlyBehavior>,
561        sink_chunk_buffer_size_metrics: LabelGuardedIntGauge,
562        preserve_row_level_changes: bool,
563        skip_compact: bool,
564    ) {
565        // To reorder records, we need to buffer chunks of the entire epoch.
566        if let Some(b) = non_append_only_behavior
567            && b.should_reorder_records()
568        {
569            assert_matches!(sink_type, SinkType::Upsert | SinkType::Retract);
570
571            let mut chunk_buffer = EstimatedVec::new();
572            let mut watermark: Option<super::Watermark> = None;
573            #[for_await]
574            for msg in input {
575                match msg? {
576                    Message::Watermark(w) => watermark = Some(w),
577                    Message::Chunk(c) => {
578                        chunk_buffer.push(c);
579                        sink_chunk_buffer_size_metrics.set(chunk_buffer.estimated_size() as i64);
580                    }
581                    Message::Barrier(barrier) => {
582                        let chunks = mem::take(&mut chunk_buffer).into_inner();
583
584                        // 1. Compact the chunk based on the **stream key**, so that we have at most 2 rows for each
585                        //    stream key. Then, move all delete records to the front.
586                        let mut delete_chunks = vec![];
587                        let mut insert_chunks = vec![];
588
589                        for c in dispatch_output_kind!(sink_type, KIND, {
590                            StreamChunkCompactor::new(stream_key.clone(), chunks)
591                                .into_compacted_chunks_inline::<KIND>(input_compact_ib)
592                        }) {
593                            let chunk = force_delete_only(c.clone());
594                            if chunk.cardinality() > 0 {
595                                delete_chunks.push(chunk);
596                            }
597                            let chunk = force_append_only(c);
598                            if chunk.cardinality() > 0 {
599                                insert_chunks.push(chunk);
600                            }
601                        }
602                        let chunks = delete_chunks
603                            .into_iter()
604                            .chain(insert_chunks.into_iter())
605                            .collect();
606
607                        // 2. If user specifies a primary key, compact the chunk based on the **downstream pk**
608                        //    to eliminate any unnecessary updates to external systems. This also rewrites the
609                        //    `DELETE` and `INSERT` operations on the same key into `UPDATE` operations, which
610                        //    usually have more efficient implementation.
611                        //    Skip this when the target table has special conflict semantics. In that case, the
612                        //    target table must observe every row-level change instead of a pre-compacted final state.
613                        if let Some(downstream_pk) = &downstream_pk
614                            && !preserve_row_level_changes
615                        {
616                            let chunks = dispatch_output_kind!(sink_type, KIND, {
617                                StreamChunkCompactor::new(downstream_pk.clone(), chunks)
618                                    .into_compacted_chunks_reconstructed::<KIND>(
619                                        chunk_size,
620                                        input_data_types.clone(),
621                                        // When compacting based on user provided primary key, we should never panic
622                                        // on inconsistency in case the user provided primary key is not unique.
623                                        InconsistencyBehavior::Warn,
624                                    )
625                            });
626                            for c in chunks {
627                                yield Message::Chunk(c);
628                            }
629                        } else {
630                            let mut chunk_builder =
631                                StreamChunkBuilder::new(chunk_size, input_data_types.clone());
632                            for chunk in chunks {
633                                for (op, row) in chunk.rows() {
634                                    if let Some(c) = chunk_builder.append_row(op, row) {
635                                        yield Message::Chunk(c);
636                                    }
637                                }
638                            }
639
640                            if let Some(c) = chunk_builder.take() {
641                                yield Message::Chunk(c);
642                            }
643                        };
644
645                        // 3. Forward watermark and barrier.
646                        if let Some(w) = mem::take(&mut watermark) {
647                            yield Message::Watermark(w)
648                        }
649                        yield Message::Barrier(barrier);
650                    }
651                }
652            }
653        } else {
654            // In this branch, we don't need to reorder records, either because the stream key matches
655            // the downstream pk, or the sink is append-only.
656            #[for_await]
657            for msg in input {
658                match msg? {
659                    Message::Watermark(w) => yield Message::Watermark(w),
660                    Message::Chunk(mut chunk) => {
661                        // Compact the chunk to eliminate any unnecessary updates to external systems.
662                        // This should be performed against the downstream pk, not the stream key, to
663                        // ensure correct retract/upsert semantics from the downstream's perspective.
664                        if !sink_type.is_append_only()
665                            && let Some(downstream_pk) = &downstream_pk
666                        {
667                            if preserve_row_level_changes {
668                                // Preserve every row-level change so the target table can apply its
669                                // own conflict semantics.
670                            } else if skip_compact {
671                                // We can only skip compaction if the keys are exactly the same, not just
672                                // matching by being a subset.
673                                assert_eq!(&stream_key, downstream_pk);
674                            } else {
675                                chunk = dispatch_output_kind!(sink_type, KIND, {
676                                    compact_chunk_inline::<KIND>(
677                                        chunk,
678                                        downstream_pk,
679                                        // When compacting based on user provided primary key, we should never panic
680                                        // on inconsistency in case the user provided primary key is not unique.
681                                        InconsistencyBehavior::Warn,
682                                    )
683                                });
684                            }
685                        }
686                        yield Message::Chunk(chunk);
687                    }
688                    Message::Barrier(barrier) => {
689                        yield Message::Barrier(barrier);
690                    }
691                }
692            }
693        }
694    }
695
696    #[expect(clippy::too_many_arguments)]
697    async fn execute_consume_log<S: Sink, R: LogReader>(
698        mut sink: S,
699        log_reader: R,
700        columns: Vec<ColumnCatalog>,
701        mut sink_param: SinkParam,
702        mut sink_writer_param: SinkWriterParam,
703        non_append_only_behavior: Option<NonAppendOnlyBehavior>,
704        actor_context: ActorContextRef,
705        rate_limit_rx: UnboundedReceiver<RateLimit>,
706        mut rebuild_sink_rx: UnboundedReceiver<RebuildSinkMessage>,
707    ) -> StreamExecutorResult<!> {
708        let visible_columns = columns
709            .iter()
710            .enumerate()
711            .filter_map(|(idx, column)| (!column.is_hidden).then_some(idx))
712            .collect_vec();
713
714        let labels = [
715            &actor_context.id.to_string(),
716            sink_writer_param.connector.as_str(),
717            &sink_writer_param.sink_id.to_string(),
718            sink_writer_param.sink_name.as_str(),
719        ];
720        let log_store_reader_wait_new_future_duration_ns = GLOBAL_SINK_METRICS
721            .log_store_reader_wait_new_future_duration_ns
722            .with_guarded_label_values(&labels);
723        let log_store_read_rows = GLOBAL_SINK_METRICS
724            .log_store_read_rows
725            .with_guarded_label_values(&labels);
726        let log_store_read_bytes = GLOBAL_SINK_METRICS
727            .log_store_read_bytes
728            .with_guarded_label_values(&labels);
729        let log_store_latest_read_epoch = GLOBAL_SINK_METRICS
730            .log_store_latest_read_epoch
731            .with_guarded_label_values(&labels);
732        let metrics = LogReaderMetrics {
733            log_store_latest_read_epoch,
734            log_store_read_rows,
735            log_store_read_bytes,
736            log_store_reader_wait_new_future_duration_ns,
737        };
738
739        let downstream_pk = sink_param.downstream_pk.clone();
740
741        let mut log_reader = log_reader
742            .transform_chunk(move |chunk| {
743                let chunk = if let Some(b) = non_append_only_behavior
744                    && b.should_compact_in_log_reader()
745                {
746                    // This guarantees that user has specified a `downstream_pk`.
747                    let downstream_pk = downstream_pk.as_ref().unwrap();
748                    dispatch_output_kind!(sink_param.sink_type, KIND, {
749                        compact_chunk_inline::<KIND>(
750                            chunk,
751                            downstream_pk,
752                            // When compacting based on user provided primary key, we should never panic
753                            // on inconsistency in case the user provided primary key is not unique.
754                            InconsistencyBehavior::Warn,
755                        )
756                    })
757                } else {
758                    chunk
759                };
760                if visible_columns.len() != columns.len() {
761                    // Do projection here because we may have columns that aren't visible to
762                    // the downstream.
763                    chunk.project(&visible_columns)
764                } else {
765                    chunk
766                }
767            })
768            .monitored(metrics)
769            .rate_limited(rate_limit_rx);
770
771        log_reader.init().await?;
772        loop {
773            let future = async {
774                loop {
775                    let Err(e) = sink
776                        .new_log_sinker(sink_writer_param.clone())
777                        .and_then(|log_sinker| log_sinker.consume_log_and_sink(&mut log_reader))
778                        .await;
779                    GLOBAL_ERROR_METRICS.user_sink_error.report([
780                        "sink_executor_error".to_owned(),
781                        sink_param.sink_id.to_string(),
782                        sink_param.sink_name.clone(),
783                        actor_context.fragment_id.to_string(),
784                    ]);
785
786                    if let Some(meta_client) = sink_writer_param.meta_client.as_ref() {
787                        meta_client
788                            .add_sink_fail_evet_log(
789                                sink_writer_param.sink_id,
790                                sink_writer_param.sink_name.clone(),
791                                sink_writer_param.connector.clone(),
792                                e.to_report_string(),
793                            )
794                            .await;
795                    }
796
797                    if F::ALLOW_REWIND {
798                        match log_reader.rewind().await {
799                            Ok(()) => {
800                                error!(
801                                    error = %e.as_report(),
802                                    executor_id = %sink_writer_param.executor_id,
803                                    sink_id = %sink_param.sink_id,
804                                    "reset log reader stream successfully after sink error"
805                                );
806                                Ok(())
807                            }
808                            Err(rewind_err) => {
809                                error!(
810                                    error = %rewind_err.as_report(),
811                                    "fail to rewind log reader"
812                                );
813                                Err(e)
814                            }
815                        }
816                    } else {
817                        Err(e)
818                    }
819                    .map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
820                }
821            };
822            select! {
823                result = future => {
824                    let Err(e): StreamExecutorResult<!> = result;
825                    return Err(e);
826                }
827                result = rebuild_sink_rx.recv() => {
828                    match result.ok_or_else(|| anyhow!("failed to receive rebuild sink notify"))? {
829                        RebuildSinkMessage::RebuildSink(new_vnode, notify) => {
830                            sink_writer_param.vnode_bitmap = Some((*new_vnode).clone());
831                            if notify.send(()).is_err() {
832                                warn!("failed to notify rebuild sink");
833                            }
834                            log_reader.init().await?;
835                        },
836                        RebuildSinkMessage::UpdateConfig(config) => {
837                            let config_has_changes =
838                                sink_config_has_changes(&sink_param.properties, &config);
839                            if F::ALLOW_REWIND {
840                                match log_reader.rewind().await {
841                                    Ok(()) => {
842                                        if config_has_changes {
843                                            sink_param.properties.extend(config.into_iter());
844                                            sink = TryFrom::try_from(sink_param.clone()).map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
845                                            info!(
846                                                executor_id = %sink_writer_param.executor_id,
847                                                sink_id = %sink_param.sink_id,
848                                                "alter sink config successfully with rewind"
849                                            );
850                                        } else {
851                                            info!(
852                                                executor_id = %sink_writer_param.executor_id,
853                                                sink_id = %sink_param.sink_id,
854                                                "skip alter sink config because properties are unchanged"
855                                            );
856                                        }
857                                        Ok(())
858                                    }
859                                    Err(rewind_err) => {
860                                        error!(
861                                            error = %rewind_err.as_report(),
862                                            "fail to rewind log reader for alter sink config "
863                                        );
864                                        Err(anyhow!("fail to rewind log reader after alter sink config notification").into())
865                                    }
866                                }
867                            } else {
868                                if config_has_changes {
869                                    sink_param.properties.extend(config.into_iter());
870                                    sink = TryFrom::try_from(sink_param.clone()).map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
871                                } else {
872                                    info!(
873                                        executor_id = %sink_writer_param.executor_id,
874                                        sink_id = %sink_param.sink_id,
875                                        "skip rebuilding sink because properties are unchanged"
876                                    );
877                                }
878                                Err(anyhow!("This is not an actual error condition. The system is intentionally triggering recovery procedures to ensure ALTER SINK CONFIG are fully applied.").into())
879                            }
880                            .map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
881                        },
882                    }
883                }
884            }
885        }
886    }
887}
888
889enum RebuildSinkMessage {
890    RebuildSink(Arc<Bitmap>, oneshot::Sender<()>),
891    UpdateConfig(HashMap<String, String>),
892}
893
894fn sink_config_has_changes(
895    current: &BTreeMap<String, String>,
896    incoming: &HashMap<String, String>,
897) -> bool {
898    incoming
899        .iter()
900        .any(|(key, value)| current.get(key) != Some(value))
901}
902
903impl<F: LogStoreFactory> Execute for SinkExecutor<F> {
904    fn execute(self: Box<Self>) -> BoxedMessageStream {
905        self.execute_inner()
906    }
907}
908
909#[cfg(test)]
910mod test {
911    use std::future::pending;
912    use std::sync::Arc;
913    use std::sync::atomic::{AtomicUsize, Ordering};
914    use std::time::Duration;
915
916    use risingwave_common::catalog::{ColumnDesc, ColumnId};
917    use risingwave_common::util::epoch::test_epoch;
918    use risingwave_connector::sink::build_sink;
919    use risingwave_connector::sink::log_store::{LogStoreReadItem, LogStoreResult, TruncateOffset};
920    use risingwave_connector::sink::trivial::BlackHoleSink;
921    use tokio::sync::Notify;
922
923    use super::*;
924    use crate::common::log_store_impl::in_mem::{
925        BoundedInMemLogStoreFactory, BoundedInMemLogStoreWriter,
926    };
927    use crate::executor::test_utils::*;
928
929    #[derive(Default)]
930    struct RewindRequiredLogReaderState {
931        start_count: AtomicUsize,
932        rewind_count: AtomicUsize,
933        started: Notify,
934    }
935
936    impl RewindRequiredLogReaderState {
937        async fn wait_for_start_count(&self, expected: usize) {
938            tokio::time::timeout(Duration::from_secs(5), async {
939                loop {
940                    let started = self.started.notified();
941                    if self.start_count.load(Ordering::SeqCst) >= expected {
942                        break;
943                    }
944                    started.await;
945                }
946            })
947            .await
948            .unwrap_or_else(|_| panic!("log reader was not started {expected} times"));
949        }
950    }
951
952    struct RewindRequiredLogReader {
953        is_reset: bool,
954        state: Arc<RewindRequiredLogReaderState>,
955    }
956
957    impl LogReader for RewindRequiredLogReader {
958        async fn init(&mut self) -> LogStoreResult<()> {
959            self.is_reset = true;
960            Ok(())
961        }
962
963        async fn start_from(&mut self, _start_offset: Option<u64>) -> LogStoreResult<()> {
964            assert!(
965                self.is_reset,
966                "log reader must be rewound before restarting"
967            );
968            self.is_reset = false;
969            self.state.start_count.fetch_add(1, Ordering::SeqCst);
970            self.state.started.notify_waiters();
971            Ok(())
972        }
973
974        async fn next_item(&mut self) -> LogStoreResult<(u64, LogStoreReadItem)> {
975            pending().await
976        }
977
978        fn truncate(&mut self, _offset: TruncateOffset) -> LogStoreResult<()> {
979            Ok(())
980        }
981
982        async fn rewind(&mut self) -> LogStoreResult<()> {
983            self.is_reset = true;
984            self.state.rewind_count.fetch_add(1, Ordering::SeqCst);
985            Ok(())
986        }
987    }
988
989    struct TestLogStoreFactory<const CAN_REWIND: bool>;
990
991    impl<const CAN_REWIND: bool> LogStoreFactory for TestLogStoreFactory<CAN_REWIND> {
992        type Reader = RewindRequiredLogReader;
993        type Writer = BoundedInMemLogStoreWriter;
994
995        const ALLOW_REWIND: bool = CAN_REWIND;
996        const REBUILD_SINK_ON_UPDATE_VNODE_BITMAP: bool = false;
997
998        async fn build(self) -> (Self::Reader, Self::Writer) {
999            unreachable!()
1000        }
1001    }
1002
1003    fn sink_param_for_config_update_test() -> SinkParam {
1004        SinkParam {
1005            sink_id: 0.into(),
1006            sink_name: "test".into(),
1007            properties: BTreeMap::from([
1008                ("connector".to_owned(), "blackhole".to_owned()),
1009                ("commit_checkpoint_interval".to_owned(), "1".to_owned()),
1010            ]),
1011            columns: vec![],
1012            downstream_pk: None,
1013            sink_type: SinkType::AppendOnly,
1014            ignore_delete: false,
1015            format_desc: None,
1016            db_name: "test".into(),
1017            sink_from_name: "test".into(),
1018        }
1019    }
1020
1021    #[test]
1022    fn test_sink_config_has_changes() {
1023        let current = BTreeMap::from([
1024            ("connector".to_owned(), "blackhole".to_owned()),
1025            ("commit_checkpoint_interval".to_owned(), "1".to_owned()),
1026        ]);
1027
1028        assert!(!sink_config_has_changes(
1029            &current,
1030            &HashMap::from([("commit_checkpoint_interval".to_owned(), "1".to_owned())])
1031        ));
1032        assert!(sink_config_has_changes(
1033            &current,
1034            &HashMap::from([("commit_checkpoint_interval".to_owned(), "2".to_owned())])
1035        ));
1036        assert!(sink_config_has_changes(
1037            &current,
1038            &HashMap::from([("force_append_only".to_owned(), "true".to_owned())])
1039        ));
1040    }
1041
1042    #[tokio::test]
1043    async fn test_no_op_sink_config_update_rewinds_log_reader() {
1044        let sink_param = sink_param_for_config_update_test();
1045        let sink = BlackHoleSink::try_from(sink_param.clone()).unwrap();
1046        let state = Arc::new(RewindRequiredLogReaderState::default());
1047        let log_reader = RewindRequiredLogReader {
1048            is_reset: false,
1049            state: state.clone(),
1050        };
1051        let (rate_limit_tx, rate_limit_rx) = unbounded_channel();
1052        let (rebuild_sink_tx, rebuild_sink_rx) = unbounded_channel();
1053
1054        let consume_log = SinkExecutor::<TestLogStoreFactory<true>>::execute_consume_log(
1055            sink,
1056            log_reader,
1057            vec![],
1058            sink_param,
1059            SinkWriterParam::for_test(),
1060            None,
1061            ActorContext::for_test(0),
1062            rate_limit_rx,
1063            rebuild_sink_rx,
1064        );
1065        tokio::pin!(consume_log);
1066
1067        tokio::select! {
1068            result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1069            () = state.wait_for_start_count(1) => {},
1070        }
1071
1072        rebuild_sink_tx
1073            .send(RebuildSinkMessage::UpdateConfig(HashMap::from([(
1074                "commit_checkpoint_interval".to_owned(),
1075                "1".to_owned(),
1076            )])))
1077            .unwrap();
1078
1079        tokio::select! {
1080            result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1081            () = state.wait_for_start_count(2) => {},
1082        }
1083
1084        assert_eq!(state.rewind_count.load(Ordering::SeqCst), 1);
1085        drop(rate_limit_tx);
1086    }
1087
1088    #[tokio::test]
1089    async fn test_no_op_sink_config_update_triggers_recovery_without_rewind() {
1090        let sink_param = sink_param_for_config_update_test();
1091        let sink = BlackHoleSink::try_from(sink_param.clone()).unwrap();
1092        let state = Arc::new(RewindRequiredLogReaderState::default());
1093        let log_reader = RewindRequiredLogReader {
1094            is_reset: false,
1095            state: state.clone(),
1096        };
1097        let (rate_limit_tx, rate_limit_rx) = unbounded_channel();
1098        let (rebuild_sink_tx, rebuild_sink_rx) = unbounded_channel();
1099
1100        let consume_log = SinkExecutor::<TestLogStoreFactory<false>>::execute_consume_log(
1101            sink,
1102            log_reader,
1103            vec![],
1104            sink_param,
1105            SinkWriterParam::for_test(),
1106            None,
1107            ActorContext::for_test(0),
1108            rate_limit_rx,
1109            rebuild_sink_rx,
1110        );
1111        tokio::pin!(consume_log);
1112
1113        tokio::select! {
1114            result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1115            () = state.wait_for_start_count(1) => {},
1116        }
1117
1118        rebuild_sink_tx
1119            .send(RebuildSinkMessage::UpdateConfig(HashMap::from([(
1120                "commit_checkpoint_interval".to_owned(),
1121                "1".to_owned(),
1122            )])))
1123            .unwrap();
1124
1125        tokio::time::timeout(Duration::from_secs(5), consume_log)
1126            .await
1127            .expect("log consumer should trigger recovery")
1128            .expect_err("log consumer should return an error");
1129        assert_eq!(state.start_count.load(Ordering::SeqCst), 1);
1130        assert_eq!(state.rewind_count.load(Ordering::SeqCst), 0);
1131        drop(rate_limit_tx);
1132    }
1133
1134    #[tokio::test]
1135    async fn test_force_append_only_sink() {
1136        use risingwave_common::array::StreamChunkTestExt;
1137        use risingwave_common::array::stream_chunk::StreamChunk;
1138        use risingwave_common::types::DataType;
1139
1140        use crate::executor::Barrier;
1141
1142        let properties = maplit::btreemap! {
1143            "connector".into() => "blackhole".into(),
1144            "type".into() => "append-only".into(),
1145            "force_append_only".into() => "true".into()
1146        };
1147
1148        // We have two visible columns and one hidden column. The hidden column will be pruned out
1149        // within the sink executor.
1150        let columns = vec![
1151            ColumnCatalog {
1152                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1153                is_hidden: false,
1154            },
1155            ColumnCatalog {
1156                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1157                is_hidden: false,
1158            },
1159            ColumnCatalog {
1160                column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1161                is_hidden: true,
1162            },
1163        ];
1164        let schema: Schema = columns
1165            .iter()
1166            .map(|column| Field::from(column.column_desc.clone()))
1167            .collect();
1168        let stream_key = vec![0];
1169
1170        let source = MockSource::with_messages(vec![
1171            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1172            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1173                " I I I
1174                    + 3 2 1",
1175            ))),
1176            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1177            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1178                "  I I I
1179                    U- 3 2 1
1180                    U+ 3 4 1
1181                     + 5 6 7",
1182            ))),
1183            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1184                " I I I
1185                    - 5 6 7",
1186            ))),
1187        ])
1188        .into_executor(schema.clone(), stream_key.clone());
1189
1190        let sink_param = SinkParam {
1191            sink_id: 0.into(),
1192            sink_name: "test".into(),
1193            properties,
1194
1195            columns: columns
1196                .iter()
1197                .filter(|col| !col.is_hidden)
1198                .map(|col| col.column_desc.clone())
1199                .collect(),
1200            downstream_pk: Some(stream_key.clone()),
1201            sink_type: SinkType::AppendOnly,
1202            ignore_delete: true,
1203            format_desc: None,
1204            db_name: "test".into(),
1205            sink_from_name: "test".into(),
1206        };
1207
1208        let info = ExecutorInfo::for_test(schema, stream_key, "SinkExecutor".to_owned(), 0);
1209
1210        let sink = build_sink(sink_param.clone()).unwrap();
1211
1212        let sink_executor = SinkExecutor::new(
1213            ActorContext::for_test(0),
1214            info,
1215            source,
1216            SinkWriterParam::for_test(),
1217            sink,
1218            sink_param,
1219            columns.clone(),
1220            BoundedInMemLogStoreFactory::for_test(1),
1221            1024,
1222            vec![DataType::Int32, DataType::Int32, DataType::Int32],
1223            None,
1224        )
1225        .unwrap();
1226
1227        let mut executor = sink_executor.boxed().execute();
1228
1229        // Barrier message.
1230        executor.next().await.unwrap().unwrap();
1231
1232        let chunk_msg = executor.next().await.unwrap().unwrap();
1233        assert_eq!(
1234            chunk_msg.into_chunk().unwrap().compact_vis(),
1235            StreamChunk::from_pretty(
1236                " I I I
1237                + 3 2 1",
1238            )
1239        );
1240
1241        // Barrier message.
1242        executor.next().await.unwrap().unwrap();
1243
1244        let chunk_msg = executor.next().await.unwrap().unwrap();
1245        assert_eq!(
1246            chunk_msg.into_chunk().unwrap().compact_vis(),
1247            StreamChunk::from_pretty(
1248                " I I I
1249                + 3 4 1
1250                + 5 6 7",
1251            )
1252        );
1253
1254        // Should not receive the third stream chunk message because the force-append-only sink
1255        // executor will drop all DELETE messages.
1256
1257        // The last barrier message.
1258        executor.next().await.unwrap().unwrap();
1259    }
1260
1261    #[tokio::test]
1262    async fn stream_key_sink_pk_mismatch_upsert() {
1263        stream_key_sink_pk_mismatch(SinkType::Upsert).await;
1264    }
1265
1266    #[tokio::test]
1267    async fn stream_key_sink_pk_mismatch_retract() {
1268        stream_key_sink_pk_mismatch(SinkType::Retract).await;
1269    }
1270
1271    async fn stream_key_sink_pk_mismatch(sink_type: SinkType) {
1272        use risingwave_common::array::StreamChunkTestExt;
1273        use risingwave_common::array::stream_chunk::StreamChunk;
1274        use risingwave_common::types::DataType;
1275
1276        use crate::executor::Barrier;
1277
1278        let properties = maplit::btreemap! {
1279            "connector".into() => "blackhole".into(),
1280        };
1281
1282        // We have two visible columns and one hidden column. The hidden column will be pruned out
1283        // within the sink executor.
1284        let columns = vec![
1285            ColumnCatalog {
1286                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1287                is_hidden: false,
1288            },
1289            ColumnCatalog {
1290                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1291                is_hidden: false,
1292            },
1293            ColumnCatalog {
1294                column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1295                is_hidden: true,
1296            },
1297        ];
1298        let schema: Schema = columns
1299            .iter()
1300            .map(|column| Field::from(column.column_desc.clone()))
1301            .collect();
1302
1303        let source = MockSource::with_messages(vec![
1304            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1305            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1306                " I I I
1307                    + 1 1 10",
1308            ))),
1309            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1310            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1311                " I I I
1312                    + 1 3 30",
1313            ))),
1314            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1315                " I I I
1316                    + 1 2 20
1317                    - 1 2 20",
1318            ))),
1319            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1320                " I I I
1321                    - 1 1 10
1322                    + 1 1 40",
1323            ))),
1324            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1325        ])
1326        .into_executor(schema.clone(), vec![0, 1]);
1327
1328        let sink_param = SinkParam {
1329            sink_id: 0.into(),
1330            sink_name: "test".into(),
1331            properties,
1332
1333            columns: columns
1334                .iter()
1335                .filter(|col| !col.is_hidden)
1336                .map(|col| col.column_desc.clone())
1337                .collect(),
1338            downstream_pk: Some(vec![0]),
1339            sink_type,
1340            ignore_delete: false,
1341            format_desc: None,
1342            db_name: "test".into(),
1343            sink_from_name: "test".into(),
1344        };
1345
1346        let info = ExecutorInfo::for_test(schema, vec![0, 1], "SinkExecutor".to_owned(), 0);
1347
1348        let sink = build_sink(sink_param.clone()).unwrap();
1349
1350        let sink_executor = SinkExecutor::new(
1351            ActorContext::for_test(0),
1352            info,
1353            source,
1354            SinkWriterParam::for_test(),
1355            sink,
1356            sink_param,
1357            columns.clone(),
1358            BoundedInMemLogStoreFactory::for_test(1),
1359            1024,
1360            vec![DataType::Int64, DataType::Int64, DataType::Int64],
1361            None,
1362        )
1363        .unwrap();
1364
1365        let mut executor = sink_executor.boxed().execute();
1366
1367        // Barrier message.
1368        executor.next().await.unwrap().unwrap();
1369
1370        let chunk_msg = executor.next().await.unwrap().unwrap();
1371        assert_eq!(
1372            chunk_msg.into_chunk().unwrap().compact_vis(),
1373            StreamChunk::from_pretty(
1374                " I I I
1375                + 1 1 10",
1376            )
1377        );
1378
1379        // Barrier message.
1380        executor.next().await.unwrap().unwrap();
1381
1382        let chunk_msg = executor.next().await.unwrap().unwrap();
1383        let expected = match sink_type {
1384            SinkType::Retract => StreamChunk::from_pretty(
1385                " I I I
1386                U- 1 1 10
1387                U+ 1 1 40",
1388            ),
1389            SinkType::Upsert => StreamChunk::from_pretty(
1390                " I I I
1391                + 1 1 40", // For upsert format, there won't be `U- 1 1 10`.
1392            ),
1393            _ => unreachable!(),
1394        };
1395        assert_eq!(chunk_msg.into_chunk().unwrap().compact_vis(), expected);
1396
1397        // The last barrier message.
1398        executor.next().await.unwrap().unwrap();
1399    }
1400
1401    #[tokio::test]
1402    async fn test_sink_into_table_preserves_special_conflict_rows_for_mismatched_pk() {
1403        use risingwave_common::array::StreamChunkTestExt;
1404        use risingwave_common::array::stream_chunk::StreamChunk;
1405        use risingwave_common::types::DataType;
1406
1407        use crate::executor::Barrier;
1408
1409        let properties = maplit::btreemap! {
1410            "connector".into() => "table".into(),
1411            SINK_USER_PRESERVE_ROW_LEVEL_CHANGES.into() => "true".into(),
1412        };
1413
1414        let columns = vec![
1415            ColumnCatalog {
1416                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1417                is_hidden: false,
1418            },
1419            ColumnCatalog {
1420                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1421                is_hidden: false,
1422            },
1423            ColumnCatalog {
1424                column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1425                is_hidden: false,
1426            },
1427        ];
1428        let schema: Schema = columns
1429            .iter()
1430            .map(|column| Field::from(column.column_desc.clone()))
1431            .collect();
1432
1433        let source = MockSource::with_messages(vec![
1434            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1435            Message::Chunk(StreamChunk::from_pretty(
1436                " I  I  I
1437                  + 1 10  1",
1438            )),
1439            Message::Chunk(StreamChunk::from_pretty(
1440                " I  I  I
1441                  + 1 20  2",
1442            )),
1443            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1444        ])
1445        .into_executor(schema.clone(), vec![0, 2]);
1446
1447        let sink_param = SinkParam {
1448            sink_id: 0.into(),
1449            sink_name: "test".into(),
1450            properties,
1451            columns: columns.iter().map(|col| col.column_desc.clone()).collect(),
1452            downstream_pk: Some(vec![0]),
1453            sink_type: SinkType::Upsert,
1454            ignore_delete: false,
1455            format_desc: None,
1456            db_name: "test".into(),
1457            sink_from_name: "test".into(),
1458        };
1459
1460        let info = ExecutorInfo::for_test(schema, vec![0, 2], "SinkExecutor".to_owned(), 0);
1461        let sink = build_sink(sink_param.clone()).unwrap();
1462
1463        let sink_executor = SinkExecutor::new(
1464            ActorContext::for_test(0),
1465            info,
1466            source,
1467            SinkWriterParam::for_test(),
1468            sink,
1469            sink_param,
1470            columns,
1471            BoundedInMemLogStoreFactory::for_test(1),
1472            1024,
1473            vec![DataType::Int64, DataType::Int64, DataType::Int64],
1474            None,
1475        )
1476        .unwrap();
1477
1478        let mut executor = sink_executor.boxed().execute();
1479
1480        executor.next().await.unwrap().unwrap();
1481
1482        let chunk_msg = executor.next().await.unwrap().unwrap();
1483        assert_eq!(
1484            chunk_msg.into_chunk().unwrap().compact_vis(),
1485            StreamChunk::from_pretty(
1486                " I  I  I
1487                  + 1 10  1
1488                  + 1 20  2",
1489            )
1490        );
1491
1492        executor.next().await.unwrap().unwrap();
1493    }
1494
1495    #[tokio::test]
1496    async fn test_sink_into_table_keeps_default_compaction_without_special_conflict_semantics() {
1497        use risingwave_common::array::StreamChunkTestExt;
1498        use risingwave_common::array::stream_chunk::StreamChunk;
1499        use risingwave_common::types::DataType;
1500
1501        use crate::executor::Barrier;
1502
1503        let properties = maplit::btreemap! {
1504            "connector".into() => "table".into(),
1505        };
1506
1507        let columns = vec![
1508            ColumnCatalog {
1509                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1510                is_hidden: false,
1511            },
1512            ColumnCatalog {
1513                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1514                is_hidden: false,
1515            },
1516        ];
1517        let schema: Schema = columns
1518            .iter()
1519            .map(|column| Field::from(column.column_desc.clone()))
1520            .collect();
1521
1522        let source = MockSource::with_messages(vec![
1523            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1524            Message::Chunk(StreamChunk::from_pretty(
1525                " I  I
1526                  + 1 10
1527                  + 1 20",
1528            )),
1529            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1530        ])
1531        .into_executor(schema.clone(), vec![0]);
1532
1533        let sink_param = SinkParam {
1534            sink_id: 0.into(),
1535            sink_name: "test".into(),
1536            properties,
1537            columns: columns.iter().map(|col| col.column_desc.clone()).collect(),
1538            downstream_pk: Some(vec![0]),
1539            sink_type: SinkType::Upsert,
1540            ignore_delete: false,
1541            format_desc: None,
1542            db_name: "test".into(),
1543            sink_from_name: "test".into(),
1544        };
1545
1546        let info = ExecutorInfo::for_test(schema, vec![0], "SinkExecutor".to_owned(), 0);
1547        let sink = build_sink(sink_param.clone()).unwrap();
1548
1549        let sink_executor = SinkExecutor::new(
1550            ActorContext::for_test(0),
1551            info,
1552            source,
1553            SinkWriterParam::for_test(),
1554            sink,
1555            sink_param,
1556            columns,
1557            BoundedInMemLogStoreFactory::for_test(1),
1558            1024,
1559            vec![DataType::Int64, DataType::Int64],
1560            None,
1561        )
1562        .unwrap();
1563
1564        let mut executor = sink_executor.boxed().execute();
1565
1566        executor.next().await.unwrap().unwrap();
1567
1568        let chunk_msg = executor.next().await.unwrap().unwrap();
1569        assert_eq!(
1570            chunk_msg.into_chunk().unwrap().compact_vis(),
1571            StreamChunk::from_pretty(
1572                " I  I
1573                  + 1 20",
1574            )
1575        );
1576
1577        executor.next().await.unwrap().unwrap();
1578    }
1579
1580    #[tokio::test]
1581    async fn test_empty_barrier_sink() {
1582        use risingwave_common::types::DataType;
1583
1584        use crate::executor::Barrier;
1585
1586        let properties = maplit::btreemap! {
1587            "connector".into() => "blackhole".into(),
1588            "type".into() => "append-only".into(),
1589            "force_append_only".into() => "true".into()
1590        };
1591        let columns = vec![
1592            ColumnCatalog {
1593                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1594                is_hidden: false,
1595            },
1596            ColumnCatalog {
1597                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1598                is_hidden: false,
1599            },
1600        ];
1601        let schema: Schema = columns
1602            .iter()
1603            .map(|column| Field::from(column.column_desc.clone()))
1604            .collect();
1605        let stream_key = vec![0];
1606
1607        let source = MockSource::with_messages(vec![
1608            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1609            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1610            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1611        ])
1612        .into_executor(schema.clone(), stream_key.clone());
1613
1614        let sink_param = SinkParam {
1615            sink_id: 0.into(),
1616            sink_name: "test".into(),
1617            properties,
1618
1619            columns: columns
1620                .iter()
1621                .filter(|col| !col.is_hidden)
1622                .map(|col| col.column_desc.clone())
1623                .collect(),
1624            downstream_pk: Some(stream_key.clone()),
1625            sink_type: SinkType::AppendOnly,
1626            ignore_delete: true,
1627            format_desc: None,
1628            db_name: "test".into(),
1629            sink_from_name: "test".into(),
1630        };
1631
1632        let info = ExecutorInfo::for_test(schema, stream_key, "SinkExecutor".to_owned(), 0);
1633
1634        let sink = build_sink(sink_param.clone()).unwrap();
1635
1636        let sink_executor = SinkExecutor::new(
1637            ActorContext::for_test(0),
1638            info,
1639            source,
1640            SinkWriterParam::for_test(),
1641            sink,
1642            sink_param,
1643            columns,
1644            BoundedInMemLogStoreFactory::for_test(1),
1645            1024,
1646            vec![DataType::Int64, DataType::Int64],
1647            None,
1648        )
1649        .unwrap();
1650
1651        let mut executor = sink_executor.boxed().execute();
1652
1653        // Barrier message.
1654        assert_eq!(
1655            executor.next().await.unwrap().unwrap(),
1656            Message::Barrier(Barrier::new_test_barrier(test_epoch(1)))
1657        );
1658
1659        // Barrier message.
1660        assert_eq!(
1661            executor.next().await.unwrap().unwrap(),
1662            Message::Barrier(Barrier::new_test_barrier(test_epoch(2)))
1663        );
1664
1665        // The last barrier message.
1666        assert_eq!(
1667            executor.next().await.unwrap().unwrap(),
1668            Message::Barrier(Barrier::new_test_barrier(test_epoch(3)))
1669        );
1670    }
1671
1672    #[tokio::test]
1673    async fn test_force_compaction() {
1674        use risingwave_common::array::StreamChunkTestExt;
1675        use risingwave_common::array::stream_chunk::StreamChunk;
1676        use risingwave_common::types::DataType;
1677
1678        use crate::executor::Barrier;
1679
1680        let properties = maplit::btreemap! {
1681            "connector".into() => "blackhole".into(),
1682            "force_compaction".into() => "true".into()
1683        };
1684
1685        // We have two visible columns and one hidden column. The hidden column will be pruned out
1686        // within the sink executor.
1687        let columns = vec![
1688            ColumnCatalog {
1689                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1690                is_hidden: false,
1691            },
1692            ColumnCatalog {
1693                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1694                is_hidden: false,
1695            },
1696            ColumnCatalog {
1697                column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1698                is_hidden: true,
1699            },
1700        ];
1701        let schema: Schema = columns
1702            .iter()
1703            .map(|column| Field::from(column.column_desc.clone()))
1704            .collect();
1705
1706        let source = MockSource::with_messages(vec![
1707            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1708            Message::Chunk(StreamChunk::from_pretty(
1709                " I I I
1710                    + 1 1 10",
1711            )),
1712            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1713            Message::Chunk(StreamChunk::from_pretty(
1714                " I I I
1715                    + 1 3 30",
1716            )),
1717            Message::Chunk(StreamChunk::from_pretty(
1718                " I I I
1719                    + 1 2 20
1720                    - 1 2 20
1721                    + 1 4 10",
1722            )),
1723            Message::Chunk(StreamChunk::from_pretty(
1724                " I I I
1725                    - 1 1 10
1726                    + 1 1 40",
1727            )),
1728            Message::Chunk(StreamChunk::from_pretty(
1729                " I I I
1730                    - 1 4 30",
1731            )),
1732            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1733        ])
1734        .into_executor(schema.clone(), vec![0, 1]);
1735
1736        let sink_param = SinkParam {
1737            sink_id: 0.into(),
1738            sink_name: "test".into(),
1739            properties,
1740
1741            columns: columns
1742                .iter()
1743                .filter(|col| !col.is_hidden)
1744                .map(|col| col.column_desc.clone())
1745                .collect(),
1746            downstream_pk: Some(vec![0, 1]),
1747            sink_type: SinkType::Upsert,
1748            ignore_delete: false,
1749            format_desc: None,
1750            db_name: "test".into(),
1751            sink_from_name: "test".into(),
1752        };
1753
1754        let info = ExecutorInfo::for_test(schema, vec![0, 1], "SinkExecutor".to_owned(), 0);
1755
1756        let sink = build_sink(sink_param.clone()).unwrap();
1757
1758        let sink_executor = SinkExecutor::new(
1759            ActorContext::for_test(0),
1760            info,
1761            source,
1762            SinkWriterParam::for_test(),
1763            sink,
1764            sink_param,
1765            columns.clone(),
1766            BoundedInMemLogStoreFactory::for_test(1),
1767            1024,
1768            vec![DataType::Int64, DataType::Int64, DataType::Int64],
1769            None,
1770        )
1771        .unwrap();
1772
1773        let mut executor = sink_executor.boxed().execute();
1774
1775        // Barrier message.
1776        executor.next().await.unwrap().unwrap();
1777
1778        let chunk_msg = executor.next().await.unwrap().unwrap();
1779        assert_eq!(
1780            chunk_msg.into_chunk().unwrap().compact_vis(),
1781            StreamChunk::from_pretty(
1782                " I I I
1783                + 1 1 10",
1784            )
1785        );
1786
1787        // Barrier message.
1788        executor.next().await.unwrap().unwrap();
1789
1790        let chunk_msg = executor.next().await.unwrap().unwrap();
1791        assert_eq!(
1792            chunk_msg.into_chunk().unwrap().compact_vis(),
1793            StreamChunk::from_pretty(
1794                " I I I
1795                + 1 3 30
1796                + 1 1 40",
1797            )
1798        );
1799
1800        // The last barrier message.
1801        executor.next().await.unwrap().unwrap();
1802    }
1803}