1use std::collections::{BTreeMap, HashMap};
16use std::{assert_matches, mem};
17
18use anyhow::anyhow;
19use futures::stream::select;
20use futures::{FutureExt, TryFutureExt, TryStreamExt};
21use itertools::Itertools;
22use risingwave_common::array::Op;
23use risingwave_common::array::stream_chunk::StreamChunkMut;
24use risingwave_common::bitmap::Bitmap;
25use risingwave_common::catalog::{ColumnCatalog, Field};
26use risingwave_common::metrics::{GLOBAL_ERROR_METRICS, LabelGuardedIntGauge};
27use risingwave_common_estimate_size::EstimateSize;
28use risingwave_common_estimate_size::collections::EstimatedVec;
29use risingwave_common_rate_limit::RateLimit;
30use risingwave_connector::dispatch_sink;
31use risingwave_connector::sink::catalog::{SinkId, SinkType};
32use risingwave_connector::sink::log_store::{
33 FlushCurrentEpochOptions, LogReader, LogReaderExt, LogReaderMetrics, LogStoreFactory,
34 LogWriter, LogWriterExt, LogWriterMetrics,
35};
36use risingwave_connector::sink::{
37 GLOBAL_SINK_METRICS, LogSinker, SINK_USER_FORCE_COMPACTION,
38 SINK_USER_PRESERVE_ROW_LEVEL_CHANGES, Sink, SinkImpl, SinkParam, SinkWriterParam,
39};
40use risingwave_pb::common::ThrottleType;
41use risingwave_pb::id::FragmentId;
42use risingwave_pb::stream_plan::stream_node::StreamKind;
43use thiserror_ext::AsReport;
44use tokio::select;
45use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
46use tokio::sync::oneshot;
47
48use crate::common::change_buffer::{OutputKind, output_kind};
49use crate::common::compact_chunk::{
50 InconsistencyBehavior, StreamChunkCompactor, compact_chunk_inline,
51};
52use crate::executor::prelude::*;
53pub struct SinkExecutor<F: LogStoreFactory> {
54 actor_context: ActorContextRef,
55 info: ExecutorInfo,
56 input: Executor,
57 sink: SinkImpl,
58 input_columns: Vec<ColumnCatalog>,
59 sink_param: SinkParam,
60 log_store_factory: F,
61 sink_writer_param: SinkWriterParam,
62 chunk_size: usize,
63 input_data_types: Vec<DataType>,
64 non_append_only_behavior: Option<NonAppendOnlyBehavior>,
65 rate_limit: Option<u32>,
66}
67
68fn force_append_only(c: StreamChunk) -> StreamChunk {
70 let mut c: StreamChunkMut = c.into();
71 for (_, mut r) in c.to_rows_mut() {
72 match r.op() {
73 Op::Insert => {}
74 Op::Delete | Op::UpdateDelete => r.set_vis(false),
75 Op::UpdateInsert => r.set_op(Op::Insert),
76 }
77 }
78 c.into()
79}
80
81fn force_delete_only(c: StreamChunk) -> StreamChunk {
83 let mut c: StreamChunkMut = c.into();
84 for (_, mut r) in c.to_rows_mut() {
85 match r.op() {
86 Op::Delete => {}
87 Op::Insert | Op::UpdateInsert => r.set_vis(false),
88 Op::UpdateDelete => r.set_op(Op::Delete),
89 }
90 }
91 c.into()
92}
93
94#[derive(Clone, Copy, Debug)]
97struct NonAppendOnlyBehavior {
98 pk_specified_and_matched: bool,
103 force_compaction: bool,
105}
106
107impl NonAppendOnlyBehavior {
108 fn should_compact_in_log_reader(self) -> bool {
129 self.pk_specified_and_matched && !self.force_compaction
130 }
131
132 fn should_reorder_records(self) -> bool {
201 !self.pk_specified_and_matched || self.force_compaction
202 }
203}
204
205fn compact_output_kind(sink_type: SinkType) -> OutputKind {
207 match sink_type {
208 SinkType::Upsert => output_kind::UPSERT,
209 SinkType::Retract => output_kind::RETRACT,
210 SinkType::AppendOnly => output_kind::RETRACT,
212 }
213}
214
215macro_rules! dispatch_output_kind {
217 ($sink_type:expr, $KIND:ident, $body:tt) => {
218 #[allow(unused_braces)]
219 match compact_output_kind($sink_type) {
220 output_kind::UPSERT => {
221 const KIND: OutputKind = output_kind::UPSERT;
222 $body
223 }
224 output_kind::RETRACT => {
225 const KIND: OutputKind = output_kind::RETRACT;
226 $body
227 }
228 }
229 };
230}
231
232impl<F: LogStoreFactory> SinkExecutor<F> {
233 #[expect(clippy::too_many_arguments)]
234 pub fn new(
235 actor_context: ActorContextRef,
236 info: ExecutorInfo,
237 input: Executor,
238 sink_writer_param: SinkWriterParam,
239 sink: SinkImpl,
240 sink_param: SinkParam,
241 columns: Vec<ColumnCatalog>,
242 log_store_factory: F,
243 chunk_size: usize,
244 input_data_types: Vec<DataType>,
245 rate_limit: Option<u32>,
246 ) -> StreamExecutorResult<Self> {
247 let sink_input_schema: Schema = columns
248 .iter()
249 .map(|column| Field::from(&column.column_desc))
250 .collect();
251
252 if let Some(col_dix) = sink_writer_param.extra_partition_col_idx {
253 assert_eq!(sink_input_schema.data_types(), {
255 let mut data_type = info.schema.data_types();
256 data_type.remove(col_dix);
257 data_type
258 });
259 } else {
260 assert_eq!(sink_input_schema.data_types(), info.schema.data_types());
261 }
262
263 let non_append_only_behavior = if !sink_param.sink_type.is_append_only() {
264 let stream_key = &info.stream_key;
265 let pk_specified_and_matched = (sink_param.downstream_pk.as_ref())
266 .is_some_and(|downstream_pk| stream_key.iter().all(|i| downstream_pk.contains(i)));
267 let force_compaction = sink_param
268 .properties
269 .get(SINK_USER_FORCE_COMPACTION)
270 .map(|v| v.eq_ignore_ascii_case("true"))
271 .unwrap_or(false);
272 Some(NonAppendOnlyBehavior {
273 pk_specified_and_matched,
274 force_compaction,
275 })
276 } else {
277 None
278 };
279
280 tracing::info!(
281 sink_id = %sink_param.sink_id,
282 actor_id = %actor_context.id,
283 ?non_append_only_behavior,
284 "Sink executor info"
285 );
286
287 Ok(Self {
288 actor_context,
289 info,
290 input,
291 sink,
292 input_columns: columns,
293 sink_param,
294 log_store_factory,
295 sink_writer_param,
296 chunk_size,
297 input_data_types,
298 non_append_only_behavior,
299 rate_limit,
300 })
301 }
302
303 fn execute_inner(self) -> BoxedMessageStream {
304 let sink_id = self.sink_param.sink_id;
305 let actor_id = self.actor_context.id;
306 let fragment_id = self.actor_context.fragment_id;
307
308 let stream_key = self.info.stream_key.clone();
309 let metrics = self.actor_context.streaming_metrics.new_sink_exec_metrics(
310 sink_id,
311 actor_id,
312 fragment_id,
313 );
314
315 let input_compact_ib = if self.input.stream_kind() == StreamKind::Upsert {
318 InconsistencyBehavior::Tolerate
319 } else {
320 InconsistencyBehavior::Panic
321 };
322
323 let input = self.input.execute();
324
325 let input = input.inspect_ok(move |msg| {
326 if let Message::Chunk(c) = msg {
327 metrics.sink_input_row_count.inc_by(c.capacity() as u64);
328 metrics.sink_input_bytes.inc_by(c.estimated_size() as u64);
329 }
330 });
331
332 let processed_input = Self::process_msg(
333 input,
334 self.sink_param.sink_type,
335 stream_key,
336 self.chunk_size,
337 self.input_data_types,
338 input_compact_ib,
339 self.sink_param.downstream_pk.clone(),
340 self.non_append_only_behavior,
341 metrics.sink_chunk_buffer_size,
342 self.sink_param
343 .properties
344 .get(SINK_USER_PRESERVE_ROW_LEVEL_CHANGES)
345 .is_some_and(|v| v.eq_ignore_ascii_case("true")),
346 self.sink.is_blackhole(), );
348
349 let processed_input = if self.sink_param.ignore_delete {
350 processed_input
352 .map_ok(|msg| match msg {
353 Message::Chunk(chunk) => Message::Chunk(force_append_only(chunk)),
354 other => other,
355 })
356 .left_stream()
357 } else {
358 processed_input.right_stream()
359 };
360
361 if self.sink.is_sink_into_table() {
362 processed_input.boxed()
364 } else {
365 let labels = [
366 &actor_id.to_string(),
367 &sink_id.to_string(),
368 self.sink_param.sink_name.as_str(),
369 ];
370 let log_store_first_write_epoch = GLOBAL_SINK_METRICS
371 .log_store_first_write_epoch
372 .with_guarded_label_values(&labels);
373 let log_store_latest_write_epoch = GLOBAL_SINK_METRICS
374 .log_store_latest_write_epoch
375 .with_guarded_label_values(&labels);
376 let log_store_write_rows = GLOBAL_SINK_METRICS
377 .log_store_write_rows
378 .with_guarded_label_values(&labels);
379 let log_writer_metrics = LogWriterMetrics {
380 log_store_first_write_epoch,
381 log_store_latest_write_epoch,
382 log_store_write_rows,
383 };
384
385 let (rate_limit_tx, rate_limit_rx) = unbounded_channel();
386 rate_limit_tx.send(self.rate_limit.into()).unwrap();
388
389 let (rebuild_sink_tx, rebuild_sink_rx) = unbounded_channel();
390
391 self.log_store_factory
392 .build()
393 .map(move |(log_reader, log_writer)| {
394 let write_log_stream = Self::execute_write_log(
395 processed_input,
396 log_writer.monitored(log_writer_metrics),
397 actor_id,
398 fragment_id,
399 sink_id,
400 rate_limit_tx,
401 rebuild_sink_tx,
402 );
403
404 let consume_log_stream_future = dispatch_sink!(self.sink, sink, {
405 let consume_log_stream = Self::execute_consume_log(
406 *sink,
407 log_reader,
408 self.input_columns,
409 self.sink_param,
410 self.sink_writer_param,
411 self.non_append_only_behavior,
412 self.actor_context,
413 rate_limit_rx,
414 rebuild_sink_rx,
415 )
416 .instrument_await(
417 await_tree::span!("consume_log (sink_id {sink_id})").long_running(),
418 )
419 .map_ok(|never| never); consume_log_stream.boxed()
422 });
423 select(consume_log_stream_future.into_stream(), write_log_stream)
424 })
425 .into_stream()
426 .flatten()
427 .boxed()
428 }
429 }
430
431 #[try_stream(ok = Message, error = StreamExecutorError)]
432 async fn execute_write_log<W: LogWriter>(
433 input: impl MessageStream,
434 mut log_writer: W,
435 actor_id: ActorId,
436 fragment_id: FragmentId,
437 sink_id: SinkId,
438 rate_limit_tx: UnboundedSender<RateLimit>,
439 rebuild_sink_tx: UnboundedSender<RebuildSinkMessage>,
440 ) {
441 pin_mut!(input);
442 let barrier = expect_first_barrier(&mut input).await?;
443 let epoch_pair = barrier.epoch;
444 let is_pause_on_startup = barrier.is_pause_on_startup();
445 yield Message::Barrier(barrier);
447
448 log_writer.init(epoch_pair, is_pause_on_startup).await?;
449
450 let mut is_paused = false;
451
452 #[for_await]
453 for msg in input {
454 match msg? {
455 Message::Watermark(w) => yield Message::Watermark(w),
456 Message::Chunk(chunk) => {
457 assert!(
458 !is_paused,
459 "Actor {actor_id} should not receive any data after pause"
460 );
461 log_writer.write_chunk(chunk.clone()).await?;
462 yield Message::Chunk(chunk);
463 }
464 Message::Barrier(barrier) => {
465 let update_vnode_bitmap = barrier.as_update_vnode_bitmap(actor_id);
466 let schema_change = barrier.as_sink_schema_change(sink_id);
467 let wait_log_store_flush = barrier.should_flush_sink_log_store(sink_id);
468 if let Some(schema_change) = &schema_change {
469 info!(?schema_change, %sink_id, "sink receive schema change");
470 }
471 if wait_log_store_flush {
472 info!(%sink_id, "sink wait for log store flush");
473 }
474 let post_flush = log_writer
475 .flush_current_epoch(
476 barrier.epoch.curr,
477 FlushCurrentEpochOptions {
478 is_checkpoint: barrier.kind.is_checkpoint(),
479 new_vnode_bitmap: update_vnode_bitmap.clone(),
480 is_stop: barrier.is_stop(actor_id),
481 schema_change,
482 wait_log_store_flush,
483 },
484 )
485 .await?;
486
487 let mutation = barrier.mutation.clone();
488 yield Message::Barrier(barrier);
489 if F::REBUILD_SINK_ON_UPDATE_VNODE_BITMAP
490 && let Some(new_vnode_bitmap) = update_vnode_bitmap.clone()
491 {
492 let (tx, rx) = oneshot::channel();
493 rebuild_sink_tx
494 .send(RebuildSinkMessage::RebuildSink(new_vnode_bitmap, tx))
495 .map_err(|_| anyhow!("fail to send rebuild sink to reader"))?;
496 rx.await
497 .map_err(|_| anyhow!("fail to wait rebuild sink finish"))?;
498 }
499 post_flush.post_yield_barrier().await?;
500
501 if let Some(mutation) = mutation.as_deref() {
502 match mutation {
503 Mutation::Pause => {
504 log_writer.pause()?;
505 is_paused = true;
506 }
507 Mutation::Resume => {
508 log_writer.resume()?;
509 is_paused = false;
510 }
511 Mutation::Throttle(fragment_to_apply) => {
512 if let Some(entry) = fragment_to_apply.get(&fragment_id)
513 && entry.throttle_type() == ThrottleType::Sink
514 {
515 tracing::info!(
516 rate_limit = entry.rate_limit,
517 "received sink rate limit on actor {actor_id}"
518 );
519 if let Err(e) = rate_limit_tx.send(entry.rate_limit.into()) {
520 error!(
521 error = %e.as_report(),
522 "fail to send sink rate limit update"
523 );
524 return Err(StreamExecutorError::from(
525 e.to_report_string(),
526 ));
527 }
528 }
529 }
530 Mutation::ConnectorPropsChange(config) => {
531 if let Some(map) = config.get(&sink_id.as_raw_id())
532 && let Err(e) = rebuild_sink_tx
533 .send(RebuildSinkMessage::UpdateConfig(map.clone()))
534 {
535 error!(
536 error = %e.as_report(),
537 "fail to send sink alter props"
538 );
539 return Err(StreamExecutorError::from(e.to_report_string()));
540 }
541 }
542 _ => (),
543 }
544 }
545 }
546 }
547 }
548 }
549
550 #[expect(clippy::too_many_arguments)]
551 #[try_stream(ok = Message, error = StreamExecutorError)]
552 async fn process_msg(
553 input: impl MessageStream,
554 sink_type: SinkType,
555 stream_key: StreamKey,
556 chunk_size: usize,
557 input_data_types: Vec<DataType>,
558 input_compact_ib: InconsistencyBehavior,
559 downstream_pk: Option<Vec<usize>>,
560 non_append_only_behavior: Option<NonAppendOnlyBehavior>,
561 sink_chunk_buffer_size_metrics: LabelGuardedIntGauge,
562 preserve_row_level_changes: bool,
563 skip_compact: bool,
564 ) {
565 if let Some(b) = non_append_only_behavior
567 && b.should_reorder_records()
568 {
569 assert_matches!(sink_type, SinkType::Upsert | SinkType::Retract);
570
571 let mut chunk_buffer = EstimatedVec::new();
572 let mut watermark: Option<super::Watermark> = None;
573 #[for_await]
574 for msg in input {
575 match msg? {
576 Message::Watermark(w) => watermark = Some(w),
577 Message::Chunk(c) => {
578 chunk_buffer.push(c);
579 sink_chunk_buffer_size_metrics.set(chunk_buffer.estimated_size() as i64);
580 }
581 Message::Barrier(barrier) => {
582 let chunks = mem::take(&mut chunk_buffer).into_inner();
583
584 let mut delete_chunks = vec![];
587 let mut insert_chunks = vec![];
588
589 for c in dispatch_output_kind!(sink_type, KIND, {
590 StreamChunkCompactor::new(stream_key.clone(), chunks)
591 .into_compacted_chunks_inline::<KIND>(input_compact_ib)
592 }) {
593 let chunk = force_delete_only(c.clone());
594 if chunk.cardinality() > 0 {
595 delete_chunks.push(chunk);
596 }
597 let chunk = force_append_only(c);
598 if chunk.cardinality() > 0 {
599 insert_chunks.push(chunk);
600 }
601 }
602 let chunks = delete_chunks
603 .into_iter()
604 .chain(insert_chunks.into_iter())
605 .collect();
606
607 if let Some(downstream_pk) = &downstream_pk
614 && !preserve_row_level_changes
615 {
616 let chunks = dispatch_output_kind!(sink_type, KIND, {
617 StreamChunkCompactor::new(downstream_pk.clone(), chunks)
618 .into_compacted_chunks_reconstructed::<KIND>(
619 chunk_size,
620 input_data_types.clone(),
621 InconsistencyBehavior::Warn,
624 )
625 });
626 for c in chunks {
627 yield Message::Chunk(c);
628 }
629 } else {
630 let mut chunk_builder =
631 StreamChunkBuilder::new(chunk_size, input_data_types.clone());
632 for chunk in chunks {
633 for (op, row) in chunk.rows() {
634 if let Some(c) = chunk_builder.append_row(op, row) {
635 yield Message::Chunk(c);
636 }
637 }
638 }
639
640 if let Some(c) = chunk_builder.take() {
641 yield Message::Chunk(c);
642 }
643 };
644
645 if let Some(w) = mem::take(&mut watermark) {
647 yield Message::Watermark(w)
648 }
649 yield Message::Barrier(barrier);
650 }
651 }
652 }
653 } else {
654 #[for_await]
657 for msg in input {
658 match msg? {
659 Message::Watermark(w) => yield Message::Watermark(w),
660 Message::Chunk(mut chunk) => {
661 if !sink_type.is_append_only()
665 && let Some(downstream_pk) = &downstream_pk
666 {
667 if preserve_row_level_changes {
668 } else if skip_compact {
671 assert_eq!(&stream_key, downstream_pk);
674 } else {
675 chunk = dispatch_output_kind!(sink_type, KIND, {
676 compact_chunk_inline::<KIND>(
677 chunk,
678 downstream_pk,
679 InconsistencyBehavior::Warn,
682 )
683 });
684 }
685 }
686 yield Message::Chunk(chunk);
687 }
688 Message::Barrier(barrier) => {
689 yield Message::Barrier(barrier);
690 }
691 }
692 }
693 }
694 }
695
696 #[expect(clippy::too_many_arguments)]
697 async fn execute_consume_log<S: Sink, R: LogReader>(
698 mut sink: S,
699 log_reader: R,
700 columns: Vec<ColumnCatalog>,
701 mut sink_param: SinkParam,
702 mut sink_writer_param: SinkWriterParam,
703 non_append_only_behavior: Option<NonAppendOnlyBehavior>,
704 actor_context: ActorContextRef,
705 rate_limit_rx: UnboundedReceiver<RateLimit>,
706 mut rebuild_sink_rx: UnboundedReceiver<RebuildSinkMessage>,
707 ) -> StreamExecutorResult<!> {
708 let visible_columns = columns
709 .iter()
710 .enumerate()
711 .filter_map(|(idx, column)| (!column.is_hidden).then_some(idx))
712 .collect_vec();
713
714 let labels = [
715 &actor_context.id.to_string(),
716 sink_writer_param.connector.as_str(),
717 &sink_writer_param.sink_id.to_string(),
718 sink_writer_param.sink_name.as_str(),
719 ];
720 let log_store_reader_wait_new_future_duration_ns = GLOBAL_SINK_METRICS
721 .log_store_reader_wait_new_future_duration_ns
722 .with_guarded_label_values(&labels);
723 let log_store_read_rows = GLOBAL_SINK_METRICS
724 .log_store_read_rows
725 .with_guarded_label_values(&labels);
726 let log_store_read_bytes = GLOBAL_SINK_METRICS
727 .log_store_read_bytes
728 .with_guarded_label_values(&labels);
729 let log_store_latest_read_epoch = GLOBAL_SINK_METRICS
730 .log_store_latest_read_epoch
731 .with_guarded_label_values(&labels);
732 let metrics = LogReaderMetrics {
733 log_store_latest_read_epoch,
734 log_store_read_rows,
735 log_store_read_bytes,
736 log_store_reader_wait_new_future_duration_ns,
737 };
738
739 let downstream_pk = sink_param.downstream_pk.clone();
740
741 let mut log_reader = log_reader
742 .transform_chunk(move |chunk| {
743 let chunk = if let Some(b) = non_append_only_behavior
744 && b.should_compact_in_log_reader()
745 {
746 let downstream_pk = downstream_pk.as_ref().unwrap();
748 dispatch_output_kind!(sink_param.sink_type, KIND, {
749 compact_chunk_inline::<KIND>(
750 chunk,
751 downstream_pk,
752 InconsistencyBehavior::Warn,
755 )
756 })
757 } else {
758 chunk
759 };
760 if visible_columns.len() != columns.len() {
761 chunk.project(&visible_columns)
764 } else {
765 chunk
766 }
767 })
768 .monitored(metrics)
769 .rate_limited(rate_limit_rx);
770
771 log_reader.init().await?;
772 loop {
773 let future = async {
774 loop {
775 let Err(e) = sink
776 .new_log_sinker(sink_writer_param.clone())
777 .and_then(|log_sinker| log_sinker.consume_log_and_sink(&mut log_reader))
778 .await;
779 GLOBAL_ERROR_METRICS.user_sink_error.report([
780 "sink_executor_error".to_owned(),
781 sink_param.sink_id.to_string(),
782 sink_param.sink_name.clone(),
783 actor_context.fragment_id.to_string(),
784 ]);
785
786 if let Some(meta_client) = sink_writer_param.meta_client.as_ref() {
787 meta_client
788 .add_sink_fail_evet_log(
789 sink_writer_param.sink_id,
790 sink_writer_param.sink_name.clone(),
791 sink_writer_param.connector.clone(),
792 e.to_report_string(),
793 )
794 .await;
795 }
796
797 if F::ALLOW_REWIND {
798 match log_reader.rewind().await {
799 Ok(()) => {
800 error!(
801 error = %e.as_report(),
802 executor_id = %sink_writer_param.executor_id,
803 sink_id = %sink_param.sink_id,
804 "reset log reader stream successfully after sink error"
805 );
806 Ok(())
807 }
808 Err(rewind_err) => {
809 error!(
810 error = %rewind_err.as_report(),
811 "fail to rewind log reader"
812 );
813 Err(e)
814 }
815 }
816 } else {
817 Err(e)
818 }
819 .map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
820 }
821 };
822 select! {
823 result = future => {
824 let Err(e): StreamExecutorResult<!> = result;
825 return Err(e);
826 }
827 result = rebuild_sink_rx.recv() => {
828 match result.ok_or_else(|| anyhow!("failed to receive rebuild sink notify"))? {
829 RebuildSinkMessage::RebuildSink(new_vnode, notify) => {
830 sink_writer_param.vnode_bitmap = Some((*new_vnode).clone());
831 if notify.send(()).is_err() {
832 warn!("failed to notify rebuild sink");
833 }
834 log_reader.init().await?;
835 },
836 RebuildSinkMessage::UpdateConfig(config) => {
837 let config_has_changes =
838 sink_config_has_changes(&sink_param.properties, &config);
839 if F::ALLOW_REWIND {
840 match log_reader.rewind().await {
841 Ok(()) => {
842 if config_has_changes {
843 sink_param.properties.extend(config.into_iter());
844 sink = TryFrom::try_from(sink_param.clone()).map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
845 info!(
846 executor_id = %sink_writer_param.executor_id,
847 sink_id = %sink_param.sink_id,
848 "alter sink config successfully with rewind"
849 );
850 } else {
851 info!(
852 executor_id = %sink_writer_param.executor_id,
853 sink_id = %sink_param.sink_id,
854 "skip alter sink config because properties are unchanged"
855 );
856 }
857 Ok(())
858 }
859 Err(rewind_err) => {
860 error!(
861 error = %rewind_err.as_report(),
862 "fail to rewind log reader for alter sink config "
863 );
864 Err(anyhow!("fail to rewind log reader after alter sink config notification").into())
865 }
866 }
867 } else {
868 if config_has_changes {
869 sink_param.properties.extend(config.into_iter());
870 sink = TryFrom::try_from(sink_param.clone()).map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
871 } else {
872 info!(
873 executor_id = %sink_writer_param.executor_id,
874 sink_id = %sink_param.sink_id,
875 "skip rebuilding sink because properties are unchanged"
876 );
877 }
878 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())
879 }
880 .map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
881 },
882 }
883 }
884 }
885 }
886 }
887}
888
889enum RebuildSinkMessage {
890 RebuildSink(Arc<Bitmap>, oneshot::Sender<()>),
891 UpdateConfig(HashMap<String, String>),
892}
893
894fn sink_config_has_changes(
895 current: &BTreeMap<String, String>,
896 incoming: &HashMap<String, String>,
897) -> bool {
898 incoming
899 .iter()
900 .any(|(key, value)| current.get(key) != Some(value))
901}
902
903impl<F: LogStoreFactory> Execute for SinkExecutor<F> {
904 fn execute(self: Box<Self>) -> BoxedMessageStream {
905 self.execute_inner()
906 }
907}
908
909#[cfg(test)]
910mod test {
911 use std::future::pending;
912 use std::sync::Arc;
913 use std::sync::atomic::{AtomicUsize, Ordering};
914 use std::time::Duration;
915
916 use risingwave_common::catalog::{ColumnDesc, ColumnId};
917 use risingwave_common::util::epoch::test_epoch;
918 use risingwave_connector::sink::build_sink;
919 use risingwave_connector::sink::log_store::{LogStoreReadItem, LogStoreResult, TruncateOffset};
920 use risingwave_connector::sink::trivial::BlackHoleSink;
921 use tokio::sync::Notify;
922
923 use super::*;
924 use crate::common::log_store_impl::in_mem::{
925 BoundedInMemLogStoreFactory, BoundedInMemLogStoreWriter,
926 };
927 use crate::executor::test_utils::*;
928
929 #[derive(Default)]
930 struct RewindRequiredLogReaderState {
931 start_count: AtomicUsize,
932 rewind_count: AtomicUsize,
933 started: Notify,
934 }
935
936 impl RewindRequiredLogReaderState {
937 async fn wait_for_start_count(&self, expected: usize) {
938 tokio::time::timeout(Duration::from_secs(5), async {
939 loop {
940 let started = self.started.notified();
941 if self.start_count.load(Ordering::SeqCst) >= expected {
942 break;
943 }
944 started.await;
945 }
946 })
947 .await
948 .unwrap_or_else(|_| panic!("log reader was not started {expected} times"));
949 }
950 }
951
952 struct RewindRequiredLogReader {
953 is_reset: bool,
954 state: Arc<RewindRequiredLogReaderState>,
955 }
956
957 impl LogReader for RewindRequiredLogReader {
958 async fn init(&mut self) -> LogStoreResult<()> {
959 self.is_reset = true;
960 Ok(())
961 }
962
963 async fn start_from(&mut self, _start_offset: Option<u64>) -> LogStoreResult<()> {
964 assert!(
965 self.is_reset,
966 "log reader must be rewound before restarting"
967 );
968 self.is_reset = false;
969 self.state.start_count.fetch_add(1, Ordering::SeqCst);
970 self.state.started.notify_waiters();
971 Ok(())
972 }
973
974 async fn next_item(&mut self) -> LogStoreResult<(u64, LogStoreReadItem)> {
975 pending().await
976 }
977
978 fn truncate(&mut self, _offset: TruncateOffset) -> LogStoreResult<()> {
979 Ok(())
980 }
981
982 async fn rewind(&mut self) -> LogStoreResult<()> {
983 self.is_reset = true;
984 self.state.rewind_count.fetch_add(1, Ordering::SeqCst);
985 Ok(())
986 }
987 }
988
989 struct TestLogStoreFactory<const CAN_REWIND: bool>;
990
991 impl<const CAN_REWIND: bool> LogStoreFactory for TestLogStoreFactory<CAN_REWIND> {
992 type Reader = RewindRequiredLogReader;
993 type Writer = BoundedInMemLogStoreWriter;
994
995 const ALLOW_REWIND: bool = CAN_REWIND;
996 const REBUILD_SINK_ON_UPDATE_VNODE_BITMAP: bool = false;
997
998 async fn build(self) -> (Self::Reader, Self::Writer) {
999 unreachable!()
1000 }
1001 }
1002
1003 fn sink_param_for_config_update_test() -> SinkParam {
1004 SinkParam {
1005 sink_id: 0.into(),
1006 sink_name: "test".into(),
1007 properties: BTreeMap::from([
1008 ("connector".to_owned(), "blackhole".to_owned()),
1009 ("commit_checkpoint_interval".to_owned(), "1".to_owned()),
1010 ]),
1011 columns: vec![],
1012 downstream_pk: None,
1013 sink_type: SinkType::AppendOnly,
1014 ignore_delete: false,
1015 format_desc: None,
1016 db_name: "test".into(),
1017 sink_from_name: "test".into(),
1018 }
1019 }
1020
1021 #[test]
1022 fn test_sink_config_has_changes() {
1023 let current = BTreeMap::from([
1024 ("connector".to_owned(), "blackhole".to_owned()),
1025 ("commit_checkpoint_interval".to_owned(), "1".to_owned()),
1026 ]);
1027
1028 assert!(!sink_config_has_changes(
1029 ¤t,
1030 &HashMap::from([("commit_checkpoint_interval".to_owned(), "1".to_owned())])
1031 ));
1032 assert!(sink_config_has_changes(
1033 ¤t,
1034 &HashMap::from([("commit_checkpoint_interval".to_owned(), "2".to_owned())])
1035 ));
1036 assert!(sink_config_has_changes(
1037 ¤t,
1038 &HashMap::from([("force_append_only".to_owned(), "true".to_owned())])
1039 ));
1040 }
1041
1042 #[tokio::test]
1043 async fn test_no_op_sink_config_update_rewinds_log_reader() {
1044 let sink_param = sink_param_for_config_update_test();
1045 let sink = BlackHoleSink::try_from(sink_param.clone()).unwrap();
1046 let state = Arc::new(RewindRequiredLogReaderState::default());
1047 let log_reader = RewindRequiredLogReader {
1048 is_reset: false,
1049 state: state.clone(),
1050 };
1051 let (rate_limit_tx, rate_limit_rx) = unbounded_channel();
1052 let (rebuild_sink_tx, rebuild_sink_rx) = unbounded_channel();
1053
1054 let consume_log = SinkExecutor::<TestLogStoreFactory<true>>::execute_consume_log(
1055 sink,
1056 log_reader,
1057 vec![],
1058 sink_param,
1059 SinkWriterParam::for_test(),
1060 None,
1061 ActorContext::for_test(0),
1062 rate_limit_rx,
1063 rebuild_sink_rx,
1064 );
1065 tokio::pin!(consume_log);
1066
1067 tokio::select! {
1068 result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1069 () = state.wait_for_start_count(1) => {},
1070 }
1071
1072 rebuild_sink_tx
1073 .send(RebuildSinkMessage::UpdateConfig(HashMap::from([(
1074 "commit_checkpoint_interval".to_owned(),
1075 "1".to_owned(),
1076 )])))
1077 .unwrap();
1078
1079 tokio::select! {
1080 result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1081 () = state.wait_for_start_count(2) => {},
1082 }
1083
1084 assert_eq!(state.rewind_count.load(Ordering::SeqCst), 1);
1085 drop(rate_limit_tx);
1086 }
1087
1088 #[tokio::test]
1089 async fn test_no_op_sink_config_update_triggers_recovery_without_rewind() {
1090 let sink_param = sink_param_for_config_update_test();
1091 let sink = BlackHoleSink::try_from(sink_param.clone()).unwrap();
1092 let state = Arc::new(RewindRequiredLogReaderState::default());
1093 let log_reader = RewindRequiredLogReader {
1094 is_reset: false,
1095 state: state.clone(),
1096 };
1097 let (rate_limit_tx, rate_limit_rx) = unbounded_channel();
1098 let (rebuild_sink_tx, rebuild_sink_rx) = unbounded_channel();
1099
1100 let consume_log = SinkExecutor::<TestLogStoreFactory<false>>::execute_consume_log(
1101 sink,
1102 log_reader,
1103 vec![],
1104 sink_param,
1105 SinkWriterParam::for_test(),
1106 None,
1107 ActorContext::for_test(0),
1108 rate_limit_rx,
1109 rebuild_sink_rx,
1110 );
1111 tokio::pin!(consume_log);
1112
1113 tokio::select! {
1114 result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1115 () = state.wait_for_start_count(1) => {},
1116 }
1117
1118 rebuild_sink_tx
1119 .send(RebuildSinkMessage::UpdateConfig(HashMap::from([(
1120 "commit_checkpoint_interval".to_owned(),
1121 "1".to_owned(),
1122 )])))
1123 .unwrap();
1124
1125 tokio::time::timeout(Duration::from_secs(5), consume_log)
1126 .await
1127 .expect("log consumer should trigger recovery")
1128 .expect_err("log consumer should return an error");
1129 assert_eq!(state.start_count.load(Ordering::SeqCst), 1);
1130 assert_eq!(state.rewind_count.load(Ordering::SeqCst), 0);
1131 drop(rate_limit_tx);
1132 }
1133
1134 #[tokio::test]
1135 async fn test_force_append_only_sink() {
1136 use risingwave_common::array::StreamChunkTestExt;
1137 use risingwave_common::array::stream_chunk::StreamChunk;
1138 use risingwave_common::types::DataType;
1139
1140 use crate::executor::Barrier;
1141
1142 let properties = maplit::btreemap! {
1143 "connector".into() => "blackhole".into(),
1144 "type".into() => "append-only".into(),
1145 "force_append_only".into() => "true".into()
1146 };
1147
1148 let columns = vec![
1151 ColumnCatalog {
1152 column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1153 is_hidden: false,
1154 },
1155 ColumnCatalog {
1156 column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1157 is_hidden: false,
1158 },
1159 ColumnCatalog {
1160 column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1161 is_hidden: true,
1162 },
1163 ];
1164 let schema: Schema = columns
1165 .iter()
1166 .map(|column| Field::from(column.column_desc.clone()))
1167 .collect();
1168 let stream_key = vec![0];
1169
1170 let source = MockSource::with_messages(vec![
1171 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1172 Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1173 " I I I
1174 + 3 2 1",
1175 ))),
1176 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1177 Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1178 " I I I
1179 U- 3 2 1
1180 U+ 3 4 1
1181 + 5 6 7",
1182 ))),
1183 Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1184 " I I I
1185 - 5 6 7",
1186 ))),
1187 ])
1188 .into_executor(schema.clone(), stream_key.clone());
1189
1190 let sink_param = SinkParam {
1191 sink_id: 0.into(),
1192 sink_name: "test".into(),
1193 properties,
1194
1195 columns: columns
1196 .iter()
1197 .filter(|col| !col.is_hidden)
1198 .map(|col| col.column_desc.clone())
1199 .collect(),
1200 downstream_pk: Some(stream_key.clone()),
1201 sink_type: SinkType::AppendOnly,
1202 ignore_delete: true,
1203 format_desc: None,
1204 db_name: "test".into(),
1205 sink_from_name: "test".into(),
1206 };
1207
1208 let info = ExecutorInfo::for_test(schema, stream_key, "SinkExecutor".to_owned(), 0);
1209
1210 let sink = build_sink(sink_param.clone()).unwrap();
1211
1212 let sink_executor = SinkExecutor::new(
1213 ActorContext::for_test(0),
1214 info,
1215 source,
1216 SinkWriterParam::for_test(),
1217 sink,
1218 sink_param,
1219 columns.clone(),
1220 BoundedInMemLogStoreFactory::for_test(1),
1221 1024,
1222 vec![DataType::Int32, DataType::Int32, DataType::Int32],
1223 None,
1224 )
1225 .unwrap();
1226
1227 let mut executor = sink_executor.boxed().execute();
1228
1229 executor.next().await.unwrap().unwrap();
1231
1232 let chunk_msg = executor.next().await.unwrap().unwrap();
1233 assert_eq!(
1234 chunk_msg.into_chunk().unwrap().compact_vis(),
1235 StreamChunk::from_pretty(
1236 " I I I
1237 + 3 2 1",
1238 )
1239 );
1240
1241 executor.next().await.unwrap().unwrap();
1243
1244 let chunk_msg = executor.next().await.unwrap().unwrap();
1245 assert_eq!(
1246 chunk_msg.into_chunk().unwrap().compact_vis(),
1247 StreamChunk::from_pretty(
1248 " I I I
1249 + 3 4 1
1250 + 5 6 7",
1251 )
1252 );
1253
1254 executor.next().await.unwrap().unwrap();
1259 }
1260
1261 #[tokio::test]
1262 async fn stream_key_sink_pk_mismatch_upsert() {
1263 stream_key_sink_pk_mismatch(SinkType::Upsert).await;
1264 }
1265
1266 #[tokio::test]
1267 async fn stream_key_sink_pk_mismatch_retract() {
1268 stream_key_sink_pk_mismatch(SinkType::Retract).await;
1269 }
1270
1271 async fn stream_key_sink_pk_mismatch(sink_type: SinkType) {
1272 use risingwave_common::array::StreamChunkTestExt;
1273 use risingwave_common::array::stream_chunk::StreamChunk;
1274 use risingwave_common::types::DataType;
1275
1276 use crate::executor::Barrier;
1277
1278 let properties = maplit::btreemap! {
1279 "connector".into() => "blackhole".into(),
1280 };
1281
1282 let columns = vec![
1285 ColumnCatalog {
1286 column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1287 is_hidden: false,
1288 },
1289 ColumnCatalog {
1290 column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1291 is_hidden: false,
1292 },
1293 ColumnCatalog {
1294 column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1295 is_hidden: true,
1296 },
1297 ];
1298 let schema: Schema = columns
1299 .iter()
1300 .map(|column| Field::from(column.column_desc.clone()))
1301 .collect();
1302
1303 let source = MockSource::with_messages(vec![
1304 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1305 Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1306 " I I I
1307 + 1 1 10",
1308 ))),
1309 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1310 Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1311 " I I I
1312 + 1 3 30",
1313 ))),
1314 Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1315 " I I I
1316 + 1 2 20
1317 - 1 2 20",
1318 ))),
1319 Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1320 " I I I
1321 - 1 1 10
1322 + 1 1 40",
1323 ))),
1324 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1325 ])
1326 .into_executor(schema.clone(), vec![0, 1]);
1327
1328 let sink_param = SinkParam {
1329 sink_id: 0.into(),
1330 sink_name: "test".into(),
1331 properties,
1332
1333 columns: columns
1334 .iter()
1335 .filter(|col| !col.is_hidden)
1336 .map(|col| col.column_desc.clone())
1337 .collect(),
1338 downstream_pk: Some(vec![0]),
1339 sink_type,
1340 ignore_delete: false,
1341 format_desc: None,
1342 db_name: "test".into(),
1343 sink_from_name: "test".into(),
1344 };
1345
1346 let info = ExecutorInfo::for_test(schema, vec![0, 1], "SinkExecutor".to_owned(), 0);
1347
1348 let sink = build_sink(sink_param.clone()).unwrap();
1349
1350 let sink_executor = SinkExecutor::new(
1351 ActorContext::for_test(0),
1352 info,
1353 source,
1354 SinkWriterParam::for_test(),
1355 sink,
1356 sink_param,
1357 columns.clone(),
1358 BoundedInMemLogStoreFactory::for_test(1),
1359 1024,
1360 vec![DataType::Int64, DataType::Int64, DataType::Int64],
1361 None,
1362 )
1363 .unwrap();
1364
1365 let mut executor = sink_executor.boxed().execute();
1366
1367 executor.next().await.unwrap().unwrap();
1369
1370 let chunk_msg = executor.next().await.unwrap().unwrap();
1371 assert_eq!(
1372 chunk_msg.into_chunk().unwrap().compact_vis(),
1373 StreamChunk::from_pretty(
1374 " I I I
1375 + 1 1 10",
1376 )
1377 );
1378
1379 executor.next().await.unwrap().unwrap();
1381
1382 let chunk_msg = executor.next().await.unwrap().unwrap();
1383 let expected = match sink_type {
1384 SinkType::Retract => StreamChunk::from_pretty(
1385 " I I I
1386 U- 1 1 10
1387 U+ 1 1 40",
1388 ),
1389 SinkType::Upsert => StreamChunk::from_pretty(
1390 " I I I
1391 + 1 1 40", ),
1393 _ => unreachable!(),
1394 };
1395 assert_eq!(chunk_msg.into_chunk().unwrap().compact_vis(), expected);
1396
1397 executor.next().await.unwrap().unwrap();
1399 }
1400
1401 #[tokio::test]
1402 async fn test_sink_into_table_preserves_special_conflict_rows_for_mismatched_pk() {
1403 use risingwave_common::array::StreamChunkTestExt;
1404 use risingwave_common::array::stream_chunk::StreamChunk;
1405 use risingwave_common::types::DataType;
1406
1407 use crate::executor::Barrier;
1408
1409 let properties = maplit::btreemap! {
1410 "connector".into() => "table".into(),
1411 SINK_USER_PRESERVE_ROW_LEVEL_CHANGES.into() => "true".into(),
1412 };
1413
1414 let columns = vec![
1415 ColumnCatalog {
1416 column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1417 is_hidden: false,
1418 },
1419 ColumnCatalog {
1420 column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1421 is_hidden: false,
1422 },
1423 ColumnCatalog {
1424 column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1425 is_hidden: false,
1426 },
1427 ];
1428 let schema: Schema = columns
1429 .iter()
1430 .map(|column| Field::from(column.column_desc.clone()))
1431 .collect();
1432
1433 let source = MockSource::with_messages(vec![
1434 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1435 Message::Chunk(StreamChunk::from_pretty(
1436 " I I I
1437 + 1 10 1",
1438 )),
1439 Message::Chunk(StreamChunk::from_pretty(
1440 " I I I
1441 + 1 20 2",
1442 )),
1443 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1444 ])
1445 .into_executor(schema.clone(), vec![0, 2]);
1446
1447 let sink_param = SinkParam {
1448 sink_id: 0.into(),
1449 sink_name: "test".into(),
1450 properties,
1451 columns: columns.iter().map(|col| col.column_desc.clone()).collect(),
1452 downstream_pk: Some(vec![0]),
1453 sink_type: SinkType::Upsert,
1454 ignore_delete: false,
1455 format_desc: None,
1456 db_name: "test".into(),
1457 sink_from_name: "test".into(),
1458 };
1459
1460 let info = ExecutorInfo::for_test(schema, vec![0, 2], "SinkExecutor".to_owned(), 0);
1461 let sink = build_sink(sink_param.clone()).unwrap();
1462
1463 let sink_executor = SinkExecutor::new(
1464 ActorContext::for_test(0),
1465 info,
1466 source,
1467 SinkWriterParam::for_test(),
1468 sink,
1469 sink_param,
1470 columns,
1471 BoundedInMemLogStoreFactory::for_test(1),
1472 1024,
1473 vec![DataType::Int64, DataType::Int64, DataType::Int64],
1474 None,
1475 )
1476 .unwrap();
1477
1478 let mut executor = sink_executor.boxed().execute();
1479
1480 executor.next().await.unwrap().unwrap();
1481
1482 let chunk_msg = executor.next().await.unwrap().unwrap();
1483 assert_eq!(
1484 chunk_msg.into_chunk().unwrap().compact_vis(),
1485 StreamChunk::from_pretty(
1486 " I I I
1487 + 1 10 1
1488 + 1 20 2",
1489 )
1490 );
1491
1492 executor.next().await.unwrap().unwrap();
1493 }
1494
1495 #[tokio::test]
1496 async fn test_sink_into_table_keeps_default_compaction_without_special_conflict_semantics() {
1497 use risingwave_common::array::StreamChunkTestExt;
1498 use risingwave_common::array::stream_chunk::StreamChunk;
1499 use risingwave_common::types::DataType;
1500
1501 use crate::executor::Barrier;
1502
1503 let properties = maplit::btreemap! {
1504 "connector".into() => "table".into(),
1505 };
1506
1507 let columns = vec![
1508 ColumnCatalog {
1509 column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1510 is_hidden: false,
1511 },
1512 ColumnCatalog {
1513 column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1514 is_hidden: false,
1515 },
1516 ];
1517 let schema: Schema = columns
1518 .iter()
1519 .map(|column| Field::from(column.column_desc.clone()))
1520 .collect();
1521
1522 let source = MockSource::with_messages(vec![
1523 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1524 Message::Chunk(StreamChunk::from_pretty(
1525 " I I
1526 + 1 10
1527 + 1 20",
1528 )),
1529 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1530 ])
1531 .into_executor(schema.clone(), vec![0]);
1532
1533 let sink_param = SinkParam {
1534 sink_id: 0.into(),
1535 sink_name: "test".into(),
1536 properties,
1537 columns: columns.iter().map(|col| col.column_desc.clone()).collect(),
1538 downstream_pk: Some(vec![0]),
1539 sink_type: SinkType::Upsert,
1540 ignore_delete: false,
1541 format_desc: None,
1542 db_name: "test".into(),
1543 sink_from_name: "test".into(),
1544 };
1545
1546 let info = ExecutorInfo::for_test(schema, vec![0], "SinkExecutor".to_owned(), 0);
1547 let sink = build_sink(sink_param.clone()).unwrap();
1548
1549 let sink_executor = SinkExecutor::new(
1550 ActorContext::for_test(0),
1551 info,
1552 source,
1553 SinkWriterParam::for_test(),
1554 sink,
1555 sink_param,
1556 columns,
1557 BoundedInMemLogStoreFactory::for_test(1),
1558 1024,
1559 vec![DataType::Int64, DataType::Int64],
1560 None,
1561 )
1562 .unwrap();
1563
1564 let mut executor = sink_executor.boxed().execute();
1565
1566 executor.next().await.unwrap().unwrap();
1567
1568 let chunk_msg = executor.next().await.unwrap().unwrap();
1569 assert_eq!(
1570 chunk_msg.into_chunk().unwrap().compact_vis(),
1571 StreamChunk::from_pretty(
1572 " I I
1573 + 1 20",
1574 )
1575 );
1576
1577 executor.next().await.unwrap().unwrap();
1578 }
1579
1580 #[tokio::test]
1581 async fn test_empty_barrier_sink() {
1582 use risingwave_common::types::DataType;
1583
1584 use crate::executor::Barrier;
1585
1586 let properties = maplit::btreemap! {
1587 "connector".into() => "blackhole".into(),
1588 "type".into() => "append-only".into(),
1589 "force_append_only".into() => "true".into()
1590 };
1591 let columns = vec![
1592 ColumnCatalog {
1593 column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1594 is_hidden: false,
1595 },
1596 ColumnCatalog {
1597 column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1598 is_hidden: false,
1599 },
1600 ];
1601 let schema: Schema = columns
1602 .iter()
1603 .map(|column| Field::from(column.column_desc.clone()))
1604 .collect();
1605 let stream_key = vec![0];
1606
1607 let source = MockSource::with_messages(vec![
1608 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1609 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1610 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1611 ])
1612 .into_executor(schema.clone(), stream_key.clone());
1613
1614 let sink_param = SinkParam {
1615 sink_id: 0.into(),
1616 sink_name: "test".into(),
1617 properties,
1618
1619 columns: columns
1620 .iter()
1621 .filter(|col| !col.is_hidden)
1622 .map(|col| col.column_desc.clone())
1623 .collect(),
1624 downstream_pk: Some(stream_key.clone()),
1625 sink_type: SinkType::AppendOnly,
1626 ignore_delete: true,
1627 format_desc: None,
1628 db_name: "test".into(),
1629 sink_from_name: "test".into(),
1630 };
1631
1632 let info = ExecutorInfo::for_test(schema, stream_key, "SinkExecutor".to_owned(), 0);
1633
1634 let sink = build_sink(sink_param.clone()).unwrap();
1635
1636 let sink_executor = SinkExecutor::new(
1637 ActorContext::for_test(0),
1638 info,
1639 source,
1640 SinkWriterParam::for_test(),
1641 sink,
1642 sink_param,
1643 columns,
1644 BoundedInMemLogStoreFactory::for_test(1),
1645 1024,
1646 vec![DataType::Int64, DataType::Int64],
1647 None,
1648 )
1649 .unwrap();
1650
1651 let mut executor = sink_executor.boxed().execute();
1652
1653 assert_eq!(
1655 executor.next().await.unwrap().unwrap(),
1656 Message::Barrier(Barrier::new_test_barrier(test_epoch(1)))
1657 );
1658
1659 assert_eq!(
1661 executor.next().await.unwrap().unwrap(),
1662 Message::Barrier(Barrier::new_test_barrier(test_epoch(2)))
1663 );
1664
1665 assert_eq!(
1667 executor.next().await.unwrap().unwrap(),
1668 Message::Barrier(Barrier::new_test_barrier(test_epoch(3)))
1669 );
1670 }
1671
1672 #[tokio::test]
1673 async fn test_force_compaction() {
1674 use risingwave_common::array::StreamChunkTestExt;
1675 use risingwave_common::array::stream_chunk::StreamChunk;
1676 use risingwave_common::types::DataType;
1677
1678 use crate::executor::Barrier;
1679
1680 let properties = maplit::btreemap! {
1681 "connector".into() => "blackhole".into(),
1682 "force_compaction".into() => "true".into()
1683 };
1684
1685 let columns = vec![
1688 ColumnCatalog {
1689 column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1690 is_hidden: false,
1691 },
1692 ColumnCatalog {
1693 column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1694 is_hidden: false,
1695 },
1696 ColumnCatalog {
1697 column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1698 is_hidden: true,
1699 },
1700 ];
1701 let schema: Schema = columns
1702 .iter()
1703 .map(|column| Field::from(column.column_desc.clone()))
1704 .collect();
1705
1706 let source = MockSource::with_messages(vec![
1707 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1708 Message::Chunk(StreamChunk::from_pretty(
1709 " I I I
1710 + 1 1 10",
1711 )),
1712 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1713 Message::Chunk(StreamChunk::from_pretty(
1714 " I I I
1715 + 1 3 30",
1716 )),
1717 Message::Chunk(StreamChunk::from_pretty(
1718 " I I I
1719 + 1 2 20
1720 - 1 2 20
1721 + 1 4 10",
1722 )),
1723 Message::Chunk(StreamChunk::from_pretty(
1724 " I I I
1725 - 1 1 10
1726 + 1 1 40",
1727 )),
1728 Message::Chunk(StreamChunk::from_pretty(
1729 " I I I
1730 - 1 4 30",
1731 )),
1732 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1733 ])
1734 .into_executor(schema.clone(), vec![0, 1]);
1735
1736 let sink_param = SinkParam {
1737 sink_id: 0.into(),
1738 sink_name: "test".into(),
1739 properties,
1740
1741 columns: columns
1742 .iter()
1743 .filter(|col| !col.is_hidden)
1744 .map(|col| col.column_desc.clone())
1745 .collect(),
1746 downstream_pk: Some(vec![0, 1]),
1747 sink_type: SinkType::Upsert,
1748 ignore_delete: false,
1749 format_desc: None,
1750 db_name: "test".into(),
1751 sink_from_name: "test".into(),
1752 };
1753
1754 let info = ExecutorInfo::for_test(schema, vec![0, 1], "SinkExecutor".to_owned(), 0);
1755
1756 let sink = build_sink(sink_param.clone()).unwrap();
1757
1758 let sink_executor = SinkExecutor::new(
1759 ActorContext::for_test(0),
1760 info,
1761 source,
1762 SinkWriterParam::for_test(),
1763 sink,
1764 sink_param,
1765 columns.clone(),
1766 BoundedInMemLogStoreFactory::for_test(1),
1767 1024,
1768 vec![DataType::Int64, DataType::Int64, DataType::Int64],
1769 None,
1770 )
1771 .unwrap();
1772
1773 let mut executor = sink_executor.boxed().execute();
1774
1775 executor.next().await.unwrap().unwrap();
1777
1778 let chunk_msg = executor.next().await.unwrap().unwrap();
1779 assert_eq!(
1780 chunk_msg.into_chunk().unwrap().compact_vis(),
1781 StreamChunk::from_pretty(
1782 " I I I
1783 + 1 1 10",
1784 )
1785 );
1786
1787 executor.next().await.unwrap().unwrap();
1789
1790 let chunk_msg = executor.next().await.unwrap().unwrap();
1791 assert_eq!(
1792 chunk_msg.into_chunk().unwrap().compact_vis(),
1793 StreamChunk::from_pretty(
1794 " I I I
1795 + 1 3 30
1796 + 1 1 40",
1797 )
1798 );
1799
1800 executor.next().await.unwrap().unwrap();
1802 }
1803}