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