Skip to main content

risingwave_stream/executor/over_window/
eowc.rs

1// Copyright 2023 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::collections::HashMap;
16use std::marker::PhantomData;
17use std::ops::Bound;
18
19use anyhow::Context;
20use futures::{TryStreamExt, stream};
21use itertools::Itertools;
22use risingwave_common::array::Op;
23use risingwave_common::array::stream_record::Record;
24use risingwave_common::row::RowExt;
25use risingwave_common::types::ToOwnedDatum;
26use risingwave_common::util::iter_util::ZipEqFast;
27use risingwave_common::util::memcmp_encoding::{self, MemcmpEncoded};
28use risingwave_common::util::row_serde::OrderedRowSerde;
29use risingwave_common::util::sort_util::OrderType;
30use risingwave_common::{must_match, row};
31use risingwave_common_estimate_size::EstimateSize;
32use risingwave_common_estimate_size::collections::EstimatedVecDeque;
33use risingwave_expr::window_function::{
34    Readiness, StateEvictHint, StateKey, WatermarkBound, WindowFuncCall, WindowStateSnapshot,
35    WindowStates, create_window_state,
36};
37use risingwave_pb::stream_plan::PbEowcOverWindowNodeVersion;
38use risingwave_pb::window_function::{
39    EowcPartitionProgress as PbEowcPartitionProgress, StateKey as PbStateKey,
40    WatermarkBound as PbWatermarkBound, WindowStateSnapshot as PbWindowStateSnapshot,
41};
42use risingwave_storage::store::PrefetchOptions;
43use tracing::debug;
44
45use super::watermark_trigger::{TriggerEntry, WatermarkTriggers};
46use crate::cache::ManagedLruCache;
47use crate::common::metrics::MetricsInfo;
48use crate::consistency::consistency_panic;
49use crate::executor::prelude::*;
50
51struct Partition {
52    states: WindowStates,
53    curr_row_buffer: EstimatedVecDeque<OwnedRow>,
54    /// Cached intermediate state row for this partition, used for upsert operations.
55    /// `None` means no prior row exists in the intermediate state table for this partition.
56    intermediate_state_row: Option<OwnedRow>,
57    /// Only maintained when windows may be closed by watermark.
58    progress: PartitionProgress,
59    /// Whether the intermediate states or the progress changed since persisted.
60    dirty: bool,
61}
62
63impl EstimateSize for Partition {
64    fn estimated_heap_size(&self) -> usize {
65        let mut total_size = self.curr_row_buffer.estimated_heap_size();
66        for state in self.states.iter() {
67            total_size += state.estimated_heap_size();
68        }
69        if let Some(row) = &self.intermediate_state_row {
70            total_size += row.estimated_heap_size();
71        }
72        total_size + self.progress.estimated_heap_size()
73    }
74}
75
76/// Output progress of a partition, persisted in the `progress` column of the intermediate state
77/// table.
78#[derive(Default, EstimateSize)]
79struct PartitionProgress {
80    /// The key of the last output row. Rows up to it may be retained for the windows of later rows,
81    /// and are skipped by it when reloading the partition, as whether a window was closed by
82    /// watermark can't be told from the rows.
83    last_output_key: Option<StateKey>,
84    /// The bound of the trigger registered in [`WatermarkTriggers`]. It's never later than the
85    /// watermark that closes the window of the next row to output. That watermark usually grows as
86    /// rows arrive, so the trigger is kept and checked again when it fires, and it's replaced only
87    /// when that watermark moves earlier.
88    trigger: Option<WatermarkBound>,
89}
90
91type PartitionCache = ManagedLruCache<MemcmpEncoded, Partition>; // TODO(rc): use `K: HashKey` as key like in hash agg?
92
93/// Number of partitions loaded concurrently.
94const LOAD_CONCURRENCY: usize = 10;
95
96fn encode_state_key(key: &StateKey, pk_ser: &OrderedRowSerde) -> PbStateKey {
97    PbStateKey {
98        order_key: key.order_key.to_vec(),
99        pk: key.pk.as_inner().memcmp_serialize(pk_ser),
100    }
101}
102
103fn decode_state_key(key: PbStateKey, pk_deser: &OrderedRowSerde) -> StreamExecutorResult<StateKey> {
104    let pk = pk_deser
105        .deserialize(&key.pk)
106        .context("failed to deserialize pk")?;
107    Ok(StateKey {
108        order_key: key.order_key.into(),
109        pk: pk.into(),
110    })
111}
112
113/// Encode a [`WindowStateSnapshot`] to bytes for persistence using protobuf.
114fn encode_snapshot(snapshot: &WindowStateSnapshot, pk_ser: &OrderedRowSerde) -> Vec<u8> {
115    use prost::Message;
116    let pb = PbWindowStateSnapshot {
117        last_output_key: snapshot
118            .last_output_key
119            .as_ref()
120            .map(|key| encode_state_key(key, pk_ser)),
121        function_state: Some(snapshot.function_state.clone()),
122    };
123    pb.encode_to_vec()
124}
125
126/// Decode a [`WindowStateSnapshot`] from bytes during recovery using protobuf.
127fn decode_snapshot(
128    bytes: &[u8],
129    pk_deser: &OrderedRowSerde,
130) -> StreamExecutorResult<WindowStateSnapshot> {
131    use prost::Message;
132    let pb = PbWindowStateSnapshot::decode(bytes).context("failed to decode snapshot")?;
133    let last_output_key = pb
134        .last_output_key
135        .map(|key| decode_state_key(key, pk_deser))
136        .transpose()?;
137    let function_state = pb
138        .function_state
139        .context("snapshot missing function_state")?;
140    Ok(WindowStateSnapshot {
141        last_output_key,
142        function_state,
143    })
144}
145
146/// Encode a [`PartitionProgress`] for persistence, or `None` if it's empty.
147fn encode_progress(progress: &PartitionProgress, pk_ser: &OrderedRowSerde) -> Option<Vec<u8>> {
148    use prost::Message;
149    if progress.last_output_key.is_none() && progress.trigger.is_none() {
150        return None;
151    }
152    let pb = PbEowcPartitionProgress {
153        last_output_key: progress
154            .last_output_key
155            .as_ref()
156            .map(|key| encode_state_key(key, pk_ser)),
157        trigger: progress.trigger.as_ref().map(|bound| PbWatermarkBound {
158            value: bound.value.to_vec(),
159            exclusive: bound.exclusive,
160        }),
161    };
162    Some(pb.encode_to_vec())
163}
164
165fn decode_progress(
166    bytes: &[u8],
167    pk_deser: &OrderedRowSerde,
168) -> StreamExecutorResult<PartitionProgress> {
169    use prost::Message;
170    let pb = PbEowcPartitionProgress::decode(bytes).context("failed to decode progress")?;
171    Ok(PartitionProgress {
172        last_output_key: pb
173            .last_output_key
174            .map(|key| decode_state_key(key, pk_deser))
175            .transpose()?,
176        trigger: pb.trigger.map(|bound| WatermarkBound {
177            value: bound.value.into(),
178            exclusive: bound.exclusive,
179        }),
180    })
181}
182
183/// [`EowcOverWindowExecutor`] consumes ordered input (on order key column with watermark in
184/// ascending order) and outputs window function results. One [`EowcOverWindowExecutor`] can handle
185/// one combination of partition key and order key.
186///
187/// The reason not to use [`SortBuffer`] is that the table schemas of [`EowcOverWindowExecutor`] and
188/// [`SortBuffer`] are different, since we don't have something like a _grouped_ sort buffer.
189///
190/// [`SortBuffer`]: crate::executor::eowc::SortBuffer
191///
192/// Basic idea:
193///
194/// ```text
195/// ──────────────┬────────────────────────────────────────────────────── curr evict row
196///               │ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING
197///        (1)    │ ─┬─
198///               │  │RANGE BETWEEN '1hr' PRECEDING AND '1hr' FOLLOWING
199///        ─┬─    │  │ ─┬─
200///   LAG(1)│        │  │
201/// ────────┴──┬─────┼──┼──────────────────────────────────────────────── curr output row
202///     LEAD(1)│     │  │GROUPS 1 PRECEDING AND 1 FOLLOWING
203///                  │
204///                  │ (2)
205/// ─────────────────┴─────────────────────────────────────────────────── curr input row
206/// (1): additional buffered input (unneeded) for some window
207/// (2): additional delay (already able to output) for some window
208/// ```
209///
210/// - State table schema = input schema, state table pk = `partition key | order key | input pk`.
211/// - Output schema = input schema + window function results.
212/// - Rows in range (`curr evict row`, `curr input row`] are in state table.
213/// - `curr evict row` <= min(last evict rows of all `WindowState`s).
214/// - `WindowState` should output agg result for `curr output row`.
215/// - Recover: iterate through state table, push rows to `WindowState`, and skip the rows that were
216///   output, see [`Self::skip_output_rows`].
217/// - Windows that may be closed by the watermark of the order key are closed by
218///   [`WatermarkTriggers`].
219pub struct EowcOverWindowExecutor<S: StateStore> {
220    input: Executor,
221    inner: ExecutorInner<S>,
222}
223
224struct ExecutorInner<S: StateStore> {
225    actor_ctx: ActorContextRef,
226
227    schema: Schema,
228    calls: Vec<WindowFuncCall>,
229    input_stream_key: Vec<usize>,
230    partition_key_indices: Vec<usize>,
231    order_key_index: usize, // no `OrderType` here, cuz we expect the input is ascending
232    state_table: StateTable<S>,
233    /// For each column of the state table, its index in
234    /// `partition key | order key | input stream key | NULL`, to rebuild the row of an evicted key.
235    evict_row_indices: Vec<usize>,
236    watermark_sequence: AtomicU64Ref,
237    /// Optional state table for persisting window function intermediate states.
238    /// See `StreamEowcOverWindow::infer_intermediate_state_table` for schema definition.
239    intermediate_state_table: Option<StateTable<S>>,
240    /// Serde for input stream key (pk), used for encoding/decoding `StateKey` in the intermediate
241    /// state table. Only initialized when `intermediate_state_table` is present.
242    pk_serde: Option<OrderedRowSerde>,
243    /// Whether windows are judged exactly by [`WindowStates::readiness`], rather than the
244    /// conservative [`WindowStates::are_ready`], see `EowcOverWindowNodeVersion`.
245    exact_readiness: bool,
246    /// Present iff windows may be closed by watermark, in which case `intermediate_state_table` has a
247    /// trailing `progress` column.
248    triggers: Option<WatermarkTriggers<S>>,
249    chunk_size: usize,
250}
251
252impl<S: StateStore> ExecutorInner<S> {
253    fn encode_partition_key(&self, partition_key: impl Row) -> StreamExecutorResult<MemcmpEncoded> {
254        Ok(memcmp_encoding::encode_row(
255            partition_key,
256            &vec![OrderType::ascending(); self.partition_key_indices.len()],
257        )?)
258    }
259}
260
261struct ExecutionVars<S: StateStore> {
262    partitions: PartitionCache,
263    _phantom: PhantomData<S>,
264}
265
266impl<S: StateStore> Execute for EowcOverWindowExecutor<S> {
267    fn execute(self: Box<Self>) -> BoxedMessageStream {
268        self.executor_inner().boxed()
269    }
270}
271
272pub struct EowcOverWindowExecutorArgs<S: StateStore> {
273    pub actor_ctx: ActorContextRef,
274
275    pub input: Executor,
276
277    pub schema: Schema,
278    pub calls: Vec<WindowFuncCall>,
279    pub partition_key_indices: Vec<usize>,
280    pub order_key_index: usize,
281    pub state_table: StateTable<S>,
282    pub watermark_epoch: AtomicU64Ref,
283    /// Optional state table for persisting window function intermediate states.
284    /// See `StreamEowcOverWindow::infer_intermediate_state_table` for schema definition.
285    pub intermediate_state_table: Option<StateTable<S>>,
286    /// See `StreamEowcOverWindow::infer_watermark_trigger_table` for schema definition.
287    pub watermark_trigger_table: Option<StateTable<S>>,
288    pub version: PbEowcOverWindowNodeVersion,
289    pub chunk_size: usize,
290}
291
292impl<S: StateStore> EowcOverWindowExecutor<S> {
293    pub fn new(args: EowcOverWindowExecutorArgs<S>) -> Self {
294        let input_info = args.input.info().clone();
295
296        // Evictions rebuild state table rows from these columns only.
297        debug_assert!(
298            args.state_table.pk_indices().iter().all(|i| {
299                args.partition_key_indices.contains(i)
300                    || *i == args.order_key_index
301                    || input_info.stream_key.contains(i)
302            }),
303            "state table pk must consist of partition key, order key and input stream key columns"
304        );
305        let num_partition_key_cols = args.partition_key_indices.len();
306        let evict_row_indices = (0..input_info.schema.len())
307            .map(|i| {
308                if let Some(j) = args.partition_key_indices.iter().position(|&k| k == i) {
309                    j
310                } else if i == args.order_key_index {
311                    num_partition_key_cols
312                } else if let Some(j) = input_info.stream_key.iter().position(|&k| k == i) {
313                    num_partition_key_cols + 1 + j
314                } else {
315                    // NULL
316                    num_partition_key_cols + 1 + input_info.stream_key.len()
317                }
318            })
319            .collect_vec();
320
321        // Build pk_serde if intermediate_state_table is present
322        let pk_serde = args.intermediate_state_table.as_ref().map(|_| {
323            let pk_data_types: Vec<_> = input_info
324                .stream_key
325                .iter()
326                .map(|&i| args.schema[i].data_type())
327                .collect();
328            let pk_order_types: Vec<_> = input_info
329                .stream_key
330                .iter()
331                .map(|_| OrderType::ascending())
332                .collect();
333            OrderedRowSerde::new(pk_data_types, pk_order_types)
334        });
335
336        let exact_readiness = args.version >= PbEowcOverWindowNodeVersion::Issue17119;
337        let triggers = args.watermark_trigger_table.map(|table| {
338            assert!(
339                exact_readiness && args.intermediate_state_table.is_some(),
340                "watermark trigger table is planned with the exact readiness and the progress column"
341            );
342            WatermarkTriggers::new(table, args.schema[args.order_key_index].data_type())
343        });
344
345        Self {
346            input: args.input,
347            inner: ExecutorInner {
348                actor_ctx: args.actor_ctx,
349                schema: args.schema,
350                calls: args.calls,
351                input_stream_key: input_info.stream_key,
352                partition_key_indices: args.partition_key_indices,
353                order_key_index: args.order_key_index,
354                state_table: args.state_table,
355                evict_row_indices,
356                watermark_sequence: args.watermark_epoch,
357                intermediate_state_table: args.intermediate_state_table,
358                pk_serde,
359                exact_readiness,
360                triggers,
361                chunk_size: args.chunk_size,
362            },
363        }
364    }
365
366    /// Restore intermediate state snapshots and the output progress of the partition from its row
367    /// in the intermediate state table.
368    fn restore_intermediate_state(
369        this: &ExecutorInner<S>,
370        partition: &mut Partition,
371        partition_key: &OwnedRow,
372        row: Option<OwnedRow>,
373    ) -> StreamExecutorResult<()> {
374        if this.intermediate_state_table.is_none() {
375            return Ok(());
376        }
377        let pk_serde = this
378            .pk_serde
379            .as_ref()
380            .expect("pk_serde must be set when intermediate_state_table is present");
381
382        for state in partition.states.iter_mut() {
383            state.enable_persistence();
384        }
385
386        if let Some(row) = row {
387            let num_partition_key_cols = this.partition_key_indices.len();
388            let num_calls = this.calls.len();
389
390            for call_index in 0..num_calls {
391                let state_col = num_partition_key_cols + call_index;
392                if state_col < row.len() {
393                    if let Some(state_bytes) = row.datum_at(state_col) {
394                        let snapshot = decode_snapshot(state_bytes.into_bytea(), pk_serde)?;
395                        debug!(
396                            "Restoring intermediate state for partition {:?}, call_index {}, has_last_key: {}",
397                            partition_key,
398                            call_index,
399                            snapshot.last_output_key.is_some()
400                        );
401                        partition
402                            .states
403                            .get_mut(call_index)
404                            .unwrap()
405                            .restore(snapshot)?;
406                    }
407                } else {
408                    return Err(anyhow::anyhow!(
409                        "intermediate state row has fewer columns ({}) than expected ({}) \
410                        at call_index {}, state may be corrupted",
411                        row.len(),
412                        num_partition_key_cols + num_calls,
413                        call_index
414                    )
415                    .into());
416                }
417            }
418            if this.triggers.is_some()
419                && let Some(progress_bytes) = row.datum_at(num_partition_key_cols + num_calls)
420            {
421                partition.progress = decode_progress(progress_bytes.into_bytea(), pk_serde)?;
422            }
423            partition.intermediate_state_row = Some(row);
424        }
425        Ok(())
426    }
427
428    /// Persist intermediate state snapshots and the output progress to the state table, if changed.
429    fn persist_partition(
430        this: &mut ExecutorInner<S>,
431        partition: &mut Partition,
432        partition_key: &OwnedRow,
433    ) {
434        if !std::mem::take(&mut partition.dirty) {
435            return;
436        }
437        let Some(intermediate_state_table) = &mut this.intermediate_state_table else {
438            return;
439        };
440        let pk_serde = this
441            .pk_serde
442            .as_ref()
443            .expect("pk_serde must be set when intermediate_state_table is present");
444
445        let num_calls = partition.states.len();
446        let num_partition_key_cols = partition_key.len();
447
448        // Build the new row: partition_key columns + state_0..state_{n-1} [+ progress]
449        let mut new_row_values = Vec::with_capacity(num_partition_key_cols + num_calls + 1);
450        for datum in partition_key.iter() {
451            new_row_values.push(datum.to_owned_datum());
452        }
453
454        // For each call, encode snapshot or preserve previous value
455        for (call_index, state) in partition.states.iter().enumerate() {
456            if let Some(snapshot) = state.snapshot() {
457                let snapshot_bytes = encode_snapshot(&snapshot, pk_serde);
458                new_row_values.push(Some(snapshot_bytes.into_boxed_slice().into()));
459            } else if let Some(ref old_row) = partition.intermediate_state_row {
460                let state_col = num_partition_key_cols + call_index;
461                if state_col < old_row.len() {
462                    new_row_values.push(old_row.datum_at(state_col).to_owned_datum());
463                } else {
464                    new_row_values.push(None);
465                }
466            } else {
467                new_row_values.push(None);
468            }
469        }
470        if this.triggers.is_some() {
471            if partition.states.is_empty() {
472                // No retained row to skip when reloading the partition.
473                partition.progress.last_output_key = None;
474            }
475            new_row_values.push(
476                encode_progress(&partition.progress, pk_serde)
477                    .map(|bytes| bytes.into_boxed_slice().into()),
478            );
479        }
480        let new_row = OwnedRow::new(new_row_values);
481        let is_empty = new_row
482            .iter()
483            .skip(num_partition_key_cols)
484            .all(|datum| datum.is_none());
485
486        partition.intermediate_state_row = match partition.intermediate_state_row.take() {
487            Some(old_row) if is_empty => {
488                intermediate_state_table.delete(old_row);
489                None
490            }
491            Some(old_row) => {
492                intermediate_state_table.update(old_row, new_row.clone());
493                Some(new_row)
494            }
495            None if is_empty => None,
496            None => {
497                intermediate_state_table.insert(new_row.clone());
498                Some(new_row)
499            }
500        };
501    }
502
503    /// Load the partition from the state tables. The rows that were output are skipped by
504    /// [`Self::skip_output_rows`] afterwards.
505    async fn load_partition(
506        this: &ExecutorInner<S>,
507        partition_key: &OwnedRow,
508    ) -> StreamExecutorResult<Partition> {
509        let mut partition = Partition {
510            states: WindowStates::new(this.calls.iter().map(create_window_state).try_collect()?),
511            curr_row_buffer: Default::default(),
512            intermediate_state_row: None,
513            progress: Default::default(),
514            dirty: false,
515        };
516
517        let sub_range: &(Bound<OwnedRow>, Bound<OwnedRow>) = &(Bound::Unbounded, Bound::Unbounded);
518        let (intermediate_state_row, table_iter) = futures::try_join!(
519            async {
520                match &this.intermediate_state_table {
521                    Some(table) => table.get_row(partition_key).await,
522                    None => Ok(None),
523                }
524            },
525            this.state_table
526                .iter_with_prefix(partition_key, sub_range, PrefetchOptions::default()),
527        )?;
528
529        // If intermediate state table exists, restore intermediate state snapshots
530        Self::restore_intermediate_state(
531            this,
532            &mut partition,
533            partition_key,
534            intermediate_state_row,
535        )?;
536
537        // Recover states from state table.
538        #[for_await]
539        for keyed_row in table_iter {
540            let row = keyed_row?.into_owned_row();
541            let order_key_enc = memcmp_encoding::encode_row(
542                row::once(Some(
543                    row.datum_at(this.order_key_index)
544                        .expect("order key column must be non-NULL")
545                        .into_scalar_impl(),
546                )),
547                &[OrderType::ascending()],
548            )?;
549            let pk = (&row).project(&this.input_stream_key).into_owned_row();
550            let key = StateKey {
551                order_key: order_key_enc,
552                pk: pk.into(),
553            };
554            for (call, state) in this.calls.iter().zip_eq_fast(partition.states.iter_mut()) {
555                state.append(
556                    key.clone(),
557                    (&row)
558                        .project(call.args.val_indices())
559                        .into_owned_row()
560                        .as_inner()
561                        .into(),
562                );
563            }
564            partition.curr_row_buffer.push_back(row);
565        }
566
567        // Ensure states correctness.
568        assert!(partition.states.are_aligned());
569
570        Ok(partition)
571    }
572
573    /// Skip the rows of a loaded partition that were output. The rows not needed any more are
574    /// evicted, as they were retained when output without the later rows.
575    fn skip_output_rows(
576        this: &mut ExecutorInner<S>,
577        partition: &mut Partition,
578        partition_key: &OwnedRow,
579    ) -> StreamExecutorResult<()> {
580        if !this.exact_readiness {
581            // Ignore ready windows (all ready windows were outputted before).
582            // Use just_slide which calls slide_no_output and respects recovery skip logic.
583            while partition.states.are_ready() {
584                partition.states.just_slide()?;
585                partition.curr_row_buffer.pop_front();
586            }
587            return Ok(());
588        }
589
590        while let Some(key) = partition.states.curr_key() {
591            let output = if this.triggers.is_some() {
592                // Whether a window was closed by watermark can't be told from the rows.
593                partition
594                    .progress
595                    .last_output_key
596                    .as_ref()
597                    .is_some_and(|last_output_key| key <= last_output_key)
598            } else {
599                // Rows are retained in order, so a window closed by the rows is still closed by the
600                // retained rows.
601                partition.states.readiness().is_ready(None)
602            };
603            if !output {
604                break;
605            }
606            let evict_hint = partition.states.slide_no_output()?;
607            partition.curr_row_buffer.pop_front();
608            Self::evict(this, partition_key, evict_hint)?;
609        }
610        Ok(())
611    }
612
613    async fn ensure_partition_in_cache(
614        this: &mut ExecutorInner<S>,
615        cache: &mut PartitionCache,
616        encoded_partition_key: &MemcmpEncoded,
617        partition_key: &OwnedRow,
618    ) -> StreamExecutorResult<()> {
619        if cache.contains(encoded_partition_key) {
620            return Ok(());
621        }
622        Self::ensure_partitions_in_cache(
623            this,
624            cache,
625            &[(encoded_partition_key.clone(), partition_key)],
626        )
627        .await
628    }
629
630    /// Load the partitions not in the cache concurrently, and put them into the cache.
631    async fn ensure_partitions_in_cache(
632        this: &mut ExecutorInner<S>,
633        cache: &mut PartitionCache,
634        partitions: &[(MemcmpEncoded, &OwnedRow)],
635    ) -> StreamExecutorResult<()> {
636        let loaded: Vec<_> = {
637            let this = &*this;
638            stream::iter(
639                partitions
640                    .iter()
641                    .filter(|(encoded_partition_key, _)| !cache.contains(encoded_partition_key))
642                    .unique_by(|(encoded_partition_key, _)| encoded_partition_key.clone())
643                    .map(|(encoded_partition_key, partition_key)| async move {
644                        let partition = Self::load_partition(this, partition_key).await?;
645                        Ok::<_, StreamExecutorError>((
646                            encoded_partition_key,
647                            partition_key,
648                            partition,
649                        ))
650                    }),
651            )
652            .buffer_unordered(LOAD_CONCURRENCY)
653            .try_collect()
654            .await?
655        };
656        for (encoded_partition_key, partition_key, mut partition) in loaded {
657            Self::skip_output_rows(this, &mut partition, partition_key)?;
658            cache.put(encoded_partition_key.clone(), partition);
659        }
660        Ok(())
661    }
662
663    /// Rebuild a partition whose states are empty, as if it's reloaded. The window states may hold
664    /// memory for the closed windows that their estimated sizes don't count.
665    fn reset_partition(
666        this: &ExecutorInner<S>,
667        partition: &mut Partition,
668    ) -> StreamExecutorResult<()> {
669        let mut states =
670            WindowStates::new(this.calls.iter().map(create_window_state).try_collect()?);
671        for (state, old_state) in states.iter_mut().zip_eq_fast(partition.states.iter()) {
672            if this.intermediate_state_table.is_some() {
673                state.enable_persistence();
674            }
675            if let Some(snapshot) = old_state.snapshot() {
676                state.restore(snapshot)?;
677            }
678        }
679        partition.states = states;
680        partition.curr_row_buffer = Default::default();
681        Ok(())
682    }
683
684    /// Delete the evicted rows from the state table.
685    fn evict(
686        this: &mut ExecutorInner<S>,
687        partition_key: &OwnedRow,
688        evict_hint: StateEvictHint,
689    ) -> StreamExecutorResult<()> {
690        let StateEvictHint::CanEvict(keys_to_evict) = evict_hint else {
691            return Ok(());
692        };
693        for key in keys_to_evict {
694            let order_key = memcmp_encoding::decode_row(
695                &key.order_key,
696                &[this.schema[this.order_key_index].data_type()],
697                &[OrderType::ascending()],
698            )?;
699            // FIXME(rc): quite hacky here, we may need `state_table.delete_by_pk`
700            let state_row = partition_key
701                .chain(&order_key)
702                .chain(key.pk.as_inner())
703                .chain(row::once(Datum::None))
704                .project(&this.evict_row_indices);
705            // NOTE: We don't know the value of the row here, so the table must allow
706            // inconsistent ops.
707            this.state_table.delete(state_row);
708        }
709        Ok(())
710    }
711
712    /// Output the rows of the partition whose windows are closed, under the current watermark of the
713    /// order key if windows may be closed by watermark. Returns the readiness of the next row.
714    fn output_ready_rows(
715        this: &mut ExecutorInner<S>,
716        partition: &mut Partition,
717        partition_key: &OwnedRow,
718        chunk_builder: &mut StreamChunkBuilder,
719        chunks: &mut Vec<StreamChunk>,
720    ) -> StreamExecutorResult<Readiness> {
721        loop {
722            let readiness = if this.exact_readiness {
723                partition.states.readiness()
724            } else if partition.states.are_ready() {
725                Readiness::Ready
726            } else {
727                Readiness::WaitForInput
728            };
729            let watermark = this
730                .triggers
731                .as_ref()
732                .and_then(|triggers| triggers.watermark());
733            if !readiness.is_ready(watermark) {
734                return Ok(readiness);
735            }
736            if this.triggers.is_some() {
737                partition.progress.last_output_key = partition.states.curr_key().cloned();
738            }
739            partition.dirty = true;
740
741            // Get all outputs.
742            let (ret_values, evict_hint) = partition.states.slide()?;
743            let curr_row = partition
744                .curr_row_buffer
745                .pop_front()
746                .expect("ready window must have corresponding current row");
747            if let Some(chunk) =
748                chunk_builder.append_row(Op::Insert, (&curr_row).chain(OwnedRow::new(ret_values)))
749            {
750                chunks.push(chunk);
751            }
752
753            Self::evict(this, partition_key, evict_hint)?;
754        }
755    }
756
757    /// Register a trigger for the partition if the window of its next row to output, whose
758    /// `readiness` is given, may be closed by watermark, unless one no later than its bound is
759    /// registered.
760    fn register_trigger(
761        this: &mut ExecutorInner<S>,
762        partition: &mut Partition,
763        partition_key: &OwnedRow,
764        readiness: Readiness,
765    ) {
766        let Some(triggers) = &mut this.triggers else {
767            return;
768        };
769        if let Some(trigger) = partition.progress.trigger.clone() {
770            if triggers
771                .watermark()
772                .is_some_and(|watermark| trigger.is_satisfied_by(watermark))
773            {
774                // The triggers satisfied by the watermark have all fired, unless the trigger table
775                // lost this one.
776                consistency_panic!(
777                    ?partition_key,
778                    ?trigger,
779                    "watermark trigger of the partition is lost"
780                );
781            } else if let Readiness::OnWatermark(bound) = &readiness
782                && bound < &trigger
783            {
784                // Adding a calendar gap to a later order value may give an earlier end: a month
785                // after `01-31 00:00` is `02-28 00:00`, before the `02-28 23:00` of `01-30 23:00`.
786                triggers.remove(&TriggerEntry {
787                    bound: trigger,
788                    partition_key: partition_key.clone(),
789                });
790            } else {
791                return;
792            }
793            partition.progress.trigger = None;
794            partition.dirty = true;
795        }
796        let Readiness::OnWatermark(bound) = readiness else {
797            return;
798        };
799        // Bounds are not less than the order key of the last row either, so they're never below
800        // the watermark of the trigger table, under which rows are hidden from reads.
801        assert!(
802            !triggers
803                .watermark()
804                .is_some_and(|watermark| bound.is_satisfied_by(watermark)),
805            "rows closed by the current watermark must have been output"
806        );
807        triggers.register(TriggerEntry {
808            bound: bound.clone(),
809            partition_key: partition_key.clone(),
810        });
811        partition.progress.trigger = Some(bound);
812        partition.dirty = true;
813    }
814
815    async fn apply_chunk(
816        this: &mut ExecutorInner<S>,
817        vars: &mut ExecutionVars<S>,
818        chunk: StreamChunk,
819    ) -> StreamExecutorResult<Vec<StreamChunk>> {
820        let mut chunk_builder = StreamChunkBuilder::new(this.chunk_size, this.schema.data_types());
821        let mut chunks = vec![];
822        // Track partitions to persist, so we persist only once per partition at the end.
823        let mut dirty_partitions: HashMap<MemcmpEncoded, OwnedRow> = HashMap::new();
824
825        // We assume that the input is sorted by order key.
826        for record in chunk.records() {
827            let input_row = must_match!(record, Record::Insert { new_row } => new_row);
828
829            let partition_key = input_row
830                .project(&this.partition_key_indices)
831                .into_owned_row();
832            let encoded_partition_key = this.encode_partition_key(&partition_key)?;
833
834            // Get the partition.
835            Self::ensure_partition_in_cache(
836                this,
837                &mut vars.partitions,
838                &encoded_partition_key,
839                &partition_key,
840            )
841            .await?;
842            let partition: &mut Partition =
843                &mut vars.partitions.get_mut(&encoded_partition_key).unwrap();
844
845            // Materialize input to state table.
846            this.state_table.insert(input_row);
847
848            // Feed the row to all window states.
849            let order_key_enc = memcmp_encoding::encode_row(
850                row::once(Some(
851                    input_row
852                        .datum_at(this.order_key_index)
853                        .expect("order key column must be non-NULL")
854                        .into_scalar_impl(),
855                )),
856                &[OrderType::ascending()],
857            )?;
858            let pk = input_row.project(&this.input_stream_key).into_owned_row();
859            let key = StateKey {
860                order_key: order_key_enc,
861                pk: pk.into(),
862            };
863            for (call, state) in this.calls.iter().zip_eq_fast(partition.states.iter_mut()) {
864                state.append(
865                    key.clone(),
866                    input_row
867                        .project(call.args.val_indices())
868                        .into_owned_row()
869                        .as_inner()
870                        .into(),
871                );
872            }
873            partition
874                .curr_row_buffer
875                .push_back(input_row.into_owned_row());
876
877            let readiness = Self::output_ready_rows(
878                this,
879                partition,
880                &partition_key,
881                &mut chunk_builder,
882                &mut chunks,
883            )?;
884            Self::register_trigger(this, partition, &partition_key, readiness);
885
886            if this.intermediate_state_table.is_some() && partition.dirty {
887                dirty_partitions
888                    .entry(encoded_partition_key)
889                    .or_insert(partition_key);
890            }
891        }
892
893        for (encoded_partition_key, partition_key) in &dirty_partitions {
894            let partition = &mut *vars.partitions.get_mut(encoded_partition_key).unwrap();
895            Self::persist_partition(this, partition, partition_key);
896        }
897
898        chunks.extend(chunk_builder.take());
899        Ok(chunks)
900    }
901
902    /// Output the rows whose windows are closed by the new watermark of the order key.
903    #[try_stream(ok = StreamChunk, error = StreamExecutorError)]
904    async fn apply_watermark<'a>(
905        this: &'a mut ExecutorInner<S>,
906        vars: &'a mut ExecutionVars<S>,
907        watermark: ScalarImpl,
908    ) {
909        let Some(triggers) = &mut this.triggers else {
910            return Ok(());
911        };
912        if !triggers.advance_watermark(watermark)? {
913            return Ok(());
914        }
915
916        let mut chunk_builder = StreamChunkBuilder::new(this.chunk_size, this.schema.data_types());
917        // Handle the fired triggers a chunk at a time, after which the partitions can be evicted and
918        // the tables flushed, so the memory doesn't grow with the number of partitions closed.
919        loop {
920            let entries = (this.triggers.as_mut().unwrap())
921                .pop_due(this.chunk_size)
922                .await?;
923            if entries.is_empty() {
924                break;
925            }
926            let partitions: Vec<_> = entries
927                .iter()
928                .map(|entry| {
929                    Ok::<_, StreamExecutorError>((
930                        this.encode_partition_key(&entry.partition_key)?,
931                        &entry.partition_key,
932                    ))
933                })
934                .try_collect()?;
935            Self::ensure_partitions_in_cache(this, &mut vars.partitions, &partitions).await?;
936
937            for (entry, (encoded_partition_key, _)) in entries.iter().zip_eq_fast(&partitions) {
938                let mut chunks = vec![];
939                {
940                    let partition = &mut *vars
941                        .partitions
942                        .get_mut(encoded_partition_key)
943                        .expect("partitions of the chunk must be in the cache");
944                    Self::fire_trigger(this, partition, entry, &mut chunk_builder, &mut chunks)?;
945                }
946                for chunk in chunks {
947                    yield chunk;
948                }
949            }
950            Self::try_flush(this).await?;
951            vars.partitions.evict();
952        }
953
954        if let Some(chunk) = chunk_builder.take() {
955            yield chunk;
956        }
957        (this.triggers.as_mut().unwrap()).clean();
958    }
959
960    /// Handle the partition whose trigger `entry` is satisfied by the watermark: output the rows
961    /// whose windows are closed, and register a trigger again if needed.
962    fn fire_trigger(
963        this: &mut ExecutorInner<S>,
964        partition: &mut Partition,
965        entry: &TriggerEntry,
966        chunk_builder: &mut StreamChunkBuilder,
967        chunks: &mut Vec<StreamChunk>,
968    ) -> StreamExecutorResult<()> {
969        let trigger = partition.progress.trigger.take();
970        if trigger.as_ref() != Some(&entry.bound) {
971            consistency_panic!(
972                partition_key = ?entry.partition_key,
973                bound = ?entry.bound,
974                progress_trigger = ?trigger,
975                "watermark trigger table diverges from the partition progress"
976            );
977            // A later trigger is still in the table, which fires then.
978            if let Some(trigger) = trigger
979                && trigger > entry.bound
980            {
981                partition.progress.trigger = Some(trigger);
982            }
983        }
984        partition.dirty = true;
985
986        let readiness =
987            Self::output_ready_rows(this, partition, &entry.partition_key, chunk_builder, chunks)?;
988        Self::register_trigger(this, partition, &entry.partition_key, readiness);
989        Self::persist_partition(this, partition, &entry.partition_key);
990        if partition.states.is_empty() {
991            Self::reset_partition(this, partition)?;
992        }
993        Ok(())
994    }
995
996    async fn try_flush(this: &mut ExecutorInner<S>) -> StreamExecutorResult<()> {
997        this.state_table.try_flush().await?;
998        if let Some(intermediate_state_table) = &mut this.intermediate_state_table {
999            intermediate_state_table.try_flush().await?;
1000        }
1001        if let Some(triggers) = &mut this.triggers {
1002            triggers.try_flush().await?;
1003        }
1004        Ok(())
1005    }
1006
1007    #[try_stream(ok = Message, error = StreamExecutorError)]
1008    async fn executor_inner(self) {
1009        let EowcOverWindowExecutor {
1010            input,
1011            inner: mut this,
1012        } = self;
1013
1014        let metrics_info = MetricsInfo::new(
1015            this.actor_ctx.streaming_metrics.clone(),
1016            this.state_table.table_id(),
1017            this.actor_ctx.id,
1018            "EowcOverWindow",
1019        );
1020
1021        let mut vars = ExecutionVars {
1022            partitions: ManagedLruCache::unbounded(this.watermark_sequence.clone(), metrics_info),
1023            _phantom: PhantomData::<S>,
1024        };
1025
1026        let mut input = input.execute();
1027        let barrier = expect_first_barrier(&mut input).await?;
1028        let first_epoch = barrier.epoch;
1029        yield Message::Barrier(barrier);
1030        this.state_table.init_epoch(first_epoch).await?;
1031        if let Some(intermediate_state_table) = &mut this.intermediate_state_table {
1032            intermediate_state_table.init_epoch(first_epoch).await?;
1033        }
1034        if let Some(triggers) = &mut this.triggers {
1035            triggers.init_epoch(first_epoch).await?;
1036        }
1037
1038        #[for_await]
1039        for msg in input {
1040            let msg = msg?;
1041            match msg {
1042                // Watermarks are not forwarded, see `StreamEowcOverWindow`.
1043                Message::Watermark(watermark) => {
1044                    if watermark.col_idx == this.order_key_index {
1045                        #[for_await]
1046                        for chunk in Self::apply_watermark(&mut this, &mut vars, watermark.val) {
1047                            yield Message::Chunk(chunk?);
1048                        }
1049                    }
1050                }
1051                Message::Chunk(chunk) => {
1052                    for chunk in Self::apply_chunk(&mut this, &mut vars, chunk).await? {
1053                        yield Message::Chunk(chunk);
1054                    }
1055                    Self::try_flush(&mut this).await?;
1056                }
1057                Message::Barrier(barrier) => {
1058                    let post_commit = this.state_table.commit(barrier.epoch).await?;
1059                    let intermediate_post_commit = if let Some(intermediate_state_table) =
1060                        &mut this.intermediate_state_table
1061                    {
1062                        Some(intermediate_state_table.commit(barrier.epoch).await?)
1063                    } else {
1064                        None
1065                    };
1066                    let triggers_post_commit = if let Some(triggers) = &mut this.triggers {
1067                        Some(triggers.commit(barrier.epoch).await?)
1068                    } else {
1069                        None
1070                    };
1071
1072                    vars.partitions.evict();
1073
1074                    let update_vnode_bitmap = barrier.as_update_vnode_bitmap(this.actor_ctx.id);
1075                    yield Message::Barrier(barrier);
1076
1077                    let mut cache_may_stale = false;
1078                    if let Some((_, stale)) = post_commit
1079                        .post_yield_barrier(update_vnode_bitmap.clone())
1080                        .await?
1081                    {
1082                        cache_may_stale = cache_may_stale || stale;
1083                    }
1084                    if let Some(intermediate_post_commit) = intermediate_post_commit
1085                        && let Some((_, stale)) = intermediate_post_commit
1086                            .post_yield_barrier(update_vnode_bitmap.clone())
1087                            .await?
1088                    {
1089                        cache_may_stale = cache_may_stale || stale;
1090                    }
1091                    if let Some(triggers_post_commit) = triggers_post_commit {
1092                        triggers_post_commit
1093                            .post_yield_barrier(update_vnode_bitmap)
1094                            .await?;
1095                    }
1096                    if cache_may_stale {
1097                        vars.partitions.clear();
1098                    }
1099                }
1100            }
1101        }
1102    }
1103}