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