1use std::future::Future;
16
17use futures::future::{Either, pending, select};
18use futures::pin_mut;
19use futures::stream::FuturesOrdered;
20use multimap::MultiMap;
21use risingwave_common::metrics::LabelGuardedIntGauge;
22use risingwave_common::row::RowExt;
23use risingwave_common::types::ToOwnedDatum;
24use risingwave_common::util::iter_util::ZipEqFast;
25use risingwave_expr::expr::NonStrictExpression;
26use tokio::sync::Semaphore;
27
28use crate::executor::prelude::*;
29
30type ProjectMessageFuture = impl Future<Output = StreamExecutorResult<ProjectedMessage>> + Send;
31type PendingProjectMessages = FuturesOrdered<ProjectMessageFuture>;
32
33enum ProjectMessageInput {
34 Chunk {
35 chunk: StreamChunk,
36 inflight_request_semaphore: Option<Arc<Semaphore>>,
37 },
38 Watermark {
39 watermark: Watermark,
40 out_col_indices: Vec<usize>,
41 },
42 Barrier(Barrier),
43}
44
45enum ProjectedMessage {
46 Chunk(StreamChunk),
47 Watermark(Vec<Watermark>),
48 Barrier(Barrier),
49}
50
51pub struct ProjectExecutor {
55 input: Executor,
56 inner: Inner,
57}
58
59struct Inner {
60 actor_id: ActorId,
61
62 exprs: Arc<Vec<NonStrictExpression>>,
64 watermark_derivations: MultiMap<usize, usize>,
67 nondecreasing_expr_indices: Vec<usize>,
69 last_nondec_expr_values: Vec<Option<ScalarImpl>>,
71
72 eliminate_noop_updates: bool,
75
76 project_expr_concurrency: usize,
78
79 project_expr_inflight_request_concurrency: Option<Arc<Semaphore>>,
82
83 project_expr_inflight_window_size: Option<LabelGuardedIntGauge>,
85}
86
87impl ProjectExecutor {
88 pub fn new(
89 ctx: ActorContextRef,
90 input: Executor,
91 exprs: Vec<NonStrictExpression>,
92 watermark_derivations: MultiMap<usize, usize>,
93 nondecreasing_expr_indices: Vec<usize>,
94 noop_update_hint: bool,
95 ) -> Self {
96 let n_nondecreasing_exprs = nondecreasing_expr_indices.len();
97 let eliminate_noop_updates =
98 noop_update_hint || ctx.config.developer.aggressive_noop_update_elimination;
99 let project_expr_concurrency = match ctx.config.developer.project_expr_concurrency {
100 0 => usize::MAX,
101 concurrency => concurrency,
102 };
103 let project_expr_inflight_request_concurrency = match ctx
104 .config
105 .developer
106 .project_expr_inflight_request_concurrency
107 {
108 0 => None,
109 concurrency => Some(Arc::new(Semaphore::new(concurrency))),
110 };
111 let project_expr_inflight_window_size = (ctx.config.developer.project_expr_concurrency
112 != risingwave_common::config::default::developer::stream_project_expr_concurrency())
113 .then(|| {
114 ctx.streaming_metrics
115 .project_expr_inflight_window_size
116 .with_guarded_label_values(&[&ctx.id.to_string(), &ctx.fragment_id.to_string()])
117 });
118 Self {
119 input,
120 inner: Inner {
121 actor_id: ctx.id,
122 exprs: Arc::new(exprs),
123 watermark_derivations,
124 nondecreasing_expr_indices,
125 last_nondec_expr_values: vec![None; n_nondecreasing_exprs],
126 eliminate_noop_updates,
127 project_expr_concurrency,
128 project_expr_inflight_request_concurrency,
129 project_expr_inflight_window_size,
130 },
131 }
132 }
133}
134
135impl Debug for ProjectExecutor {
136 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
137 f.debug_struct("ProjectExecutor")
138 .field("exprs", &self.inner.exprs)
139 .finish()
140 }
141}
142
143impl Execute for ProjectExecutor {
144 fn execute(self: Box<Self>) -> BoxedMessageStream {
145 self.inner.execute(self.input).boxed()
146 }
147}
148
149pub async fn apply_project_exprs(
150 exprs: &[NonStrictExpression],
151 chunk: StreamChunk,
152) -> StreamExecutorResult<StreamChunk> {
153 let (data_chunk, ops) = chunk.into_parts();
154 let mut projected_columns = Vec::new();
155
156 for expr in exprs {
157 let evaluated_expr = expr.eval_infallible(&data_chunk).await;
158 projected_columns.push(evaluated_expr);
159 }
160 let (_, vis) = data_chunk.into_parts();
161
162 let new_chunk = StreamChunk::with_visibility(ops, projected_columns, vis);
163
164 Ok(new_chunk)
165}
166
167impl Inner {
168 #[define_opaque(ProjectMessageFuture)]
169 fn project_message(
170 exprs: Arc<Vec<NonStrictExpression>>,
171 eliminate_noop_updates: bool,
172 input: ProjectMessageInput,
173 ) -> ProjectMessageFuture {
174 async move {
175 match input {
176 ProjectMessageInput::Chunk {
177 chunk,
178 inflight_request_semaphore,
179 } => {
180 let _permit = if let Some(semaphore) = inflight_request_semaphore {
181 Some(semaphore.acquire_owned().await.expect(
182 "project expression in-flight request semaphore should not be closed",
183 ))
184 } else {
185 None
186 };
187 let mut new_chunk = apply_project_exprs(&exprs, chunk)
188 .instrument_await("project_eval_chunk")
189 .await?;
190 if eliminate_noop_updates {
191 new_chunk = new_chunk.eliminate_adjacent_noop_update();
192 }
193 Ok(ProjectedMessage::Chunk(new_chunk))
194 }
195 ProjectMessageInput::Watermark {
196 watermark,
197 out_col_indices,
198 } => {
199 let mut ret = vec![];
200 for out_col_idx in out_col_indices {
201 let derived_watermark = watermark
202 .clone()
203 .transform_with_expr(&exprs[out_col_idx], out_col_idx)
204 .await;
205 if let Some(derived_watermark) = derived_watermark {
206 ret.push(derived_watermark);
207 } else {
208 warn!(
209 "a NULL watermark is derived with the expression {}!",
210 out_col_idx
211 );
212 }
213 }
214 Ok(ProjectedMessage::Watermark(ret))
215 }
216 ProjectMessageInput::Barrier(barrier) => Ok(ProjectedMessage::Barrier(barrier)),
217 }
218 }
219 }
220
221 fn update_last_nondec_expr_values(
222 nondecreasing_expr_indices: &[usize],
223 last_nondec_expr_values: &mut [Option<ScalarImpl>],
224 new_chunk: &StreamChunk,
225 ) {
226 {
227 {
228 {
229 {
230 if !nondecreasing_expr_indices.is_empty()
231 && let Some((_, first_visible_row)) = new_chunk.rows().next()
232 {
233 first_visible_row
235 .project(nondecreasing_expr_indices)
236 .iter()
237 .enumerate()
238 .for_each(|(idx, value)| {
239 last_nondec_expr_values[idx] =
240 Some(value.to_owned_datum().expect(
241 "non-decreasing expression should never be NULL",
242 ));
243 });
244 }
245 }
246 }
247 }
248 }
249 }
250
251 #[try_stream(ok = Message, error = StreamExecutorError)]
252 async fn execute(self, input: Executor) {
253 let Inner {
254 actor_id,
255 exprs,
256 watermark_derivations,
257 nondecreasing_expr_indices,
258 mut last_nondec_expr_values,
259 eliminate_noop_updates,
260 project_expr_concurrency,
261 project_expr_inflight_request_concurrency,
262 project_expr_inflight_window_size,
263 } = self;
264
265 let mut input = input.execute();
266 let first_barrier = expect_first_barrier(&mut input).await?;
267 let mut is_paused = first_barrier.is_pause_on_startup();
268 let mut received_stop_barrier = first_barrier.is_stop(actor_id);
269 yield Message::Barrier(first_barrier);
270 if received_stop_barrier {
271 return Ok(());
272 }
273
274 let mut pending_project_messages = PendingProjectMessages::new();
275 let mut pending_project_chunks = 0;
276
277 loop {
278 let has_pending_project_message = !pending_project_messages.is_empty();
279 let can_read_input =
280 !received_stop_barrier && pending_project_chunks < project_expr_concurrency;
281 let next_projected_message = async {
282 if has_pending_project_message {
283 pending_project_messages
284 .next()
285 .await
286 .expect("pending project messages should not be empty")
287 } else {
288 pending().await
289 }
290 };
291 let next_input_msg = async {
292 if can_read_input {
293 input.next().await.ok_or_else(|| {
294 StreamExecutorError::channel_closed("upstream executor closed unexpectedly")
295 })?
296 } else {
297 pending().await
298 }
299 };
300
301 pin_mut!(next_projected_message);
302 pin_mut!(next_input_msg);
303
304 match select(next_projected_message, next_input_msg).await {
305 Either::Left((projected_message, _)) => match projected_message? {
306 ProjectedMessage::Chunk(new_chunk) => {
307 pending_project_chunks -= 1;
308 if let Some(metric) = &project_expr_inflight_window_size {
309 metric.set(pending_project_messages.len() as _);
310 }
311 Self::update_last_nondec_expr_values(
312 &nondecreasing_expr_indices,
313 &mut last_nondec_expr_values,
314 &new_chunk,
315 );
316 yield Message::Chunk(new_chunk);
317 }
318 ProjectedMessage::Watermark(watermarks) => {
319 if let Some(metric) = &project_expr_inflight_window_size {
320 metric.set(pending_project_messages.len() as _);
321 }
322 for watermark in watermarks {
323 yield Message::Watermark(watermark);
324 }
325 }
326 ProjectedMessage::Barrier(barrier) => {
327 if let Some(metric) = &project_expr_inflight_window_size {
328 metric.set(pending_project_messages.len() as _);
329 }
330 if !is_paused {
331 for (&expr_idx, value) in nondecreasing_expr_indices
332 .iter()
333 .zip_eq_fast(&mut last_nondec_expr_values)
334 {
335 if let Some(value) = std::mem::take(value) {
336 yield Message::Watermark(Watermark::new(
337 expr_idx,
338 exprs[expr_idx].return_type(),
339 value,
340 ))
341 }
342 }
343 }
344
345 if let Some(mutation) = barrier.mutation.as_deref() {
346 match mutation {
347 Mutation::Pause => {
348 is_paused = true;
349 }
350 Mutation::Resume => {
351 is_paused = false;
352 }
353 _ => (),
354 }
355 }
356
357 let should_stop = barrier.is_stop(actor_id);
358 yield Message::Barrier(barrier);
359 if should_stop {
360 break;
361 }
362 }
363 },
364 Either::Right((msg, _)) => match msg? {
365 Message::Watermark(w) => {
366 let out_col_indices = match watermark_derivations.get_vec(&w.col_idx) {
367 Some(v) => v,
368 None => continue,
369 };
370 pending_project_messages.push_back(Self::project_message(
371 exprs.clone(),
372 eliminate_noop_updates,
373 ProjectMessageInput::Watermark {
374 watermark: w,
375 out_col_indices: out_col_indices.clone(),
376 },
377 ));
378 if let Some(metric) = &project_expr_inflight_window_size {
379 metric.set(pending_project_messages.len() as _);
380 }
381 }
382 Message::Chunk(chunk) => {
383 pending_project_messages.push_back(Self::project_message(
384 exprs.clone(),
385 eliminate_noop_updates,
386 ProjectMessageInput::Chunk {
387 chunk,
388 inflight_request_semaphore:
389 project_expr_inflight_request_concurrency.clone(),
390 },
391 ));
392 pending_project_chunks += 1;
393 if let Some(metric) = &project_expr_inflight_window_size {
394 metric.set(pending_project_messages.len() as _);
395 }
396 }
397 Message::Barrier(barrier) => {
398 if barrier.is_stop(actor_id) {
399 received_stop_barrier = true;
400 }
401 pending_project_messages.push_back(Self::project_message(
402 exprs.clone(),
403 eliminate_noop_updates,
404 ProjectMessageInput::Barrier(barrier),
405 ));
406 if let Some(metric) = &project_expr_inflight_window_size {
407 metric.set(pending_project_messages.len() as _);
408 }
409 }
410 },
411 }
412 }
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use std::sync::atomic::{self, AtomicBool, AtomicI64, AtomicUsize};
419 use std::time::Duration;
420
421 use risingwave_common::array::DataChunk;
422 use risingwave_common::array::stream_chunk::StreamChunkTestExt;
423 use risingwave_common::catalog::Field;
424 use risingwave_common::config::StreamingConfig;
425 use risingwave_common::types::DefaultOrd;
426 use risingwave_common::util::epoch::test_epoch;
427 use risingwave_expr::expr::{
428 self, AsyncExpression, AsyncExpressionBoxExt, ExpressionInfo, SyncExpression, ValueImpl,
429 };
430 use tokio::sync::Notify;
431 use tokio::time::timeout;
432
433 use super::*;
434 use crate::executor::StopMutation;
435 use crate::executor::test_utils::expr::build_from_pretty;
436 use crate::executor::test_utils::{MockSource, StreamExecutorTestExt};
437
438 fn actor_context_with_project_expr_concurrency(concurrency: usize) -> ActorContextRef {
439 actor_context_with_project_expr_limits(concurrency, 0)
440 }
441
442 fn actor_context_with_project_expr_limits(
443 concurrency: usize,
444 inflight_request_concurrency: usize,
445 ) -> ActorContextRef {
446 let mut config = StreamingConfig::default();
447 config.developer.project_expr_concurrency = concurrency;
448 config.developer.project_expr_inflight_request_concurrency = inflight_request_concurrency;
449 let mut ctx = ActorContext::for_test(123);
450 Arc::get_mut(&mut ctx)
451 .expect("test actor context should not be shared")
452 .config = Arc::new(config);
453 ctx
454 }
455
456 #[tokio::test]
457 async fn test_projection() {
458 let chunk1 = StreamChunk::from_pretty(
459 " I I
460 + 1 4
461 + 2 5
462 + 3 6",
463 );
464 let chunk2 = StreamChunk::from_pretty(
465 " I I
466 + 7 8
467 - 3 6",
468 );
469 let schema = Schema {
470 fields: vec![
471 Field::unnamed(DataType::Int64),
472 Field::unnamed(DataType::Int64),
473 ],
474 };
475 let stream_key = vec![0];
476 let (mut tx, source) = MockSource::channel();
477 let source = source.into_executor(schema, stream_key);
478
479 let test_expr = build_from_pretty("(add:int8 $0:int8 $1:int8)");
480
481 let proj = ProjectExecutor::new(
482 ActorContext::for_test(123),
483 source,
484 vec![test_expr],
485 MultiMap::new(),
486 vec![],
487 false,
488 );
489 let mut proj = proj.boxed().execute();
490
491 tx.push_barrier(test_epoch(1), false);
492 let barrier = proj.next().await.unwrap().unwrap();
493 barrier.as_barrier().unwrap();
494
495 tx.push_chunk(chunk1);
496 tx.push_chunk(chunk2);
497
498 let msg = proj.next().await.unwrap().unwrap();
499 assert_eq!(
500 *msg.as_chunk().unwrap(),
501 StreamChunk::from_pretty(
502 " I
503 + 5
504 + 7
505 + 9"
506 )
507 );
508
509 let msg = proj.next().await.unwrap().unwrap();
510 assert_eq!(
511 *msg.as_chunk().unwrap(),
512 StreamChunk::from_pretty(
513 " I
514 + 15
515 - 9"
516 )
517 );
518
519 tx.push_barrier(test_epoch(2), true);
520 assert!(proj.next().await.unwrap().unwrap().is_stop());
521 }
522
523 #[tokio::test]
524 async fn test_projection_does_not_poll_after_stop_barrier() {
525 let schema = Schema {
526 fields: vec![Field::unnamed(DataType::Int64)],
527 };
528 let (mut tx, source) = MockSource::channel();
529 let source = source.into_executor(schema, StreamKey::new());
530
531 let test_expr = build_from_pretty("(add:int8 $0:int8 1:int8)");
532
533 let proj = ProjectExecutor::new(
534 ActorContext::for_test(123),
535 source,
536 vec![test_expr],
537 MultiMap::new(),
538 vec![],
539 false,
540 );
541 let mut proj = proj.boxed().execute();
542
543 tx.push_barrier(test_epoch(1), false);
544 proj.expect_barrier().await;
545
546 tx.send_barrier(
547 Barrier::new_test_barrier(test_epoch(2)).with_mutation(Mutation::Stop(StopMutation {
548 dropped_actors: std::iter::once(123.into()).collect(),
549 ..Default::default()
550 })),
551 );
552 tx.push_chunk(StreamChunk::from_pretty(
553 " I
554 + 1",
555 ));
556
557 assert!(proj.next().await.unwrap().unwrap().is_stop());
558 assert!(proj.next().await.is_none());
559 }
560
561 #[derive(Debug)]
562 struct BlockingProjectExpr {
563 started_count: Arc<AtomicUsize>,
564 second_started: Arc<AtomicBool>,
565 second_started_notify: Arc<Notify>,
566 release_first: Arc<AtomicBool>,
567 release_first_notify: Arc<Notify>,
568 }
569
570 impl ExpressionInfo for BlockingProjectExpr {
571 fn return_type(&self) -> DataType {
572 DataType::Int64
573 }
574 }
575
576 impl AsyncExpression for BlockingProjectExpr {
577 async fn eval_v2(&self, input: &DataChunk) -> expr::Result<ValueImpl> {
578 let call_idx = self.started_count.fetch_add(1, atomic::Ordering::SeqCst);
579 if call_idx == 0 {
580 loop {
581 let notified = self.second_started_notify.notified();
582 if self.second_started.load(atomic::Ordering::SeqCst) {
583 break;
584 }
585 notified.await;
586 }
587 loop {
588 let notified = self.release_first_notify.notified();
589 if self.release_first.load(atomic::Ordering::SeqCst) {
590 break;
591 }
592 notified.await;
593 }
594 } else if call_idx == 1 {
595 self.second_started.store(true, atomic::Ordering::SeqCst);
596 self.second_started_notify.notify_waiters();
597 }
598
599 Ok(ValueImpl::Scalar {
600 value: Some((call_idx as i64).into()),
601 capacity: input.capacity(),
602 })
603 }
604
605 async fn eval_row(&self, _input: &OwnedRow) -> expr::Result<Datum> {
606 unimplemented!()
607 }
608 }
609
610 #[derive(Debug)]
611 struct FirstProjectExprWaitsForStartedCount {
612 started_count: Arc<AtomicUsize>,
613 started_notify: Arc<Notify>,
614 unblock_first_at_started_count: usize,
615 }
616
617 impl ExpressionInfo for FirstProjectExprWaitsForStartedCount {
618 fn return_type(&self) -> DataType {
619 DataType::Int64
620 }
621 }
622
623 impl AsyncExpression for FirstProjectExprWaitsForStartedCount {
624 async fn eval_v2(&self, input: &DataChunk) -> expr::Result<ValueImpl> {
625 let call_idx = self.started_count.fetch_add(1, atomic::Ordering::SeqCst);
626 self.started_notify.notify_waiters();
627 if call_idx == 0 {
628 loop {
629 let notified = self.started_notify.notified();
630 if self.started_count.load(atomic::Ordering::SeqCst)
631 >= self.unblock_first_at_started_count
632 {
633 break;
634 }
635 notified.await;
636 }
637 }
638
639 Ok(ValueImpl::Scalar {
640 value: Some((call_idx as i64).into()),
641 capacity: input.capacity(),
642 })
643 }
644
645 async fn eval_row(&self, _input: &OwnedRow) -> expr::Result<Datum> {
646 Ok(Some(0_i64.into()))
647 }
648 }
649
650 #[tokio::test]
651 async fn test_projection_evaluates_chunks_concurrently_before_barrier() {
652 let schema = Schema {
653 fields: vec![Field::unnamed(DataType::Int64)],
654 };
655 let (mut tx, source) = MockSource::channel();
656 let source = source.into_executor(schema, StreamKey::new());
657
658 let started_count = Arc::new(AtomicUsize::new(0));
659 let second_started = Arc::new(AtomicBool::new(false));
660 let second_started_notify = Arc::new(Notify::new());
661 let release_first = Arc::new(AtomicBool::new(false));
662 let release_first_notify = Arc::new(Notify::new());
663
664 let test_expr = NonStrictExpression::for_test(
665 BlockingProjectExpr {
666 started_count,
667 second_started: second_started.clone(),
668 second_started_notify: second_started_notify.clone(),
669 release_first: release_first.clone(),
670 release_first_notify: release_first_notify.clone(),
671 }
672 .boxed(),
673 );
674
675 let proj = ProjectExecutor::new(
676 actor_context_with_project_expr_concurrency(2),
677 source,
678 vec![test_expr],
679 MultiMap::new(),
680 vec![],
681 false,
682 );
683 let mut proj = proj.boxed().execute();
684
685 tx.push_barrier(test_epoch(1), false);
686 proj.expect_barrier().await;
687
688 tx.push_chunk(StreamChunk::from_pretty(
689 " I
690 + 1",
691 ));
692 tx.push_chunk(StreamChunk::from_pretty(
693 " I
694 + 2",
695 ));
696 tx.push_barrier(test_epoch(2), true);
697
698 let next_msg = proj.next();
699 pin_mut!(next_msg);
700 timeout(Duration::from_secs(5), async {
701 loop {
702 let notified = second_started_notify.notified();
703 if second_started.load(atomic::Ordering::SeqCst) {
704 break;
705 }
706 tokio::select! {
707 _ = notified => {}
708 msg = &mut next_msg => {
709 panic!("project executor emitted before the second chunk started: {msg:?}");
710 }
711 }
712 }
713 })
714 .await
715 .expect("second chunk expression did not start");
716
717 release_first.store(true, atomic::Ordering::SeqCst);
718 release_first_notify.notify_waiters();
719
720 let msg = next_msg.await.unwrap().unwrap();
721 assert_eq!(
722 *msg.as_chunk().unwrap(),
723 StreamChunk::from_pretty(
724 " I
725 + 0"
726 )
727 );
728
729 let msg = proj.next().await.unwrap().unwrap();
730 assert_eq!(
731 *msg.as_chunk().unwrap(),
732 StreamChunk::from_pretty(
733 " I
734 + 1"
735 )
736 );
737
738 assert!(proj.next().await.unwrap().unwrap().is_stop());
739 }
740
741 #[tokio::test]
742 async fn test_projection_reuses_finished_inflight_permit_across_barrier() {
743 let schema = Schema {
744 fields: vec![Field::unnamed(DataType::Int64)],
745 };
746 let (mut tx, source) = MockSource::channel();
747 let source = source.into_executor(schema, StreamKey::new());
748
749 let started_count = Arc::new(AtomicUsize::new(0));
750 let started_notify = Arc::new(Notify::new());
751 let test_expr = NonStrictExpression::for_test(
752 FirstProjectExprWaitsForStartedCount {
753 started_count: started_count.clone(),
754 started_notify: started_notify.clone(),
755 unblock_first_at_started_count: 3,
756 }
757 .boxed(),
758 );
759
760 let proj = ProjectExecutor::new(
761 actor_context_with_project_expr_limits(3, 2),
762 source,
763 vec![test_expr],
764 MultiMap::new(),
765 vec![],
766 false,
767 );
768 let mut proj = proj.boxed().execute();
769
770 tx.push_barrier(test_epoch(1), false);
771 proj.expect_barrier().await;
772
773 tx.push_chunk(StreamChunk::from_pretty(
774 " I
775 + 1",
776 ));
777 tx.push_chunk(StreamChunk::from_pretty(
778 " I
779 + 2",
780 ));
781 tx.push_barrier(test_epoch(2), false);
782 tx.push_chunk(StreamChunk::from_pretty(
783 " I
784 + 3",
785 ));
786 tx.push_barrier(test_epoch(3), true);
787
788 let first_msg = timeout(Duration::from_secs(5), async {
789 let next_msg = proj.next();
790 pin_mut!(next_msg);
791 let mut first_msg = None;
792 loop {
793 let notified = started_notify.notified();
794 if started_count.load(atomic::Ordering::SeqCst) >= 3 {
795 break;
796 }
797 tokio::select! {
798 _ = notified => {}
799 msg = &mut next_msg => {
800 if started_count.load(atomic::Ordering::SeqCst) < 3 {
801 panic!("project executor emitted before the post-barrier chunk started: {msg:?}");
802 }
803 first_msg = Some(msg.unwrap().unwrap());
804 break;
805 }
806 }
807 }
808 match first_msg {
809 Some(msg) => msg,
810 None => next_msg.await.unwrap().unwrap(),
811 }
812 })
813 .await
814 .expect("post-barrier chunk expression did not start");
815
816 assert_eq!(
817 *first_msg.as_chunk().unwrap(),
818 StreamChunk::from_pretty(
819 " I
820 + 0"
821 )
822 );
823 assert_eq!(
824 *proj.next().await.unwrap().unwrap().as_chunk().unwrap(),
825 StreamChunk::from_pretty(
826 " I
827 + 1"
828 )
829 );
830 proj.expect_barrier().await;
831 assert_eq!(
832 *proj.next().await.unwrap().unwrap().as_chunk().unwrap(),
833 StreamChunk::from_pretty(
834 " I
835 + 2"
836 )
837 );
838 assert!(proj.next().await.unwrap().unwrap().is_stop());
839 }
840
841 #[tokio::test]
842 async fn test_projection_reuses_finished_inflight_permit_across_watermark() {
843 let schema = Schema {
844 fields: vec![Field::unnamed(DataType::Int64)],
845 };
846 let (mut tx, source) = MockSource::channel();
847 let source = source.into_executor(schema, StreamKey::new());
848
849 let started_count = Arc::new(AtomicUsize::new(0));
850 let started_notify = Arc::new(Notify::new());
851 let test_expr = NonStrictExpression::for_test(
852 FirstProjectExprWaitsForStartedCount {
853 started_count: started_count.clone(),
854 started_notify: started_notify.clone(),
855 unblock_first_at_started_count: 3,
856 }
857 .boxed(),
858 );
859
860 let proj = ProjectExecutor::new(
861 actor_context_with_project_expr_limits(3, 2),
862 source,
863 vec![test_expr],
864 MultiMap::from_iter(vec![(0, 0)].into_iter()),
865 vec![],
866 false,
867 );
868 let mut proj = proj.boxed().execute();
869
870 tx.push_barrier(test_epoch(1), false);
871 proj.expect_barrier().await;
872
873 tx.push_chunk(StreamChunk::from_pretty(
874 " I
875 + 1",
876 ));
877 tx.push_chunk(StreamChunk::from_pretty(
878 " I
879 + 2",
880 ));
881 tx.push_int64_watermark(0, 100);
882 tx.push_chunk(StreamChunk::from_pretty(
883 " I
884 + 3",
885 ));
886 tx.push_barrier(test_epoch(2), true);
887
888 let first_msg = timeout(Duration::from_secs(5), async {
889 let next_msg = proj.next();
890 pin_mut!(next_msg);
891 let mut first_msg = None;
892 loop {
893 let notified = started_notify.notified();
894 if started_count.load(atomic::Ordering::SeqCst) >= 3 {
895 break;
896 }
897 tokio::select! {
898 _ = notified => {}
899 msg = &mut next_msg => {
900 if started_count.load(atomic::Ordering::SeqCst) < 3 {
901 panic!("project executor emitted before the post-watermark chunk started: {msg:?}");
902 }
903 first_msg = Some(msg.unwrap().unwrap());
904 break;
905 }
906 }
907 }
908 match first_msg {
909 Some(msg) => msg,
910 None => next_msg.await.unwrap().unwrap(),
911 }
912 })
913 .await
914 .expect("post-watermark chunk expression did not start");
915
916 assert_eq!(
917 *first_msg.as_chunk().unwrap(),
918 StreamChunk::from_pretty(
919 " I
920 + 0"
921 )
922 );
923 assert_eq!(
924 *proj.next().await.unwrap().unwrap().as_chunk().unwrap(),
925 StreamChunk::from_pretty(
926 " I
927 + 1"
928 )
929 );
930 assert_eq!(
931 proj.expect_watermark().await,
932 Watermark {
933 col_idx: 0,
934 data_type: DataType::Int64,
935 val: ScalarImpl::Int64(0)
936 }
937 );
938 assert_eq!(
939 *proj.next().await.unwrap().unwrap().as_chunk().unwrap(),
940 StreamChunk::from_pretty(
941 " I
942 + 2"
943 )
944 );
945 assert!(proj.next().await.unwrap().unwrap().is_stop());
946 }
947
948 static DUMMY_COUNTER: AtomicI64 = AtomicI64::new(0);
949
950 #[derive(Debug)]
951 struct DummyNondecreasingExpr;
952
953 impl ExpressionInfo for DummyNondecreasingExpr {
954 fn return_type(&self) -> DataType {
955 DataType::Int64
956 }
957 }
958
959 impl SyncExpression for DummyNondecreasingExpr {
960 fn eval_v2(&self, input: &DataChunk) -> expr::Result<ValueImpl> {
961 let value = DUMMY_COUNTER.fetch_add(1, atomic::Ordering::SeqCst);
962 Ok(ValueImpl::Scalar {
963 value: Some(value.into()),
964 capacity: input.capacity(),
965 })
966 }
967
968 fn eval_row(&self, _input: &OwnedRow) -> expr::Result<Datum> {
969 let value = DUMMY_COUNTER.fetch_add(1, atomic::Ordering::SeqCst);
970 Ok(Some(value.into()))
971 }
972 }
973
974 #[tokio::test]
975 async fn test_watermark_projection() {
976 let schema = Schema {
977 fields: vec![
978 Field::unnamed(DataType::Int64),
979 Field::unnamed(DataType::Int64),
980 ],
981 };
982 let (mut tx, source) = MockSource::channel();
983 let source = source.into_executor(schema, StreamKey::new());
984
985 let a_expr = build_from_pretty("(add:int8 $0:int8 1:int8)");
986 let b_expr = build_from_pretty("(subtract:int8 $0:int8 1:int8)");
987 let c_expr = NonStrictExpression::for_test(DummyNondecreasingExpr);
988
989 let proj = ProjectExecutor::new(
990 ActorContext::for_test(123),
991 source,
992 vec![a_expr, b_expr, c_expr],
993 MultiMap::from_iter(vec![(0, 0), (0, 1)].into_iter()),
994 vec![2],
995 false,
996 );
997 let mut proj = proj.boxed().execute();
998
999 tx.push_barrier(test_epoch(1), false);
1000 tx.push_int64_watermark(0, 100);
1001
1002 proj.expect_barrier().await;
1003 let w1 = proj.expect_watermark().await;
1004 let w2 = proj.expect_watermark().await;
1005 let (w1, w2) = if w1.col_idx < w2.col_idx {
1006 (w1, w2)
1007 } else {
1008 (w2, w1)
1009 };
1010
1011 assert_eq!(
1012 w1,
1013 Watermark {
1014 col_idx: 0,
1015 data_type: DataType::Int64,
1016 val: ScalarImpl::Int64(101)
1017 }
1018 );
1019 assert_eq!(
1020 w2,
1021 Watermark {
1022 col_idx: 1,
1023 data_type: DataType::Int64,
1024 val: ScalarImpl::Int64(99)
1025 }
1026 );
1027
1028 tx.push_chunk(StreamChunk::from_pretty(
1030 " I I
1031 + 120 4
1032 + 146 5
1033 + 133 6",
1034 ));
1035 proj.expect_chunk().await;
1036 tx.push_chunk(StreamChunk::from_pretty(
1037 " I I
1038 + 213 8
1039 - 133 6",
1040 ));
1041 proj.expect_chunk().await;
1042
1043 tx.push_barrier(test_epoch(2), false);
1044 let w3 = proj.expect_watermark().await;
1045 proj.expect_barrier().await;
1046
1047 tx.push_chunk(StreamChunk::from_pretty(
1048 " I I
1049 + 100 3
1050 + 104 5
1051 + 187 3",
1052 ));
1053 proj.expect_chunk().await;
1054
1055 tx.push_barrier(test_epoch(3), false);
1056 let w4 = proj.expect_watermark().await;
1057 proj.expect_barrier().await;
1058
1059 assert_eq!(w3.col_idx, w4.col_idx);
1060 assert!(w3.val.default_cmp(&w4.val).is_le());
1061
1062 tx.push_int64_watermark(1, 100);
1063 tx.push_barrier(test_epoch(4), true);
1064
1065 assert!(proj.next().await.unwrap().unwrap().is_stop());
1066 }
1067}