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                            if !sink_config_has_changes(&sink_param.properties, &config) {
838                                info!(
839                                    executor_id = %sink_writer_param.executor_id,
840                                    sink_id = %sink_param.sink_id,
841                                    "skip alter sink config because properties are unchanged"
842                                );
843                                Ok(())
844                            } else if F::ALLOW_REWIND {
845                                match log_reader.rewind().await {
846                                    Ok(()) => {
847                                        sink_param.properties.extend(config.into_iter());
848                                        sink = TryFrom::try_from(sink_param.clone()).map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
849                                        info!(
850                                            executor_id = %sink_writer_param.executor_id,
851                                            sink_id = %sink_param.sink_id,
852                                            "alter sink config successfully with rewind"
853                                        );
854                                        Ok(())
855                                    }
856                                    Err(rewind_err) => {
857                                        error!(
858                                            error = %rewind_err.as_report(),
859                                            "fail to rewind log reader for alter sink config "
860                                        );
861                                        Err(anyhow!("fail to rewind log after alter table").into())
862                                    }
863                                }
864                            } else {
865                                sink_param.properties.extend(config.into_iter());
866                                sink = TryFrom::try_from(sink_param.clone()).map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
867                                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())
868                            }
869                            .map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
870                        },
871                    }
872                }
873            }
874        }
875    }
876}
877
878enum RebuildSinkMessage {
879    RebuildSink(Arc<Bitmap>, oneshot::Sender<()>),
880    UpdateConfig(HashMap<String, String>),
881}
882
883fn sink_config_has_changes(
884    current: &BTreeMap<String, String>,
885    incoming: &HashMap<String, String>,
886) -> bool {
887    incoming
888        .iter()
889        .any(|(key, value)| current.get(key) != Some(value))
890}
891
892impl<F: LogStoreFactory> Execute for SinkExecutor<F> {
893    fn execute(self: Box<Self>) -> BoxedMessageStream {
894        self.execute_inner()
895    }
896}
897
898#[cfg(test)]
899mod test {
900    use risingwave_common::catalog::{ColumnDesc, ColumnId};
901    use risingwave_common::util::epoch::test_epoch;
902    use risingwave_connector::sink::build_sink;
903
904    use super::*;
905    use crate::common::log_store_impl::in_mem::BoundedInMemLogStoreFactory;
906    use crate::executor::test_utils::*;
907
908    #[test]
909    fn test_sink_config_has_changes() {
910        let current = BTreeMap::from([
911            ("connector".to_owned(), "blackhole".to_owned()),
912            ("commit_checkpoint_interval".to_owned(), "1".to_owned()),
913        ]);
914
915        assert!(!sink_config_has_changes(
916            &current,
917            &HashMap::from([("commit_checkpoint_interval".to_owned(), "1".to_owned())])
918        ));
919        assert!(sink_config_has_changes(
920            &current,
921            &HashMap::from([("commit_checkpoint_interval".to_owned(), "2".to_owned())])
922        ));
923        assert!(sink_config_has_changes(
924            &current,
925            &HashMap::from([("force_append_only".to_owned(), "true".to_owned())])
926        ));
927    }
928
929    #[tokio::test]
930    async fn test_force_append_only_sink() {
931        use risingwave_common::array::StreamChunkTestExt;
932        use risingwave_common::array::stream_chunk::StreamChunk;
933        use risingwave_common::types::DataType;
934
935        use crate::executor::Barrier;
936
937        let properties = maplit::btreemap! {
938            "connector".into() => "blackhole".into(),
939            "type".into() => "append-only".into(),
940            "force_append_only".into() => "true".into()
941        };
942
943        // We have two visible columns and one hidden column. The hidden column will be pruned out
944        // within the sink executor.
945        let columns = vec![
946            ColumnCatalog {
947                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
948                is_hidden: false,
949            },
950            ColumnCatalog {
951                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
952                is_hidden: false,
953            },
954            ColumnCatalog {
955                column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
956                is_hidden: true,
957            },
958        ];
959        let schema: Schema = columns
960            .iter()
961            .map(|column| Field::from(column.column_desc.clone()))
962            .collect();
963        let stream_key = vec![0];
964
965        let source = MockSource::with_messages(vec![
966            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
967            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
968                " I I I
969                    + 3 2 1",
970            ))),
971            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
972            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
973                "  I I I
974                    U- 3 2 1
975                    U+ 3 4 1
976                     + 5 6 7",
977            ))),
978            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
979                " I I I
980                    - 5 6 7",
981            ))),
982        ])
983        .into_executor(schema.clone(), stream_key.clone());
984
985        let sink_param = SinkParam {
986            sink_id: 0.into(),
987            sink_name: "test".into(),
988            properties,
989
990            columns: columns
991                .iter()
992                .filter(|col| !col.is_hidden)
993                .map(|col| col.column_desc.clone())
994                .collect(),
995            downstream_pk: Some(stream_key.clone()),
996            sink_type: SinkType::AppendOnly,
997            ignore_delete: true,
998            format_desc: None,
999            db_name: "test".into(),
1000            sink_from_name: "test".into(),
1001        };
1002
1003        let info = ExecutorInfo::for_test(schema, stream_key, "SinkExecutor".to_owned(), 0);
1004
1005        let sink = build_sink(sink_param.clone()).unwrap();
1006
1007        let sink_executor = SinkExecutor::new(
1008            ActorContext::for_test(0),
1009            info,
1010            source,
1011            SinkWriterParam::for_test(),
1012            sink,
1013            sink_param,
1014            columns.clone(),
1015            BoundedInMemLogStoreFactory::for_test(1),
1016            1024,
1017            vec![DataType::Int32, DataType::Int32, DataType::Int32],
1018            None,
1019        )
1020        .unwrap();
1021
1022        let mut executor = sink_executor.boxed().execute();
1023
1024        // Barrier message.
1025        executor.next().await.unwrap().unwrap();
1026
1027        let chunk_msg = executor.next().await.unwrap().unwrap();
1028        assert_eq!(
1029            chunk_msg.into_chunk().unwrap().compact_vis(),
1030            StreamChunk::from_pretty(
1031                " I I I
1032                + 3 2 1",
1033            )
1034        );
1035
1036        // Barrier message.
1037        executor.next().await.unwrap().unwrap();
1038
1039        let chunk_msg = executor.next().await.unwrap().unwrap();
1040        assert_eq!(
1041            chunk_msg.into_chunk().unwrap().compact_vis(),
1042            StreamChunk::from_pretty(
1043                " I I I
1044                + 3 4 1
1045                + 5 6 7",
1046            )
1047        );
1048
1049        // Should not receive the third stream chunk message because the force-append-only sink
1050        // executor will drop all DELETE messages.
1051
1052        // The last barrier message.
1053        executor.next().await.unwrap().unwrap();
1054    }
1055
1056    #[tokio::test]
1057    async fn stream_key_sink_pk_mismatch_upsert() {
1058        stream_key_sink_pk_mismatch(SinkType::Upsert).await;
1059    }
1060
1061    #[tokio::test]
1062    async fn stream_key_sink_pk_mismatch_retract() {
1063        stream_key_sink_pk_mismatch(SinkType::Retract).await;
1064    }
1065
1066    async fn stream_key_sink_pk_mismatch(sink_type: SinkType) {
1067        use risingwave_common::array::StreamChunkTestExt;
1068        use risingwave_common::array::stream_chunk::StreamChunk;
1069        use risingwave_common::types::DataType;
1070
1071        use crate::executor::Barrier;
1072
1073        let properties = maplit::btreemap! {
1074            "connector".into() => "blackhole".into(),
1075        };
1076
1077        // We have two visible columns and one hidden column. The hidden column will be pruned out
1078        // within the sink executor.
1079        let columns = vec![
1080            ColumnCatalog {
1081                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1082                is_hidden: false,
1083            },
1084            ColumnCatalog {
1085                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1086                is_hidden: false,
1087            },
1088            ColumnCatalog {
1089                column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1090                is_hidden: true,
1091            },
1092        ];
1093        let schema: Schema = columns
1094            .iter()
1095            .map(|column| Field::from(column.column_desc.clone()))
1096            .collect();
1097
1098        let source = MockSource::with_messages(vec![
1099            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1100            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1101                " I I I
1102                    + 1 1 10",
1103            ))),
1104            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1105            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1106                " I I I
1107                    + 1 3 30",
1108            ))),
1109            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1110                " I I I
1111                    + 1 2 20
1112                    - 1 2 20",
1113            ))),
1114            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1115                " I I I
1116                    - 1 1 10
1117                    + 1 1 40",
1118            ))),
1119            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1120        ])
1121        .into_executor(schema.clone(), vec![0, 1]);
1122
1123        let sink_param = SinkParam {
1124            sink_id: 0.into(),
1125            sink_name: "test".into(),
1126            properties,
1127
1128            columns: columns
1129                .iter()
1130                .filter(|col| !col.is_hidden)
1131                .map(|col| col.column_desc.clone())
1132                .collect(),
1133            downstream_pk: Some(vec![0]),
1134            sink_type,
1135            ignore_delete: false,
1136            format_desc: None,
1137            db_name: "test".into(),
1138            sink_from_name: "test".into(),
1139        };
1140
1141        let info = ExecutorInfo::for_test(schema, vec![0, 1], "SinkExecutor".to_owned(), 0);
1142
1143        let sink = build_sink(sink_param.clone()).unwrap();
1144
1145        let sink_executor = SinkExecutor::new(
1146            ActorContext::for_test(0),
1147            info,
1148            source,
1149            SinkWriterParam::for_test(),
1150            sink,
1151            sink_param,
1152            columns.clone(),
1153            BoundedInMemLogStoreFactory::for_test(1),
1154            1024,
1155            vec![DataType::Int64, DataType::Int64, DataType::Int64],
1156            None,
1157        )
1158        .unwrap();
1159
1160        let mut executor = sink_executor.boxed().execute();
1161
1162        // Barrier message.
1163        executor.next().await.unwrap().unwrap();
1164
1165        let chunk_msg = executor.next().await.unwrap().unwrap();
1166        assert_eq!(
1167            chunk_msg.into_chunk().unwrap().compact_vis(),
1168            StreamChunk::from_pretty(
1169                " I I I
1170                + 1 1 10",
1171            )
1172        );
1173
1174        // Barrier message.
1175        executor.next().await.unwrap().unwrap();
1176
1177        let chunk_msg = executor.next().await.unwrap().unwrap();
1178        let expected = match sink_type {
1179            SinkType::Retract => StreamChunk::from_pretty(
1180                " I I I
1181                U- 1 1 10
1182                U+ 1 1 40",
1183            ),
1184            SinkType::Upsert => StreamChunk::from_pretty(
1185                " I I I
1186                + 1 1 40", // For upsert format, there won't be `U- 1 1 10`.
1187            ),
1188            _ => unreachable!(),
1189        };
1190        assert_eq!(chunk_msg.into_chunk().unwrap().compact_vis(), expected);
1191
1192        // The last barrier message.
1193        executor.next().await.unwrap().unwrap();
1194    }
1195
1196    #[tokio::test]
1197    async fn test_sink_into_table_preserves_special_conflict_rows_for_mismatched_pk() {
1198        use risingwave_common::array::StreamChunkTestExt;
1199        use risingwave_common::array::stream_chunk::StreamChunk;
1200        use risingwave_common::types::DataType;
1201
1202        use crate::executor::Barrier;
1203
1204        let properties = maplit::btreemap! {
1205            "connector".into() => "table".into(),
1206            SINK_USER_PRESERVE_ROW_LEVEL_CHANGES.into() => "true".into(),
1207        };
1208
1209        let columns = vec![
1210            ColumnCatalog {
1211                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1212                is_hidden: false,
1213            },
1214            ColumnCatalog {
1215                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1216                is_hidden: false,
1217            },
1218            ColumnCatalog {
1219                column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1220                is_hidden: false,
1221            },
1222        ];
1223        let schema: Schema = columns
1224            .iter()
1225            .map(|column| Field::from(column.column_desc.clone()))
1226            .collect();
1227
1228        let source = MockSource::with_messages(vec![
1229            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1230            Message::Chunk(StreamChunk::from_pretty(
1231                " I  I  I
1232                  + 1 10  1",
1233            )),
1234            Message::Chunk(StreamChunk::from_pretty(
1235                " I  I  I
1236                  + 1 20  2",
1237            )),
1238            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1239        ])
1240        .into_executor(schema.clone(), vec![0, 2]);
1241
1242        let sink_param = SinkParam {
1243            sink_id: 0.into(),
1244            sink_name: "test".into(),
1245            properties,
1246            columns: columns.iter().map(|col| col.column_desc.clone()).collect(),
1247            downstream_pk: Some(vec![0]),
1248            sink_type: SinkType::Upsert,
1249            ignore_delete: false,
1250            format_desc: None,
1251            db_name: "test".into(),
1252            sink_from_name: "test".into(),
1253        };
1254
1255        let info = ExecutorInfo::for_test(schema, vec![0, 2], "SinkExecutor".to_owned(), 0);
1256        let sink = build_sink(sink_param.clone()).unwrap();
1257
1258        let sink_executor = SinkExecutor::new(
1259            ActorContext::for_test(0),
1260            info,
1261            source,
1262            SinkWriterParam::for_test(),
1263            sink,
1264            sink_param,
1265            columns,
1266            BoundedInMemLogStoreFactory::for_test(1),
1267            1024,
1268            vec![DataType::Int64, DataType::Int64, DataType::Int64],
1269            None,
1270        )
1271        .unwrap();
1272
1273        let mut executor = sink_executor.boxed().execute();
1274
1275        executor.next().await.unwrap().unwrap();
1276
1277        let chunk_msg = executor.next().await.unwrap().unwrap();
1278        assert_eq!(
1279            chunk_msg.into_chunk().unwrap().compact_vis(),
1280            StreamChunk::from_pretty(
1281                " I  I  I
1282                  + 1 10  1
1283                  + 1 20  2",
1284            )
1285        );
1286
1287        executor.next().await.unwrap().unwrap();
1288    }
1289
1290    #[tokio::test]
1291    async fn test_sink_into_table_keeps_default_compaction_without_special_conflict_semantics() {
1292        use risingwave_common::array::StreamChunkTestExt;
1293        use risingwave_common::array::stream_chunk::StreamChunk;
1294        use risingwave_common::types::DataType;
1295
1296        use crate::executor::Barrier;
1297
1298        let properties = maplit::btreemap! {
1299            "connector".into() => "table".into(),
1300        };
1301
1302        let columns = vec![
1303            ColumnCatalog {
1304                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1305                is_hidden: false,
1306            },
1307            ColumnCatalog {
1308                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1309                is_hidden: false,
1310            },
1311        ];
1312        let schema: Schema = columns
1313            .iter()
1314            .map(|column| Field::from(column.column_desc.clone()))
1315            .collect();
1316
1317        let source = MockSource::with_messages(vec![
1318            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1319            Message::Chunk(StreamChunk::from_pretty(
1320                " I  I
1321                  + 1 10
1322                  + 1 20",
1323            )),
1324            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1325        ])
1326        .into_executor(schema.clone(), vec![0]);
1327
1328        let sink_param = SinkParam {
1329            sink_id: 0.into(),
1330            sink_name: "test".into(),
1331            properties,
1332            columns: columns.iter().map(|col| col.column_desc.clone()).collect(),
1333            downstream_pk: Some(vec![0]),
1334            sink_type: SinkType::Upsert,
1335            ignore_delete: false,
1336            format_desc: None,
1337            db_name: "test".into(),
1338            sink_from_name: "test".into(),
1339        };
1340
1341        let info = ExecutorInfo::for_test(schema, vec![0], "SinkExecutor".to_owned(), 0);
1342        let sink = build_sink(sink_param.clone()).unwrap();
1343
1344        let sink_executor = SinkExecutor::new(
1345            ActorContext::for_test(0),
1346            info,
1347            source,
1348            SinkWriterParam::for_test(),
1349            sink,
1350            sink_param,
1351            columns,
1352            BoundedInMemLogStoreFactory::for_test(1),
1353            1024,
1354            vec![DataType::Int64, DataType::Int64],
1355            None,
1356        )
1357        .unwrap();
1358
1359        let mut executor = sink_executor.boxed().execute();
1360
1361        executor.next().await.unwrap().unwrap();
1362
1363        let chunk_msg = executor.next().await.unwrap().unwrap();
1364        assert_eq!(
1365            chunk_msg.into_chunk().unwrap().compact_vis(),
1366            StreamChunk::from_pretty(
1367                " I  I
1368                  + 1 20",
1369            )
1370        );
1371
1372        executor.next().await.unwrap().unwrap();
1373    }
1374
1375    #[tokio::test]
1376    async fn test_empty_barrier_sink() {
1377        use risingwave_common::types::DataType;
1378
1379        use crate::executor::Barrier;
1380
1381        let properties = maplit::btreemap! {
1382            "connector".into() => "blackhole".into(),
1383            "type".into() => "append-only".into(),
1384            "force_append_only".into() => "true".into()
1385        };
1386        let columns = vec![
1387            ColumnCatalog {
1388                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1389                is_hidden: false,
1390            },
1391            ColumnCatalog {
1392                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1393                is_hidden: false,
1394            },
1395        ];
1396        let schema: Schema = columns
1397            .iter()
1398            .map(|column| Field::from(column.column_desc.clone()))
1399            .collect();
1400        let stream_key = vec![0];
1401
1402        let source = MockSource::with_messages(vec![
1403            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1404            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1405            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1406        ])
1407        .into_executor(schema.clone(), stream_key.clone());
1408
1409        let sink_param = SinkParam {
1410            sink_id: 0.into(),
1411            sink_name: "test".into(),
1412            properties,
1413
1414            columns: columns
1415                .iter()
1416                .filter(|col| !col.is_hidden)
1417                .map(|col| col.column_desc.clone())
1418                .collect(),
1419            downstream_pk: Some(stream_key.clone()),
1420            sink_type: SinkType::AppendOnly,
1421            ignore_delete: true,
1422            format_desc: None,
1423            db_name: "test".into(),
1424            sink_from_name: "test".into(),
1425        };
1426
1427        let info = ExecutorInfo::for_test(schema, stream_key, "SinkExecutor".to_owned(), 0);
1428
1429        let sink = build_sink(sink_param.clone()).unwrap();
1430
1431        let sink_executor = SinkExecutor::new(
1432            ActorContext::for_test(0),
1433            info,
1434            source,
1435            SinkWriterParam::for_test(),
1436            sink,
1437            sink_param,
1438            columns,
1439            BoundedInMemLogStoreFactory::for_test(1),
1440            1024,
1441            vec![DataType::Int64, DataType::Int64],
1442            None,
1443        )
1444        .unwrap();
1445
1446        let mut executor = sink_executor.boxed().execute();
1447
1448        // Barrier message.
1449        assert_eq!(
1450            executor.next().await.unwrap().unwrap(),
1451            Message::Barrier(Barrier::new_test_barrier(test_epoch(1)))
1452        );
1453
1454        // Barrier message.
1455        assert_eq!(
1456            executor.next().await.unwrap().unwrap(),
1457            Message::Barrier(Barrier::new_test_barrier(test_epoch(2)))
1458        );
1459
1460        // The last barrier message.
1461        assert_eq!(
1462            executor.next().await.unwrap().unwrap(),
1463            Message::Barrier(Barrier::new_test_barrier(test_epoch(3)))
1464        );
1465    }
1466
1467    #[tokio::test]
1468    async fn test_force_compaction() {
1469        use risingwave_common::array::StreamChunkTestExt;
1470        use risingwave_common::array::stream_chunk::StreamChunk;
1471        use risingwave_common::types::DataType;
1472
1473        use crate::executor::Barrier;
1474
1475        let properties = maplit::btreemap! {
1476            "connector".into() => "blackhole".into(),
1477            "force_compaction".into() => "true".into()
1478        };
1479
1480        // We have two visible columns and one hidden column. The hidden column will be pruned out
1481        // within the sink executor.
1482        let columns = vec![
1483            ColumnCatalog {
1484                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1485                is_hidden: false,
1486            },
1487            ColumnCatalog {
1488                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1489                is_hidden: false,
1490            },
1491            ColumnCatalog {
1492                column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1493                is_hidden: true,
1494            },
1495        ];
1496        let schema: Schema = columns
1497            .iter()
1498            .map(|column| Field::from(column.column_desc.clone()))
1499            .collect();
1500
1501        let source = MockSource::with_messages(vec![
1502            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1503            Message::Chunk(StreamChunk::from_pretty(
1504                " I I I
1505                    + 1 1 10",
1506            )),
1507            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1508            Message::Chunk(StreamChunk::from_pretty(
1509                " I I I
1510                    + 1 3 30",
1511            )),
1512            Message::Chunk(StreamChunk::from_pretty(
1513                " I I I
1514                    + 1 2 20
1515                    - 1 2 20
1516                    + 1 4 10",
1517            )),
1518            Message::Chunk(StreamChunk::from_pretty(
1519                " I I I
1520                    - 1 1 10
1521                    + 1 1 40",
1522            )),
1523            Message::Chunk(StreamChunk::from_pretty(
1524                " I I I
1525                    - 1 4 30",
1526            )),
1527            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1528        ])
1529        .into_executor(schema.clone(), vec![0, 1]);
1530
1531        let sink_param = SinkParam {
1532            sink_id: 0.into(),
1533            sink_name: "test".into(),
1534            properties,
1535
1536            columns: columns
1537                .iter()
1538                .filter(|col| !col.is_hidden)
1539                .map(|col| col.column_desc.clone())
1540                .collect(),
1541            downstream_pk: Some(vec![0, 1]),
1542            sink_type: SinkType::Upsert,
1543            ignore_delete: false,
1544            format_desc: None,
1545            db_name: "test".into(),
1546            sink_from_name: "test".into(),
1547        };
1548
1549        let info = ExecutorInfo::for_test(schema, vec![0, 1], "SinkExecutor".to_owned(), 0);
1550
1551        let sink = build_sink(sink_param.clone()).unwrap();
1552
1553        let sink_executor = SinkExecutor::new(
1554            ActorContext::for_test(0),
1555            info,
1556            source,
1557            SinkWriterParam::for_test(),
1558            sink,
1559            sink_param,
1560            columns.clone(),
1561            BoundedInMemLogStoreFactory::for_test(1),
1562            1024,
1563            vec![DataType::Int64, DataType::Int64, DataType::Int64],
1564            None,
1565        )
1566        .unwrap();
1567
1568        let mut executor = sink_executor.boxed().execute();
1569
1570        // Barrier message.
1571        executor.next().await.unwrap().unwrap();
1572
1573        let chunk_msg = executor.next().await.unwrap().unwrap();
1574        assert_eq!(
1575            chunk_msg.into_chunk().unwrap().compact_vis(),
1576            StreamChunk::from_pretty(
1577                " I I I
1578                + 1 1 10",
1579            )
1580        );
1581
1582        // Barrier message.
1583        executor.next().await.unwrap().unwrap();
1584
1585        let chunk_msg = executor.next().await.unwrap().unwrap();
1586        assert_eq!(
1587            chunk_msg.into_chunk().unwrap().compact_vis(),
1588            StreamChunk::from_pretty(
1589                " I I I
1590                + 1 3 30
1591                + 1 1 40",
1592            )
1593        );
1594
1595        // The last barrier message.
1596        executor.next().await.unwrap().unwrap();
1597    }
1598}