Skip to main content

risingwave_stream/executor/project/
project_scalar.rs

1// Copyright 2025 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
51/// `ProjectExecutor` project data with the `expr`. The `expr` takes a chunk of data,
52/// and returns a new data chunk. And then, `ProjectExecutor` will insert, delete
53/// or update element into next operator according to the result of the expression.
54pub struct ProjectExecutor {
55    input: Executor,
56    inner: Inner,
57}
58
59struct Inner {
60    actor_id: ActorId,
61
62    /// Expressions of the current projection.
63    exprs: Arc<Vec<NonStrictExpression>>,
64    /// All the watermark derivations, (`input_column_index`, `output_column_index`). And the
65    /// derivation expression is the project's expression itself.
66    watermark_derivations: MultiMap<usize, usize>,
67    /// Indices of nondecreasing expressions in the expression list.
68    nondecreasing_expr_indices: Vec<usize>,
69    /// Last seen values of nondecreasing expressions, buffered to periodically produce watermarks.
70    last_nondec_expr_values: Vec<Option<ScalarImpl>>,
71
72    /// Whether there are likely no-op updates in the output chunks, so that eliminating them with
73    /// `StreamChunk::eliminate_adjacent_noop_update` could be beneficial.
74    eliminate_noop_updates: bool,
75
76    /// Maximum number of chunks with pending projection evaluation.
77    project_expr_concurrency: usize,
78
79    /// Limits the number of chunks whose projection evaluation has started but not finished.
80    /// `None` means unlimited in-flight requests.
81    project_expr_inflight_request_concurrency: Option<Arc<Semaphore>>,
82
83    /// Number of messages waiting in the ordered projection window.
84    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                            // it's ok to use the first row here, just one chunk delay
234                            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        // just push some random chunks
1029        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}