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, RelabeledAggregatedIntGauge};
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_is_upsert = self.input.stream_kind() == StreamKind::Upsert;
352 let input_compact_ib = if input_is_upsert {
355 InconsistencyBehavior::Tolerate
356 } else {
357 InconsistencyBehavior::Panic
358 };
359
360 let input = self.input.execute();
361
362 let input = input.inspect_ok(move |msg| {
363 if let Message::Chunk(c) = msg {
364 metrics.sink_input_row_count.inc_by(c.capacity() as u64);
365 metrics.sink_input_bytes.inc_by(c.estimated_size() as u64);
366 }
367 });
368
369 let processed_input = Self::process_msg(
370 input,
371 self.sink_param.sink_type,
372 stream_key,
373 self.chunk_size,
374 self.input_data_types,
375 input_compact_ib,
376 input_is_upsert,
377 self.sink_param.downstream_pk.clone(),
378 self.non_append_only_behavior,
379 metrics.sink_chunk_buffer_size,
380 self.sink_param
381 .properties
382 .get(SINK_USER_PRESERVE_ROW_LEVEL_CHANGES)
383 .is_some_and(|v| v.eq_ignore_ascii_case("true")),
384 self.sink.is_blackhole(), );
386
387 let processed_input = if self.sink_param.ignore_delete {
388 processed_input
390 .map_ok(|msg| match msg {
391 Message::Chunk(chunk) => Message::Chunk(force_append_only(chunk)),
392 other => other,
393 })
394 .left_stream()
395 } else {
396 processed_input.right_stream()
397 };
398
399 if self.sink.is_sink_into_table() {
400 processed_input.boxed()
402 } else {
403 let labels = [
404 &actor_id.to_string(),
405 &sink_id.to_string(),
406 self.sink_param.sink_name.as_str(),
407 ];
408 let log_store_first_write_epoch = GLOBAL_SINK_METRICS
409 .log_store_first_write_epoch
410 .with_guarded_label_values(&labels);
411 let log_store_latest_write_epoch = GLOBAL_SINK_METRICS
412 .log_store_latest_write_epoch
413 .with_guarded_label_values(&labels);
414 let log_store_write_rows = GLOBAL_SINK_METRICS
415 .log_store_write_rows
416 .with_guarded_label_values(&labels);
417 let log_writer_metrics = LogWriterMetrics {
418 log_store_first_write_epoch,
419 log_store_latest_write_epoch,
420 log_store_write_rows,
421 };
422
423 let (rate_limit_tx, rate_limit_rx) = unbounded_channel();
424 rate_limit_tx.send(self.rate_limit.into()).unwrap();
426
427 let (rebuild_sink_tx, rebuild_sink_rx) = unbounded_channel();
428
429 self.log_store_factory
430 .build()
431 .map(move |(log_reader, log_writer)| {
432 let write_log_stream = Self::execute_write_log(
433 processed_input,
434 log_writer.monitored(log_writer_metrics),
435 actor_id,
436 fragment_id,
437 sink_id,
438 rate_limit_tx,
439 rebuild_sink_tx,
440 );
441
442 let consume_log_stream_future = dispatch_sink!(self.sink, sink, {
443 let consume_log_stream = Self::execute_consume_log(
444 *sink,
445 log_reader,
446 self.input_columns,
447 self.sink_param,
448 self.sink_writer_param,
449 self.non_append_only_behavior,
450 self.actor_context,
451 rate_limit_rx,
452 rebuild_sink_rx,
453 )
454 .instrument_await(
455 await_tree::span!("consume_log (sink_id {sink_id})").long_running(),
456 )
457 .map_ok(|never| never); consume_log_stream.boxed()
460 });
461 select(consume_log_stream_future.into_stream(), write_log_stream)
462 })
463 .into_stream()
464 .flatten()
465 .boxed()
466 }
467 }
468
469 #[try_stream(ok = Message, error = StreamExecutorError)]
470 async fn execute_write_log<W: LogWriter>(
471 input: impl MessageStream,
472 mut log_writer: W,
473 actor_id: ActorId,
474 fragment_id: FragmentId,
475 sink_id: SinkId,
476 rate_limit_tx: UnboundedSender<RateLimit>,
477 rebuild_sink_tx: UnboundedSender<RebuildSinkMessage>,
478 ) {
479 pin_mut!(input);
480 let barrier = expect_first_barrier(&mut input).await?;
481 let epoch_pair = barrier.epoch;
482 let is_pause_on_startup = barrier.is_pause_on_startup();
483 yield Message::Barrier(barrier);
485
486 log_writer.init(epoch_pair, is_pause_on_startup).await?;
487
488 let mut is_paused = false;
489
490 #[for_await]
491 for msg in input {
492 match msg? {
493 Message::Watermark(w) => yield Message::Watermark(w),
494 Message::Chunk(chunk) => {
495 assert!(
496 !is_paused,
497 "Actor {actor_id} should not receive any data after pause"
498 );
499 log_writer.write_chunk(chunk.clone()).await?;
500 yield Message::Chunk(chunk);
501 }
502 Message::Barrier(barrier) => {
503 let update_vnode_bitmap = barrier.as_update_vnode_bitmap(actor_id);
504 let schema_change = barrier.as_sink_schema_change(sink_id);
505 let wait_log_store_flush = barrier.should_flush_sink_log_store(sink_id);
506 if let Some(schema_change) = &schema_change {
507 info!(?schema_change, %sink_id, "sink receive schema change");
508 }
509 if wait_log_store_flush {
510 info!(%sink_id, "sink wait for log store flush");
511 }
512 let post_flush = log_writer
513 .flush_current_epoch(
514 barrier.epoch.curr,
515 FlushCurrentEpochOptions {
516 is_checkpoint: barrier.kind.is_checkpoint(),
517 new_vnode_bitmap: update_vnode_bitmap.clone(),
518 is_stop: barrier.is_stop(actor_id),
519 schema_change,
520 wait_log_store_flush,
521 },
522 )
523 .await?;
524
525 let mutation = barrier.mutation.clone();
526 yield Message::Barrier(barrier);
527 if F::REBUILD_SINK_ON_UPDATE_VNODE_BITMAP
528 && let Some(new_vnode_bitmap) = update_vnode_bitmap.clone()
529 {
530 let (tx, rx) = oneshot::channel();
531 rebuild_sink_tx
532 .send(RebuildSinkMessage::RebuildSink(new_vnode_bitmap, tx))
533 .map_err(|_| {
534 anyhow!("failed to send the rebuild-sink request to the reader")
535 })?;
536 rx.await
537 .map_err(|_| anyhow!("failed to wait for sink rebuild to finish"))?;
538 }
539 post_flush.post_yield_barrier().await?;
540
541 if let Some(mutation) = mutation.as_deref() {
542 match mutation {
543 Mutation::Pause => {
544 log_writer.pause()?;
545 is_paused = true;
546 }
547 Mutation::Resume => {
548 log_writer.resume()?;
549 is_paused = false;
550 }
551 Mutation::Throttle(fragment_to_apply) => {
552 if let Some(entry) = fragment_to_apply.get(&fragment_id)
553 && entry.throttle_type() == ThrottleType::Sink
554 {
555 tracing::info!(
556 rate_limit = entry.rate_limit,
557 "received sink rate limit on actor {actor_id}"
558 );
559 if let Err(e) = rate_limit_tx.send(entry.rate_limit.into()) {
560 error!(
561 error = %e.as_report(),
562 "failed to send the sink rate limit update"
563 );
564 return Err(StreamExecutorError::from(
565 e.to_report_string(),
566 ));
567 }
568 }
569 }
570 Mutation::ConnectorPropsChange(config) => {
571 if let Some(map) = config.get(&sink_id.as_raw_id())
572 && let Err(e) = rebuild_sink_tx
573 .send(RebuildSinkMessage::UpdateConfig(map.clone()))
574 {
575 error!(
576 error = %e.as_report(),
577 "failed to send sink property updates"
578 );
579 return Err(StreamExecutorError::from(e.to_report_string()));
580 }
581 }
582 _ => (),
583 }
584 }
585 }
586 }
587 }
588 }
589
590 #[expect(clippy::too_many_arguments)]
591 #[try_stream(ok = Message, error = StreamExecutorError)]
592 async fn process_msg(
593 input: impl MessageStream,
594 sink_type: SinkType,
595 stream_key: StreamKey,
596 chunk_size: usize,
597 input_data_types: Vec<DataType>,
598 input_compact_ib: InconsistencyBehavior,
599 input_is_upsert: bool,
600 downstream_pk: Option<Vec<usize>>,
601 non_append_only_behavior: Option<NonAppendOnlyBehavior>,
602 sink_chunk_buffer_size_metrics: RelabeledAggregatedIntGauge,
603 preserve_row_level_changes: bool,
604 skip_compact: bool,
605 ) {
606 if let Some(b) = non_append_only_behavior
608 && b.should_reorder_records()
609 {
610 assert_matches!(sink_type, SinkType::Upsert | SinkType::Retract);
611
612 let mut chunk_buffer = EstimatedVec::new();
613 let mut watermark: Option<super::Watermark> = None;
614 #[for_await]
615 for msg in input {
616 match msg? {
617 Message::Watermark(w) => watermark = Some(w),
618 Message::Chunk(c) => {
619 chunk_buffer.push(c);
620 sink_chunk_buffer_size_metrics.set(chunk_buffer.estimated_size() as i64);
621 }
622 Message::Barrier(barrier) => {
623 let chunks = mem::take(&mut chunk_buffer).into_inner();
624
625 let mut delete_chunks = vec![];
632 let mut insert_chunks = vec![];
633
634 let compactor = StreamChunkCompactor::new(stream_key.clone(), chunks);
635 let compacted = if downstream_pk.is_some() && !input_is_upsert {
636 compactor.into_compacted_chunks_inline::<{ output_kind::RETRACT }>(
637 input_compact_ib,
638 )
639 } else {
640 dispatch_output_kind!(sink_type, KIND, {
641 compactor.into_compacted_chunks_inline::<KIND>(input_compact_ib)
642 })
643 };
644 for c in compacted {
645 let chunk = force_delete_only(c.clone());
646 if chunk.cardinality() > 0 {
647 delete_chunks.push(chunk);
648 }
649 let chunk = force_append_only(c);
650 if chunk.cardinality() > 0 {
651 insert_chunks.push(chunk);
652 }
653 }
654 if let Some(downstream_pk) = &downstream_pk
661 && !preserve_row_level_changes
662 {
663 let chunks = delete_chunks
664 .into_iter()
665 .chain(insert_chunks.into_iter())
666 .collect();
667 let chunks = dispatch_output_kind!(sink_type, KIND, {
668 StreamChunkCompactor::new(downstream_pk.clone(), chunks)
669 .into_compacted_chunks_reconstructed::<KIND>(
670 chunk_size,
671 input_data_types.clone(),
672 InconsistencyBehavior::Warn,
675 )
676 });
677 for c in chunks {
678 yield Message::Chunk(c);
679 }
680 } else {
681 if let Some(downstream_pk) = &downstream_pk
682 && compact_output_kind(sink_type) == output_kind::UPSERT
683 {
684 delete_chunks = drop_deletes_of_reinserted_keys(
685 delete_chunks,
686 &insert_chunks,
687 downstream_pk,
688 );
689 }
690 let mut chunk_builder =
691 StreamChunkBuilder::new(chunk_size, input_data_types.clone());
692 for chunk in delete_chunks.into_iter().chain(insert_chunks) {
693 for (op, row) in chunk.rows() {
694 if let Some(c) = chunk_builder.append_row(op, row) {
695 yield Message::Chunk(c);
696 }
697 }
698 }
699
700 if let Some(c) = chunk_builder.take() {
701 yield Message::Chunk(c);
702 }
703 };
704
705 if let Some(w) = mem::take(&mut watermark) {
707 yield Message::Watermark(w)
708 }
709 yield Message::Barrier(barrier);
710 }
711 }
712 }
713 } else {
714 #[for_await]
717 for msg in input {
718 match msg? {
719 Message::Watermark(w) => yield Message::Watermark(w),
720 Message::Chunk(mut chunk) => {
721 if !sink_type.is_append_only()
725 && let Some(downstream_pk) = &downstream_pk
726 {
727 if preserve_row_level_changes {
728 } else if skip_compact {
731 assert_eq!(&stream_key, downstream_pk);
734 } else {
735 chunk = dispatch_output_kind!(sink_type, KIND, {
736 compact_chunk_inline::<KIND>(
737 chunk,
738 downstream_pk,
739 InconsistencyBehavior::Warn,
742 )
743 });
744 }
745 }
746 yield Message::Chunk(chunk);
747 }
748 Message::Barrier(barrier) => {
749 yield Message::Barrier(barrier);
750 }
751 }
752 }
753 }
754 }
755
756 #[expect(clippy::too_many_arguments)]
757 async fn execute_consume_log<S: Sink, R: LogReader>(
758 mut sink: S,
759 log_reader: R,
760 columns: Vec<ColumnCatalog>,
761 mut sink_param: SinkParam,
762 mut sink_writer_param: SinkWriterParam,
763 non_append_only_behavior: Option<NonAppendOnlyBehavior>,
764 actor_context: ActorContextRef,
765 rate_limit_rx: UnboundedReceiver<RateLimit>,
766 mut rebuild_sink_rx: UnboundedReceiver<RebuildSinkMessage>,
767 ) -> StreamExecutorResult<!> {
768 let mut visible_columns = columns
769 .iter()
770 .enumerate()
771 .filter_map(|(idx, column)| (!column.is_hidden).then_some(idx))
772 .collect_vec();
773
774 let needs_projection = visible_columns.len() != columns.len();
775
776 if needs_projection
777 && let Some(extra_partition_col_idx) = sink_writer_param.extra_partition_col_idx
778 {
779 debug_assert_eq!(extra_partition_col_idx, columns.len());
781 sink_writer_param.extra_partition_col_idx = Some(visible_columns.len());
782 visible_columns.push(extra_partition_col_idx);
783 }
784
785 let labels = [
786 &actor_context.id.to_string(),
787 sink_writer_param.connector.as_str(),
788 &sink_writer_param.sink_id.to_string(),
789 sink_writer_param.sink_name.as_str(),
790 ];
791 let log_store_reader_wait_new_future_duration_ns = GLOBAL_SINK_METRICS
792 .log_store_reader_wait_new_future_duration_ns
793 .with_guarded_label_values(&labels);
794 let log_store_read_rows = GLOBAL_SINK_METRICS
795 .log_store_read_rows
796 .with_guarded_label_values(&labels);
797 let log_store_read_bytes = GLOBAL_SINK_METRICS
798 .log_store_read_bytes
799 .with_guarded_label_values(&labels);
800 let log_store_latest_read_epoch = GLOBAL_SINK_METRICS
801 .log_store_latest_read_epoch
802 .with_guarded_label_values(&labels);
803 let metrics = LogReaderMetrics {
804 log_store_latest_read_epoch,
805 log_store_read_rows,
806 log_store_read_bytes,
807 log_store_reader_wait_new_future_duration_ns,
808 };
809
810 let downstream_pk = sink_param.downstream_pk.clone();
811
812 let mut log_reader = log_reader
813 .transform_chunk(move |chunk| {
814 let chunk = if let Some(b) = non_append_only_behavior
815 && b.should_compact_in_log_reader()
816 {
817 let downstream_pk = downstream_pk.as_ref().unwrap();
819 dispatch_output_kind!(sink_param.sink_type, KIND, {
820 compact_chunk_inline::<KIND>(
821 chunk,
822 downstream_pk,
823 InconsistencyBehavior::Warn,
826 )
827 })
828 } else {
829 chunk
830 };
831 if needs_projection {
832 chunk.project(&visible_columns)
835 } else {
836 chunk
837 }
838 })
839 .monitored(metrics)
840 .rate_limited(rate_limit_rx);
841
842 log_reader.init().await?;
843 let mut retry_backoff = sink_retry_backoff();
844 loop {
845 let mut future = Box::pin(async {
847 loop {
848 let attempt_started_at = Instant::now();
849 let Err(e) = sink
850 .new_log_sinker(sink_writer_param.clone())
851 .and_then(|log_sinker| log_sinker.consume_log_and_sink(&mut log_reader))
852 .await;
853 let attempt_duration = attempt_started_at.elapsed();
854 GLOBAL_ERROR_METRICS.user_sink_error.report([
855 "sink_executor_error".to_owned(),
856 sink_param.sink_id.to_string(),
857 sink_param.sink_name.clone(),
858 actor_context.fragment_id.to_string(),
859 ]);
860
861 if let Some(meta_client) = sink_writer_param.meta_client.as_ref() {
862 meta_client
863 .add_sink_fail_evet_log(
864 sink_writer_param.sink_id,
865 sink_writer_param.sink_name.clone(),
866 sink_writer_param.connector.clone(),
867 e.to_report_string(),
868 )
869 .await;
870 }
871
872 if F::ALLOW_REWIND {
873 match log_reader.rewind().await {
874 Ok(()) => {
875 if attempt_duration >= SINK_RETRY_BACKOFF_RESET_INTERVAL {
876 retry_backoff = sink_retry_backoff();
877 }
878 let retry_delay =
879 retry_backoff.next().expect("retry strategy is infinite");
880 error!(
881 error = %e.as_report(),
882 executor_id = %sink_writer_param.executor_id,
883 sink_id = %sink_param.sink_id,
884 ?retry_delay,
885 "reset log reader stream successfully after sink error; retrying after backoff"
886 );
887 tokio::time::sleep(retry_delay).await;
888 Ok(())
889 }
890 Err(rewind_err) => {
891 error!(
892 error = %rewind_err.as_report(),
893 "failed to rewind the log reader"
894 );
895 Err(e)
896 }
897 }
898 } else {
899 Err(e)
900 }
901 .map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
902 }
903 });
904 let message = loop {
905 select! {
906 result = &mut future => {
907 let Err(e): StreamExecutorResult<!> = result;
908 return Err(e);
909 }
910 result = rebuild_sink_rx.recv() => {
911 let message = result.ok_or_else(|| anyhow!("failed to receive rebuild sink notify"))?;
912 if let RebuildSinkMessage::UpdateConfig(config) = &message
915 && !sink_config_has_changes(&sink_param.properties, config)
916 {
917 info!(
918 executor_id = %sink_writer_param.executor_id,
919 sink_id = %sink_param.sink_id,
920 "skip alter sink config because properties are unchanged"
921 );
922 continue;
923 }
924 break message;
925 }
926 }
927 };
928 drop(future);
929 match message {
930 RebuildSinkMessage::RebuildSink(new_vnode, notify) => {
931 sink_writer_param.vnode_bitmap = (*new_vnode).clone();
932 if notify.send(()).is_err() {
933 warn!("failed to notify rebuild sink");
934 }
935 log_reader.init().await?;
936 }
937 RebuildSinkMessage::UpdateConfig(config) => {
938 if F::ALLOW_REWIND {
939 match log_reader.rewind().await {
940 Ok(()) => {
941 sink_param.properties.extend(config);
942 sink = TryFrom::try_from(sink_param.clone()).map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
943 info!(
944 executor_id = %sink_writer_param.executor_id,
945 sink_id = %sink_param.sink_id,
946 "alter sink config successfully with rewind"
947 );
948 Ok(())
949 }
950 Err(rewind_err) => {
951 error!(
952 error = %rewind_err.as_report(),
953 "failed to rewind the log reader for ALTER SINK CONFIG"
954 );
955 Err(anyhow!("failed to rewind the log after ALTER SINK CONFIG").into())
956 }
957 }
958 } else {
959 sink_param.properties.extend(config);
960 sink = TryFrom::try_from(sink_param.clone()).map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
961 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())
962 }
963 .map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
964 }
965 }
966 }
967 }
968}
969
970enum RebuildSinkMessage {
971 RebuildSink(Arc<Bitmap>, oneshot::Sender<()>),
972 UpdateConfig(HashMap<String, String>),
973}
974
975fn sink_config_has_changes(
976 current: &BTreeMap<String, String>,
977 incoming: &HashMap<String, String>,
978) -> bool {
979 incoming
980 .iter()
981 .any(|(key, value)| current.get(key) != Some(value))
982}
983
984impl<F: LogStoreFactory> Execute for SinkExecutor<F> {
985 fn execute(self: Box<Self>) -> BoxedMessageStream {
986 self.execute_inner()
987 }
988}
989
990#[cfg(test)]
991mod test {
992 use std::future::pending;
993 use std::sync::Arc;
994 use std::sync::atomic::{AtomicUsize, Ordering};
995 use std::time::Duration;
996
997 use risingwave_common::catalog::{ColumnDesc, ColumnId};
998 use risingwave_common::util::epoch::test_epoch;
999 use risingwave_connector::sink::build_sink;
1000 use risingwave_connector::sink::log_store::{LogStoreReadItem, LogStoreResult, TruncateOffset};
1001 use risingwave_connector::sink::trivial::BlackHoleSink;
1002 use tokio::sync::Notify;
1003
1004 use super::*;
1005 use crate::common::log_store_impl::in_mem::{
1006 BoundedInMemLogStoreFactory, BoundedInMemLogStoreWriter,
1007 };
1008 use crate::executor::test_utils::*;
1009
1010 #[derive(Default)]
1011 struct RewindRequiredLogReaderState {
1012 start_count: AtomicUsize,
1013 rewind_count: AtomicUsize,
1014 started: Notify,
1015 }
1016
1017 impl RewindRequiredLogReaderState {
1018 async fn wait_for_start_count(&self, expected: usize) {
1019 tokio::time::timeout(Duration::from_secs(5), async {
1020 loop {
1021 let started = self.started.notified();
1022 if self.start_count.load(Ordering::SeqCst) >= expected {
1023 break;
1024 }
1025 started.await;
1026 }
1027 })
1028 .await
1029 .unwrap_or_else(|_| panic!("log reader was not started {expected} times"));
1030 }
1031 }
1032
1033 struct RewindRequiredLogReader {
1034 is_reset: bool,
1035 state: Arc<RewindRequiredLogReaderState>,
1036 }
1037
1038 impl LogReader for RewindRequiredLogReader {
1039 async fn init(&mut self) -> LogStoreResult<()> {
1040 self.is_reset = true;
1041 Ok(())
1042 }
1043
1044 async fn start_from(&mut self, _start_offset: Option<u64>) -> LogStoreResult<()> {
1045 assert!(
1046 self.is_reset,
1047 "log reader must be rewound before restarting"
1048 );
1049 self.is_reset = false;
1050 self.state.start_count.fetch_add(1, Ordering::SeqCst);
1051 self.state.started.notify_waiters();
1052 Ok(())
1053 }
1054
1055 async fn next_item(&mut self) -> LogStoreResult<(u64, LogStoreReadItem)> {
1056 pending().await
1057 }
1058
1059 fn truncate(&mut self, _offset: TruncateOffset) -> LogStoreResult<()> {
1060 Ok(())
1061 }
1062
1063 async fn rewind(&mut self) -> LogStoreResult<()> {
1064 self.is_reset = true;
1065 self.state.rewind_count.fetch_add(1, Ordering::SeqCst);
1066 Ok(())
1067 }
1068 }
1069
1070 struct TestLogStoreFactory<const CAN_REWIND: bool>;
1071
1072 impl<const CAN_REWIND: bool> LogStoreFactory for TestLogStoreFactory<CAN_REWIND> {
1073 type Reader = RewindRequiredLogReader;
1074 type Writer = BoundedInMemLogStoreWriter;
1075
1076 const ALLOW_REWIND: bool = CAN_REWIND;
1077 const REBUILD_SINK_ON_UPDATE_VNODE_BITMAP: bool = false;
1078
1079 async fn build(self) -> (Self::Reader, Self::Writer) {
1080 unreachable!()
1081 }
1082 }
1083
1084 fn sink_param_for_config_update_test() -> SinkParam {
1085 SinkParam {
1086 sink_id: 0.into(),
1087 sink_name: "test".into(),
1088 properties: BTreeMap::from([
1089 ("connector".to_owned(), "blackhole".to_owned()),
1090 ("commit_checkpoint_interval".to_owned(), "1".to_owned()),
1091 ]),
1092 columns: vec![],
1093 downstream_pk: None,
1094 sink_type: SinkType::AppendOnly,
1095 ignore_delete: false,
1096 format_desc: None,
1097 db_name: "test".into(),
1098 sink_from_name: "test".into(),
1099 }
1100 }
1101
1102 #[test]
1103 fn test_sink_config_has_changes() {
1104 let current = BTreeMap::from([
1105 ("connector".to_owned(), "blackhole".to_owned()),
1106 ("commit_checkpoint_interval".to_owned(), "1".to_owned()),
1107 ]);
1108
1109 assert!(!sink_config_has_changes(
1110 ¤t,
1111 &HashMap::from([("commit_checkpoint_interval".to_owned(), "1".to_owned())])
1112 ));
1113 assert!(sink_config_has_changes(
1114 ¤t,
1115 &HashMap::from([("commit_checkpoint_interval".to_owned(), "2".to_owned())])
1116 ));
1117 assert!(sink_config_has_changes(
1118 ¤t,
1119 &HashMap::from([("force_append_only".to_owned(), "true".to_owned())])
1120 ));
1121 }
1122
1123 fn no_op_config_update() -> RebuildSinkMessage {
1124 RebuildSinkMessage::UpdateConfig(HashMap::from([(
1125 "commit_checkpoint_interval".to_owned(),
1126 "1".to_owned(),
1127 )]))
1128 }
1129
1130 fn changed_config_update() -> RebuildSinkMessage {
1131 RebuildSinkMessage::UpdateConfig(HashMap::from([(
1132 "commit_checkpoint_interval".to_owned(),
1133 "2".to_owned(),
1134 )]))
1135 }
1136
1137 async fn assert_no_op_config_update_keeps_consuming<const CAN_REWIND: bool>() {
1138 let sink_param = sink_param_for_config_update_test();
1139 let sink = BlackHoleSink::try_from(sink_param.clone()).unwrap();
1140 let state = Arc::new(RewindRequiredLogReaderState::default());
1141 let log_reader = RewindRequiredLogReader {
1142 is_reset: false,
1143 state: state.clone(),
1144 };
1145 let (rate_limit_tx, rate_limit_rx) = unbounded_channel();
1146 let (rebuild_sink_tx, rebuild_sink_rx) = unbounded_channel();
1147
1148 let consume_log = SinkExecutor::<TestLogStoreFactory<CAN_REWIND>>::execute_consume_log(
1149 sink,
1150 log_reader,
1151 vec![],
1152 sink_param,
1153 SinkWriterParam::for_test(),
1154 None,
1155 ActorContext::for_test(0),
1156 rate_limit_rx,
1157 rebuild_sink_rx,
1158 );
1159 tokio::pin!(consume_log);
1160
1161 tokio::select! {
1162 result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1163 () = state.wait_for_start_count(1) => {},
1164 }
1165
1166 rebuild_sink_tx.send(no_op_config_update()).unwrap();
1167
1168 let (notify_tx, notify_rx) = oneshot::channel();
1171 rebuild_sink_tx
1172 .send(RebuildSinkMessage::RebuildSink(
1173 Arc::new(Bitmap::ones(1)),
1174 notify_tx,
1175 ))
1176 .unwrap();
1177 tokio::select! {
1178 result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1179 result = notify_rx => result.expect("rebuild sink should be acknowledged"),
1180 }
1181
1182 assert_eq!(state.rewind_count.load(Ordering::SeqCst), 0);
1183 drop(rate_limit_tx);
1184 }
1185
1186 #[tokio::test]
1187 async fn test_no_op_sink_config_update_keeps_consuming_with_rewind() {
1188 assert_no_op_config_update_keeps_consuming::<true>().await;
1189 }
1190
1191 #[tokio::test]
1192 async fn test_no_op_sink_config_update_keeps_consuming_without_rewind() {
1193 assert_no_op_config_update_keeps_consuming::<false>().await;
1194 }
1195
1196 #[tokio::test]
1197 async fn test_sink_config_update_rewinds_log_reader() {
1198 let sink_param = sink_param_for_config_update_test();
1199 let sink = BlackHoleSink::try_from(sink_param.clone()).unwrap();
1200 let state = Arc::new(RewindRequiredLogReaderState::default());
1201 let log_reader = RewindRequiredLogReader {
1202 is_reset: false,
1203 state: state.clone(),
1204 };
1205 let (rate_limit_tx, rate_limit_rx) = unbounded_channel();
1206 let (rebuild_sink_tx, rebuild_sink_rx) = unbounded_channel();
1207
1208 let consume_log = SinkExecutor::<TestLogStoreFactory<true>>::execute_consume_log(
1209 sink,
1210 log_reader,
1211 vec![],
1212 sink_param,
1213 SinkWriterParam::for_test(),
1214 None,
1215 ActorContext::for_test(0),
1216 rate_limit_rx,
1217 rebuild_sink_rx,
1218 );
1219 tokio::pin!(consume_log);
1220
1221 tokio::select! {
1222 result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1223 () = state.wait_for_start_count(1) => {},
1224 }
1225
1226 rebuild_sink_tx.send(changed_config_update()).unwrap();
1227
1228 tokio::select! {
1229 result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1230 () = state.wait_for_start_count(2) => {},
1231 }
1232
1233 assert_eq!(state.rewind_count.load(Ordering::SeqCst), 1);
1234 drop(rate_limit_tx);
1235 }
1236
1237 #[tokio::test]
1238 async fn test_sink_config_update_triggers_recovery_without_rewind() {
1239 let sink_param = sink_param_for_config_update_test();
1240 let sink = BlackHoleSink::try_from(sink_param.clone()).unwrap();
1241 let state = Arc::new(RewindRequiredLogReaderState::default());
1242 let log_reader = RewindRequiredLogReader {
1243 is_reset: false,
1244 state: state.clone(),
1245 };
1246 let (rate_limit_tx, rate_limit_rx) = unbounded_channel();
1247 let (rebuild_sink_tx, rebuild_sink_rx) = unbounded_channel();
1248
1249 let consume_log = SinkExecutor::<TestLogStoreFactory<false>>::execute_consume_log(
1250 sink,
1251 log_reader,
1252 vec![],
1253 sink_param,
1254 SinkWriterParam::for_test(),
1255 None,
1256 ActorContext::for_test(0),
1257 rate_limit_rx,
1258 rebuild_sink_rx,
1259 );
1260 tokio::pin!(consume_log);
1261
1262 tokio::select! {
1263 result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1264 () = state.wait_for_start_count(1) => {},
1265 }
1266
1267 rebuild_sink_tx.send(changed_config_update()).unwrap();
1268
1269 tokio::time::timeout(Duration::from_secs(5), consume_log)
1270 .await
1271 .expect("log consumer should trigger recovery")
1272 .expect_err("log consumer should return an error");
1273 assert_eq!(state.start_count.load(Ordering::SeqCst), 1);
1274 assert_eq!(state.rewind_count.load(Ordering::SeqCst), 0);
1275 drop(rate_limit_tx);
1276 }
1277
1278 #[test]
1279 fn test_sink_retry_backoff_is_bounded() {
1280 let mut retry_backoff = sink_retry_backoff();
1281 for max_delay in [1, 2, 4, 8, 16, 30, 30].map(Duration::from_secs) {
1282 assert!(retry_backoff.next().expect("retry strategy is infinite") <= max_delay);
1283 }
1284 }
1285
1286 #[tokio::test]
1287 async fn test_force_append_only_sink() {
1288 use risingwave_common::array::StreamChunkTestExt;
1289 use risingwave_common::array::stream_chunk::StreamChunk;
1290 use risingwave_common::types::DataType;
1291
1292 use crate::executor::Barrier;
1293
1294 let properties = maplit::btreemap! {
1295 "connector".into() => "blackhole".into(),
1296 "type".into() => "append-only".into(),
1297 "force_append_only".into() => "true".into()
1298 };
1299
1300 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 ColumnCatalog {
1312 column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1313 is_hidden: true,
1314 },
1315 ];
1316 let schema: Schema = columns
1317 .iter()
1318 .map(|column| Field::from(column.column_desc.clone()))
1319 .collect();
1320 let stream_key = vec![0];
1321
1322 let source = MockSource::with_messages(vec![
1323 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1324 Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1325 " I I I
1326 + 3 2 1",
1327 ))),
1328 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1329 Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1330 " I I I
1331 U- 3 2 1
1332 U+ 3 4 1
1333 + 5 6 7",
1334 ))),
1335 Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1336 " I I I
1337 - 5 6 7",
1338 ))),
1339 ])
1340 .into_executor(schema.clone(), stream_key.clone());
1341
1342 let sink_param = SinkParam {
1343 sink_id: 0.into(),
1344 sink_name: "test".into(),
1345 properties,
1346
1347 columns: columns
1348 .iter()
1349 .filter(|col| !col.is_hidden)
1350 .map(|col| col.column_desc.clone())
1351 .collect(),
1352 downstream_pk: Some(stream_key.clone()),
1353 sink_type: SinkType::AppendOnly,
1354 ignore_delete: true,
1355 format_desc: None,
1356 db_name: "test".into(),
1357 sink_from_name: "test".into(),
1358 };
1359
1360 let info = ExecutorInfo::for_test(schema, stream_key, "SinkExecutor".to_owned(), 0);
1361
1362 let sink = build_sink(sink_param.clone()).unwrap();
1363
1364 let sink_executor = SinkExecutor::new(
1365 ActorContext::for_test(0),
1366 info,
1367 source,
1368 SinkWriterParam::for_test(),
1369 sink,
1370 sink_param,
1371 columns.clone(),
1372 BoundedInMemLogStoreFactory::for_test(1),
1373 1024,
1374 vec![DataType::Int32, DataType::Int32, DataType::Int32],
1375 None,
1376 )
1377 .unwrap();
1378
1379 let mut executor = sink_executor.boxed().execute();
1380
1381 executor.next().await.unwrap().unwrap();
1383
1384 let chunk_msg = executor.next().await.unwrap().unwrap();
1385 assert_eq!(
1386 chunk_msg.into_chunk().unwrap().compact_vis(),
1387 StreamChunk::from_pretty(
1388 " I I I
1389 + 3 2 1",
1390 )
1391 );
1392
1393 executor.next().await.unwrap().unwrap();
1395
1396 let chunk_msg = executor.next().await.unwrap().unwrap();
1397 assert_eq!(
1398 chunk_msg.into_chunk().unwrap().compact_vis(),
1399 StreamChunk::from_pretty(
1400 " I I I
1401 + 3 4 1
1402 + 5 6 7",
1403 )
1404 );
1405
1406 executor.next().await.unwrap().unwrap();
1411 }
1412
1413 #[tokio::test]
1414 async fn stream_key_sink_pk_mismatch_upsert() {
1415 stream_key_sink_pk_mismatch(SinkType::Upsert).await;
1416 }
1417
1418 #[tokio::test]
1419 async fn stream_key_sink_pk_mismatch_retract() {
1420 stream_key_sink_pk_mismatch(SinkType::Retract).await;
1421 }
1422
1423 async fn stream_key_sink_pk_mismatch(sink_type: SinkType) {
1424 use risingwave_common::array::StreamChunkTestExt;
1425 use risingwave_common::array::stream_chunk::StreamChunk;
1426 use risingwave_common::types::DataType;
1427
1428 use crate::executor::Barrier;
1429
1430 let properties = maplit::btreemap! {
1431 "connector".into() => "blackhole".into(),
1432 };
1433
1434 let columns = vec![
1437 ColumnCatalog {
1438 column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1439 is_hidden: false,
1440 },
1441 ColumnCatalog {
1442 column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1443 is_hidden: false,
1444 },
1445 ColumnCatalog {
1446 column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1447 is_hidden: true,
1448 },
1449 ];
1450 let schema: Schema = columns
1451 .iter()
1452 .map(|column| Field::from(column.column_desc.clone()))
1453 .collect();
1454
1455 let source = MockSource::with_messages(vec![
1456 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1457 Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1458 " I I I
1459 + 1 1 10",
1460 ))),
1461 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1462 Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1463 " I I I
1464 + 1 3 30",
1465 ))),
1466 Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1467 " I I I
1468 + 1 2 20
1469 - 1 2 20",
1470 ))),
1471 Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1472 " I I I
1473 - 1 1 10
1474 + 1 1 40",
1475 ))),
1476 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1477 ])
1478 .into_executor(schema.clone(), vec![0, 1]);
1479
1480 let sink_param = SinkParam {
1481 sink_id: 0.into(),
1482 sink_name: "test".into(),
1483 properties,
1484
1485 columns: columns
1486 .iter()
1487 .filter(|col| !col.is_hidden)
1488 .map(|col| col.column_desc.clone())
1489 .collect(),
1490 downstream_pk: Some(vec![0]),
1491 sink_type,
1492 ignore_delete: false,
1493 format_desc: None,
1494 db_name: "test".into(),
1495 sink_from_name: "test".into(),
1496 };
1497
1498 let info = ExecutorInfo::for_test(schema, vec![0, 1], "SinkExecutor".to_owned(), 0);
1499
1500 let sink = build_sink(sink_param.clone()).unwrap();
1501
1502 let sink_executor = SinkExecutor::new(
1503 ActorContext::for_test(0),
1504 info,
1505 source,
1506 SinkWriterParam::for_test(),
1507 sink,
1508 sink_param,
1509 columns.clone(),
1510 BoundedInMemLogStoreFactory::for_test(1),
1511 1024,
1512 vec![DataType::Int64, DataType::Int64, DataType::Int64],
1513 None,
1514 )
1515 .unwrap();
1516
1517 let mut executor = sink_executor.boxed().execute();
1518
1519 executor.next().await.unwrap().unwrap();
1521
1522 let chunk_msg = executor.next().await.unwrap().unwrap();
1523 assert_eq!(
1524 chunk_msg.into_chunk().unwrap().compact_vis(),
1525 StreamChunk::from_pretty(
1526 " I I I
1527 + 1 1 10",
1528 )
1529 );
1530
1531 executor.next().await.unwrap().unwrap();
1533
1534 let chunk_msg = executor.next().await.unwrap().unwrap();
1535 let expected = match sink_type {
1536 SinkType::Retract => StreamChunk::from_pretty(
1537 " I I I
1538 U- 1 1 10
1539 U+ 1 1 40",
1540 ),
1541 SinkType::Upsert => StreamChunk::from_pretty(
1542 " I I I
1543 + 1 1 40", ),
1545 _ => unreachable!(),
1546 };
1547 assert_eq!(chunk_msg.into_chunk().unwrap().compact_vis(), expected);
1548
1549 executor.next().await.unwrap().unwrap();
1551 }
1552
1553 #[tokio::test]
1554 async fn test_sink_into_table_preserves_special_conflict_rows_for_mismatched_pk() {
1555 use risingwave_common::array::StreamChunkTestExt;
1556 use risingwave_common::array::stream_chunk::StreamChunk;
1557 use risingwave_common::types::DataType;
1558
1559 use crate::executor::Barrier;
1560
1561 let properties = maplit::btreemap! {
1562 "connector".into() => "table".into(),
1563 SINK_USER_PRESERVE_ROW_LEVEL_CHANGES.into() => "true".into(),
1564 };
1565
1566 let columns = vec![
1567 ColumnCatalog {
1568 column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1569 is_hidden: false,
1570 },
1571 ColumnCatalog {
1572 column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1573 is_hidden: false,
1574 },
1575 ColumnCatalog {
1576 column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1577 is_hidden: false,
1578 },
1579 ];
1580 let schema: Schema = columns
1581 .iter()
1582 .map(|column| Field::from(column.column_desc.clone()))
1583 .collect();
1584
1585 let source = MockSource::with_messages(vec![
1586 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1587 Message::Chunk(StreamChunk::from_pretty(
1588 " I I I
1589 + 1 10 1",
1590 )),
1591 Message::Chunk(StreamChunk::from_pretty(
1592 " I I I
1593 + 1 20 2",
1594 )),
1595 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1596 Message::Chunk(StreamChunk::from_pretty(
1598 " I I I
1599 - 1 10 1
1600 - 1 20 2
1601 + 1 30 3
1602 - 2 50 5",
1603 )),
1604 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1605 ])
1606 .into_executor(schema.clone(), vec![0, 2]);
1607
1608 let sink_param = SinkParam {
1609 sink_id: 0.into(),
1610 sink_name: "test".into(),
1611 properties,
1612 columns: columns.iter().map(|col| col.column_desc.clone()).collect(),
1613 downstream_pk: Some(vec![0]),
1614 sink_type: SinkType::Upsert,
1615 ignore_delete: false,
1616 format_desc: None,
1617 db_name: "test".into(),
1618 sink_from_name: "test".into(),
1619 };
1620
1621 let info = ExecutorInfo::for_test(schema, vec![0, 2], "SinkExecutor".to_owned(), 0);
1622 let sink = build_sink(sink_param.clone()).unwrap();
1623
1624 let sink_executor = SinkExecutor::new(
1625 ActorContext::for_test(0),
1626 info,
1627 source,
1628 SinkWriterParam::for_test(),
1629 sink,
1630 sink_param,
1631 columns,
1632 BoundedInMemLogStoreFactory::for_test(1),
1633 1024,
1634 vec![DataType::Int64, DataType::Int64, DataType::Int64],
1635 None,
1636 )
1637 .unwrap();
1638
1639 let mut executor = sink_executor.boxed().execute();
1640
1641 executor.next().await.unwrap().unwrap();
1642
1643 let chunk_msg = executor.next().await.unwrap().unwrap();
1644 assert_eq!(
1645 chunk_msg.into_chunk().unwrap().compact_vis(),
1646 StreamChunk::from_pretty(
1647 " I I I
1648 + 1 10 1
1649 + 1 20 2",
1650 )
1651 );
1652
1653 executor.next().await.unwrap().unwrap();
1654
1655 let chunk_msg = executor.next().await.unwrap().unwrap();
1656 assert_eq!(
1657 chunk_msg.into_chunk().unwrap().compact_vis(),
1658 StreamChunk::from_pretty(
1659 " I I I
1660 - 2 50 5
1661 + 1 30 3",
1662 )
1663 );
1664
1665 executor.next().await.unwrap().unwrap();
1666 }
1667
1668 #[tokio::test]
1669 async fn test_sink_into_table_keeps_default_compaction_without_special_conflict_semantics() {
1670 use risingwave_common::array::StreamChunkTestExt;
1671 use risingwave_common::array::stream_chunk::StreamChunk;
1672 use risingwave_common::types::DataType;
1673
1674 use crate::executor::Barrier;
1675
1676 let properties = maplit::btreemap! {
1677 "connector".into() => "table".into(),
1678 };
1679
1680 let columns = vec![
1681 ColumnCatalog {
1682 column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1683 is_hidden: false,
1684 },
1685 ColumnCatalog {
1686 column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1687 is_hidden: false,
1688 },
1689 ];
1690 let schema: Schema = columns
1691 .iter()
1692 .map(|column| Field::from(column.column_desc.clone()))
1693 .collect();
1694
1695 let source = MockSource::with_messages(vec![
1696 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1697 Message::Chunk(StreamChunk::from_pretty(
1698 " I I
1699 + 1 10
1700 + 1 20",
1701 )),
1702 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1703 ])
1704 .into_executor(schema.clone(), vec![0]);
1705
1706 let sink_param = SinkParam {
1707 sink_id: 0.into(),
1708 sink_name: "test".into(),
1709 properties,
1710 columns: columns.iter().map(|col| col.column_desc.clone()).collect(),
1711 downstream_pk: Some(vec![0]),
1712 sink_type: SinkType::Upsert,
1713 ignore_delete: false,
1714 format_desc: None,
1715 db_name: "test".into(),
1716 sink_from_name: "test".into(),
1717 };
1718
1719 let info = ExecutorInfo::for_test(schema, vec![0], "SinkExecutor".to_owned(), 0);
1720 let sink = build_sink(sink_param.clone()).unwrap();
1721
1722 let sink_executor = SinkExecutor::new(
1723 ActorContext::for_test(0),
1724 info,
1725 source,
1726 SinkWriterParam::for_test(),
1727 sink,
1728 sink_param,
1729 columns,
1730 BoundedInMemLogStoreFactory::for_test(1),
1731 1024,
1732 vec![DataType::Int64, DataType::Int64],
1733 None,
1734 )
1735 .unwrap();
1736
1737 let mut executor = sink_executor.boxed().execute();
1738
1739 executor.next().await.unwrap().unwrap();
1740
1741 let chunk_msg = executor.next().await.unwrap().unwrap();
1742 assert_eq!(
1743 chunk_msg.into_chunk().unwrap().compact_vis(),
1744 StreamChunk::from_pretty(
1745 " I I
1746 + 1 20",
1747 )
1748 );
1749
1750 executor.next().await.unwrap().unwrap();
1751 }
1752
1753 #[tokio::test]
1754 async fn test_empty_barrier_sink() {
1755 use risingwave_common::types::DataType;
1756
1757 use crate::executor::Barrier;
1758
1759 let properties = maplit::btreemap! {
1760 "connector".into() => "blackhole".into(),
1761 "type".into() => "append-only".into(),
1762 "force_append_only".into() => "true".into()
1763 };
1764 let columns = vec![
1765 ColumnCatalog {
1766 column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1767 is_hidden: false,
1768 },
1769 ColumnCatalog {
1770 column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1771 is_hidden: false,
1772 },
1773 ];
1774 let schema: Schema = columns
1775 .iter()
1776 .map(|column| Field::from(column.column_desc.clone()))
1777 .collect();
1778 let stream_key = vec![0];
1779
1780 let source = MockSource::with_messages(vec![
1781 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1782 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1783 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1784 ])
1785 .into_executor(schema.clone(), stream_key.clone());
1786
1787 let sink_param = SinkParam {
1788 sink_id: 0.into(),
1789 sink_name: "test".into(),
1790 properties,
1791
1792 columns: columns
1793 .iter()
1794 .filter(|col| !col.is_hidden)
1795 .map(|col| col.column_desc.clone())
1796 .collect(),
1797 downstream_pk: Some(stream_key.clone()),
1798 sink_type: SinkType::AppendOnly,
1799 ignore_delete: true,
1800 format_desc: None,
1801 db_name: "test".into(),
1802 sink_from_name: "test".into(),
1803 };
1804
1805 let info = ExecutorInfo::for_test(schema, stream_key, "SinkExecutor".to_owned(), 0);
1806
1807 let sink = build_sink(sink_param.clone()).unwrap();
1808
1809 let sink_executor = SinkExecutor::new(
1810 ActorContext::for_test(0),
1811 info,
1812 source,
1813 SinkWriterParam::for_test(),
1814 sink,
1815 sink_param,
1816 columns,
1817 BoundedInMemLogStoreFactory::for_test(1),
1818 1024,
1819 vec![DataType::Int64, DataType::Int64],
1820 None,
1821 )
1822 .unwrap();
1823
1824 let mut executor = sink_executor.boxed().execute();
1825
1826 assert_eq!(
1828 executor.next().await.unwrap().unwrap(),
1829 Message::Barrier(Barrier::new_test_barrier(test_epoch(1)))
1830 );
1831
1832 assert_eq!(
1834 executor.next().await.unwrap().unwrap(),
1835 Message::Barrier(Barrier::new_test_barrier(test_epoch(2)))
1836 );
1837
1838 assert_eq!(
1840 executor.next().await.unwrap().unwrap(),
1841 Message::Barrier(Barrier::new_test_barrier(test_epoch(3)))
1842 );
1843 }
1844
1845 #[tokio::test]
1846 async fn test_force_compaction() {
1847 use risingwave_common::array::StreamChunkTestExt;
1848 use risingwave_common::array::stream_chunk::StreamChunk;
1849 use risingwave_common::types::DataType;
1850
1851 use crate::executor::Barrier;
1852
1853 let properties = maplit::btreemap! {
1854 "connector".into() => "blackhole".into(),
1855 "force_compaction".into() => "true".into()
1856 };
1857
1858 let columns = vec![
1861 ColumnCatalog {
1862 column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1863 is_hidden: false,
1864 },
1865 ColumnCatalog {
1866 column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1867 is_hidden: false,
1868 },
1869 ColumnCatalog {
1870 column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1871 is_hidden: true,
1872 },
1873 ];
1874 let schema: Schema = columns
1875 .iter()
1876 .map(|column| Field::from(column.column_desc.clone()))
1877 .collect();
1878
1879 let source = MockSource::with_messages(vec![
1880 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1881 Message::Chunk(StreamChunk::from_pretty(
1882 " I I I
1883 + 1 1 10",
1884 )),
1885 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1886 Message::Chunk(StreamChunk::from_pretty(
1887 " I I I
1888 + 1 3 30",
1889 )),
1890 Message::Chunk(StreamChunk::from_pretty(
1891 " I I I
1892 + 1 2 20
1893 - 1 2 20
1894 + 1 4 10",
1895 )),
1896 Message::Chunk(StreamChunk::from_pretty(
1897 " I I I
1898 - 1 1 10
1899 + 1 1 40",
1900 )),
1901 Message::Chunk(StreamChunk::from_pretty(
1902 " I I I
1903 - 1 4 30",
1904 )),
1905 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1906 ])
1907 .into_executor(schema.clone(), vec![0, 1]);
1908
1909 let sink_param = SinkParam {
1910 sink_id: 0.into(),
1911 sink_name: "test".into(),
1912 properties,
1913
1914 columns: columns
1915 .iter()
1916 .filter(|col| !col.is_hidden)
1917 .map(|col| col.column_desc.clone())
1918 .collect(),
1919 downstream_pk: Some(vec![0, 1]),
1920 sink_type: SinkType::Upsert,
1921 ignore_delete: false,
1922 format_desc: None,
1923 db_name: "test".into(),
1924 sink_from_name: "test".into(),
1925 };
1926
1927 let info = ExecutorInfo::for_test(schema, vec![0, 1], "SinkExecutor".to_owned(), 0);
1928
1929 let sink = build_sink(sink_param.clone()).unwrap();
1930
1931 let sink_executor = SinkExecutor::new(
1932 ActorContext::for_test(0),
1933 info,
1934 source,
1935 SinkWriterParam::for_test(),
1936 sink,
1937 sink_param,
1938 columns.clone(),
1939 BoundedInMemLogStoreFactory::for_test(1),
1940 1024,
1941 vec![DataType::Int64, DataType::Int64, DataType::Int64],
1942 None,
1943 )
1944 .unwrap();
1945
1946 let mut executor = sink_executor.boxed().execute();
1947
1948 executor.next().await.unwrap().unwrap();
1950
1951 let chunk_msg = executor.next().await.unwrap().unwrap();
1952 assert_eq!(
1953 chunk_msg.into_chunk().unwrap().compact_vis(),
1954 StreamChunk::from_pretty(
1955 " I I I
1956 + 1 1 10",
1957 )
1958 );
1959
1960 executor.next().await.unwrap().unwrap();
1962
1963 let chunk_msg = executor.next().await.unwrap().unwrap();
1964 assert_eq!(
1965 chunk_msg.into_chunk().unwrap().compact_vis(),
1966 StreamChunk::from_pretty(
1967 " I I I
1968 + 1 1 40
1969 + 1 3 30",
1970 )
1971 );
1972
1973 executor.next().await.unwrap().unwrap();
1975 }
1976
1977 async fn run_sink_executor(
1979 properties: BTreeMap<String, String>,
1980 sink_type: SinkType,
1981 input_kind: StreamKind,
1982 stream_key: Vec<usize>,
1983 downstream_pk: Vec<usize>,
1984 input: Vec<Message>,
1985 ) -> Vec<StreamChunk> {
1986 let num_columns = input
1987 .iter()
1988 .find_map(|msg| msg.as_chunk())
1989 .unwrap()
1990 .columns()
1991 .len();
1992 let columns = (0..num_columns)
1993 .map(|i| ColumnCatalog {
1994 column_desc: ColumnDesc::unnamed(ColumnId::new(i as i32), DataType::Int64),
1995 is_hidden: false,
1996 })
1997 .collect_vec();
1998 let schema: Schema = columns
1999 .iter()
2000 .map(|column| Field::from(column.column_desc.clone()))
2001 .collect();
2002 let num_barriers = input.iter().filter(|msg| msg.is_barrier()).count();
2003
2004 let source = Executor::new(
2005 ExecutorInfo {
2006 stream_kind: input_kind,
2007 ..ExecutorInfo::for_test(
2008 schema.clone(),
2009 stream_key.clone(),
2010 "MockSource".to_owned(),
2011 0,
2012 )
2013 },
2014 MockSource::with_messages(input).boxed(),
2015 );
2016 let sink_param = SinkParam {
2017 sink_id: 0.into(),
2018 sink_name: "test".into(),
2019 properties,
2020 columns: columns.iter().map(|col| col.column_desc.clone()).collect(),
2021 downstream_pk: Some(downstream_pk),
2022 sink_type,
2023 ignore_delete: false,
2024 format_desc: None,
2025 db_name: "test".into(),
2026 sink_from_name: "test".into(),
2027 };
2028 let info = ExecutorInfo::for_test(schema, stream_key, "SinkExecutor".to_owned(), 0);
2029 let sink = build_sink(sink_param.clone()).unwrap();
2030 let mut executor = SinkExecutor::new(
2031 ActorContext::for_test(0),
2032 info,
2033 source,
2034 SinkWriterParam::for_test(),
2035 sink,
2036 sink_param,
2037 columns,
2038 BoundedInMemLogStoreFactory::for_test(1),
2039 1024,
2040 vec![DataType::Int64; num_columns],
2041 None,
2042 )
2043 .unwrap()
2044 .boxed()
2045 .execute();
2046
2047 let mut output = vec![];
2048 for _ in 0..num_barriers {
2049 loop {
2050 match executor.next().await.unwrap().unwrap() {
2051 Message::Chunk(chunk) => output.push(chunk.compact_vis()),
2052 Message::Barrier(_) => break,
2053 Message::Watermark(_) => unreachable!(),
2054 }
2055 }
2056 }
2057 output
2058 }
2059
2060 fn sink_pk_change_input() -> Vec<Message> {
2063 use risingwave_common::array::StreamChunkTestExt;
2064
2065 vec![
2066 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
2067 Message::Chunk(StreamChunk::from_pretty(
2068 " I I I I
2069 + 1 1 6 10
2070 + 2 2 6 10",
2071 )),
2072 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
2073 Message::Chunk(StreamChunk::from_pretty(
2075 " I I I I
2076 U- 1 1 6 10
2077 U+ 1 1 8 10
2078 U- 2 2 6 10
2079 U+ 2 2 6 20",
2080 )),
2081 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
2082 ]
2083 }
2084
2085 #[tokio::test]
2086 async fn test_sink_pk_change_upsert() {
2087 test_sink_pk_change(SinkType::Upsert).await;
2088 }
2089
2090 #[tokio::test]
2091 async fn test_sink_pk_change_retract() {
2092 test_sink_pk_change(SinkType::Retract).await;
2093 }
2094
2095 async fn test_sink_pk_change(sink_type: SinkType) {
2096 use risingwave_common::array::StreamChunkTestExt;
2097
2098 let output = run_sink_executor(
2099 maplit::btreemap! { "connector".into() => "blackhole".into() },
2100 sink_type,
2101 StreamKind::Retract,
2102 vec![0, 1],
2103 vec![0, 2],
2104 sink_pk_change_input(),
2105 )
2106 .await;
2107
2108 let expected = match sink_type {
2109 SinkType::Upsert => StreamChunk::from_pretty(
2110 " I I I I
2111 - 1 1 6 10
2112 + 2 2 6 20
2113 + 1 1 8 10",
2114 ),
2115 SinkType::Retract => StreamChunk::from_pretty(
2116 " I I I I
2117 - 1 1 6 10
2118 U- 2 2 6 10
2119 U+ 2 2 6 20
2120 + 1 1 8 10",
2121 ),
2122 _ => unreachable!(),
2123 };
2124 assert_eq!(
2125 output,
2126 vec![
2127 StreamChunk::from_pretty(
2128 " I I I I
2129 + 1 1 6 10
2130 + 2 2 6 10",
2131 ),
2132 expected,
2133 ]
2134 );
2135 }
2136
2137 #[tokio::test]
2138 async fn test_sink_into_table_preserves_special_conflict_rows_for_pk_change() {
2139 use risingwave_common::array::StreamChunkTestExt;
2140
2141 let output = run_sink_executor(
2142 maplit::btreemap! {
2143 "connector".into() => "table".into(),
2144 SINK_USER_PRESERVE_ROW_LEVEL_CHANGES.into() => "true".into()
2145 },
2146 SinkType::Upsert,
2147 StreamKind::Retract,
2148 vec![0, 1],
2149 vec![0, 2],
2150 sink_pk_change_input(),
2151 )
2152 .await;
2153
2154 assert_eq!(
2156 output,
2157 vec![
2158 StreamChunk::from_pretty(
2159 " I I I I
2160 + 1 1 6 10
2161 + 2 2 6 10",
2162 ),
2163 StreamChunk::from_pretty(
2164 " I I I I
2165 - 1 1 6 10
2166 + 1 1 8 10
2167 + 2 2 6 20",
2168 ),
2169 ]
2170 );
2171 }
2172
2173 #[tokio::test]
2176 async fn test_force_compaction_sink_pk_change() {
2177 use risingwave_common::array::StreamChunkTestExt;
2178
2179 let input = vec![
2180 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
2181 Message::Chunk(StreamChunk::from_pretty(
2182 " I I I
2183 + 1 6 10",
2184 )),
2185 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
2186 Message::Chunk(StreamChunk::from_pretty(
2187 " I I I
2188 U- 1 6 10
2189 U+ 1 8 10",
2190 )),
2191 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
2192 ];
2193 let output = run_sink_executor(
2194 maplit::btreemap! {
2195 "connector".into() => "blackhole".into(),
2196 "force_compaction".into() => "true".into()
2197 },
2198 SinkType::Upsert,
2199 StreamKind::Retract,
2200 vec![0],
2201 vec![0, 1],
2202 input,
2203 )
2204 .await;
2205
2206 assert_eq!(
2207 output,
2208 vec![
2209 StreamChunk::from_pretty(
2210 " I I I
2211 + 1 6 10",
2212 ),
2213 StreamChunk::from_pretty(
2214 " I I I
2215 - 1 6 10
2216 + 1 8 10",
2217 ),
2218 ]
2219 );
2220 }
2221
2222 #[tokio::test]
2225 async fn test_upsert_input_sink_pk_change() {
2226 use risingwave_common::array::StreamChunkTestExt;
2227
2228 let input = vec![
2229 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
2230 Message::Chunk(StreamChunk::from_pretty(
2231 " I I I I
2232 + 1 1 6 10",
2233 )),
2234 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
2235 Message::Chunk(StreamChunk::from_pretty(
2236 " I I I I
2237 - 1 1 . .
2238 + 1 1 8 10",
2239 )),
2240 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
2241 ];
2242 let output = run_sink_executor(
2243 maplit::btreemap! { "connector".into() => "blackhole".into() },
2244 SinkType::Upsert,
2245 StreamKind::Upsert,
2246 vec![0, 1],
2247 vec![0, 2],
2248 input,
2249 )
2250 .await;
2251
2252 assert_eq!(
2253 output,
2254 vec![
2255 StreamChunk::from_pretty(
2256 " I I I I
2257 + 1 1 6 10",
2258 ),
2259 StreamChunk::from_pretty(
2260 " I I I I
2261 + 1 1 8 10",
2262 ),
2263 ]
2264 );
2265 }
2266}