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