1use 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
78fn 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
91fn 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
104fn 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#[derive(Clone, Copy, Debug)]
133struct NonAppendOnlyBehavior {
134 pk_specified_and_matched: bool,
139 force_compaction: bool,
141}
142
143impl NonAppendOnlyBehavior {
144 fn should_compact_in_log_reader(self) -> bool {
165 self.pk_specified_and_matched && !self.force_compaction
166 }
167
168 fn should_reorder_records(self) -> bool {
237 !self.pk_specified_and_matched || self.force_compaction
238 }
239}
240
241fn compact_output_kind(sink_type: SinkType) -> OutputKind {
243 match sink_type {
244 SinkType::Upsert => output_kind::UPSERT,
245 SinkType::Retract => output_kind::RETRACT,
246 SinkType::AppendOnly => output_kind::RETRACT,
248 }
249}
250
251macro_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 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 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(), );
384
385 let processed_input = if self.sink_param.ignore_delete {
386 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 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 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); 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 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 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 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 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 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 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 #[for_await]
703 for msg in input {
704 match msg? {
705 Message::Watermark(w) => yield Message::Watermark(w),
706 Message::Chunk(mut chunk) => {
707 if !sink_type.is_append_only()
711 && let Some(downstream_pk) = &downstream_pk
712 {
713 if preserve_row_level_changes {
714 } else if skip_compact {
717 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 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 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 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 InconsistencyBehavior::Warn,
812 )
813 })
814 } else {
815 chunk
816 };
817 if needs_projection {
818 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 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 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 ¤t,
1097 &HashMap::from([("commit_checkpoint_interval".to_owned(), "1".to_owned())])
1098 ));
1099 assert!(sink_config_has_changes(
1100 ¤t,
1101 &HashMap::from([("commit_checkpoint_interval".to_owned(), "2".to_owned())])
1102 ));
1103 assert!(sink_config_has_changes(
1104 ¤t,
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 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 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 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 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 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 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 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 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", ),
1531 _ => unreachable!(),
1532 };
1533 assert_eq!(chunk_msg.into_chunk().unwrap().compact_vis(), expected);
1534
1535 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 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 assert_eq!(
1814 executor.next().await.unwrap().unwrap(),
1815 Message::Barrier(Barrier::new_test_barrier(test_epoch(1)))
1816 );
1817
1818 assert_eq!(
1820 executor.next().await.unwrap().unwrap(),
1821 Message::Barrier(Barrier::new_test_barrier(test_epoch(2)))
1822 );
1823
1824 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 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 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 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 executor.next().await.unwrap().unwrap();
1961 }
1962}