Skip to main content

risingwave_stream/executor/over_window/
general.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::{BTreeMap, HashSet, btree_map};
16use std::marker::PhantomData;
17use std::ops::RangeInclusive;
18
19use delta_btree_map::Change;
20use itertools::Itertools;
21use risingwave_common::array::stream_record::Record;
22use risingwave_common::config::streaming::OverWindowCachePolicy as CachePolicy;
23use risingwave_common::row::RowExt;
24use risingwave_common::types::{DefaultOrd, DefaultOrdered, ScalarImpl};
25use risingwave_common::util::memcmp_encoding::{self, MemcmpEncoded};
26use risingwave_common::util::sort_util::OrderType;
27use risingwave_expr::window_function::{
28    RangeFrameBounds, RowsFrameBounds, StateKey, WindowFuncCall,
29};
30
31use super::frame_finder::merge_rows_frames;
32use super::over_partition::{OverPartition, PartitionDelta};
33use super::range_cache::{CacheKey, PartitionCache};
34use crate::cache::ManagedLruCache;
35use crate::common::change_buffer::ChangeBuffer;
36use crate::common::metrics::MetricsInfo;
37use crate::consistency::consistency_panic;
38use crate::executor::monitor::OverWindowMetrics;
39use crate::executor::prelude::*;
40
41/// [`OverWindowExecutor`] consumes retractable input stream and produces window function outputs.
42/// One [`OverWindowExecutor`] can handle one combination of partition key and order key.
43///
44/// - State table schema = output schema, state table pk = `partition key | order key | input pk`.
45/// - Output schema = input schema + window function results.
46/// - When [`StateCleaning`] is enabled, stale rows below the watermark of the first order key
47///   column are deleted from recently touched partitions at barriers.
48pub struct OverWindowExecutor<S: StateStore> {
49    input: Executor,
50    inner: ExecutorInner<S>,
51}
52
53struct ExecutorInner<S: StateStore> {
54    actor_ctx: ActorContextRef,
55
56    schema: Schema,
57    calls: Calls,
58    deduped_part_key_indices: Vec<usize>,
59    order_key_indices: Vec<usize>,
60    order_key_data_types: Vec<DataType>,
61    order_key_order_types: Vec<OrderType>,
62    input_stream_key: Vec<usize>,
63    state_key_to_table_sub_pk_proj: Vec<usize>,
64
65    state_table: StateTable<S>,
66    watermark_sequence: AtomicU64Ref,
67
68    /// The maximum size of the chunk produced by executor at a time.
69    chunk_size: usize,
70    cache_policy: CachePolicy,
71    /// Watermark-driven state cleaning strategy, `None` if disabled.
72    state_cleaning: Option<StateCleaning>,
73}
74
75struct ExecutionVars<S: StateStore> {
76    /// partition key => partition range cache.
77    cached_partitions: ManagedLruCache<OwnedRow, PartitionCache>,
78    /// partition key => recently accessed range.
79    recently_accessed_ranges: BTreeMap<DefaultOrdered<OwnedRow>, RangeInclusive<StateKey>>,
80    /// The latest watermark received on the watermark column for state cleaning.
81    cleaning_watermark: Option<ScalarImpl>,
82    /// Partitions touched since the last barrier, which need state cleaning at the next barrier.
83    touched_partitions: HashSet<OwnedRow>,
84    stats: ExecutionStats,
85    _phantom: PhantomData<S>,
86}
87
88/// Watermark-driven state cleaning strategy of [`OverWindowExecutor`].
89///
90/// This is only enabled when the optimizer decides it's safe, i.e., when the input is append-only,
91/// all window frames are bounded `ROWS` frames, and the first order key column is a watermark
92/// column with NULLs ordered as the largest values.
93///
94/// Under these conditions, a row can only affect (and be affected by) a bounded number of
95/// neighboring rows in the partition. Once a watermark `wm` is received, no row with order key
96/// `< wm` will ever arrive, so rows with order key `< wm` (*stale rows*) can never get new
97/// neighbors on the "smaller" side. Therefore, among the stale rows, only the `n_retain` ones that
98/// are closest to the watermark boundary can still be involved in future computation (as frame
99/// members of, or as rows affected by, rows arriving in the future), and all the others can be
100/// safely deleted from the state table.
101///
102/// The cleaning happens at barriers for partitions touched since the last barrier. This keeps the
103/// work proportional to recently active partitions and avoids keeping per-partition cleanup
104/// metadata in memory. Active partitions retain only the required rows plus rows not yet behind
105/// the watermark. A partition that stops receiving rows is cleaned lazily when it's touched again.
106#[derive(Debug)]
107pub(super) struct StateCleaning {
108    /// Index of the watermark column (the first order key column) in the input schema.
109    pub watermark_col_idx: usize,
110    /// Whether stale rows are at the front (`ASC` order key) or at the back (`DESC` order key)
111    /// of the partition.
112    pub stale_rows_at_front: bool,
113    /// Number of stale rows to retain in each partition, which is the number of preceding rows
114    /// plus the number of following rows of the union of all `ROWS` frames.
115    pub n_retain: usize,
116}
117
118#[derive(Default)]
119struct ExecutionStats {
120    cache_miss: u64,
121    cache_lookup: u64,
122}
123
124impl<S: StateStore> Execute for OverWindowExecutor<S> {
125    fn execute(self: Box<Self>) -> crate::executor::BoxedMessageStream {
126        self.executor_inner().boxed()
127    }
128}
129
130impl<S: StateStore> ExecutorInner<S> {
131    /// Get deduplicated partition key from a full row, which happened to be the prefix of table PK.
132    fn get_partition_key(&self, full_row: impl Row) -> OwnedRow {
133        full_row
134            .project(&self.deduped_part_key_indices)
135            .into_owned_row()
136    }
137
138    fn get_input_pk(&self, full_row: impl Row) -> OwnedRow {
139        full_row.project(&self.input_stream_key).into_owned_row()
140    }
141
142    /// `full_row` can be an input row or state table row.
143    fn encode_order_key(&self, full_row: impl Row) -> StreamExecutorResult<MemcmpEncoded> {
144        Ok(memcmp_encoding::encode_row(
145            full_row.project(&self.order_key_indices),
146            &self.order_key_order_types,
147        )?)
148    }
149
150    fn row_to_cache_key(&self, full_row: impl Row + Copy) -> StreamExecutorResult<CacheKey> {
151        Ok(CacheKey::Normal(StateKey {
152            order_key: self.encode_order_key(full_row)?,
153            pk: self.get_input_pk(full_row).into(),
154        }))
155    }
156}
157
158pub struct OverWindowExecutorArgs<S: StateStore> {
159    pub actor_ctx: ActorContextRef,
160
161    pub input: Executor,
162
163    pub schema: Schema,
164    pub calls: Vec<WindowFuncCall>,
165    pub partition_key_indices: Vec<usize>,
166    pub order_key_indices: Vec<usize>,
167    pub order_key_order_types: Vec<OrderType>,
168
169    pub state_table: StateTable<S>,
170    pub watermark_epoch: AtomicU64Ref,
171    pub metrics: Arc<StreamingMetrics>,
172
173    pub chunk_size: usize,
174    pub cache_policy: CachePolicy,
175    /// Whether to enable watermark-driven state cleaning. See [`StateCleaning`].
176    pub enable_state_cleaning: bool,
177}
178
179/// Information about the window function calls.
180/// Contains the original calls and many other information that can be derived from the calls to avoid
181/// repeated calculation.
182pub(super) struct Calls {
183    calls: Vec<WindowFuncCall>,
184
185    /// The `ROWS` frame that is the union of all `ROWS` frames.
186    pub(super) super_rows_frame_bounds: RowsFrameBounds,
187    /// All `RANGE` frames.
188    pub(super) range_frames: Vec<RangeFrameBounds>,
189    pub(super) start_is_unbounded: bool,
190    pub(super) end_is_unbounded: bool,
191    /// Deduplicated indices of all arguments of all calls.
192    pub(super) all_arg_indices: Vec<usize>,
193
194    // TODO(rc): The following flags are used to optimize for `row_number`, `rank` and `dense_rank`.
195    // We should try our best to remove these flags while maintaining the performance in the future.
196    pub(super) numbering_only: bool,
197    pub(super) has_rank: bool,
198}
199
200impl Calls {
201    fn new(calls: Vec<WindowFuncCall>) -> Self {
202        let rows_frames = calls
203            .iter()
204            .filter_map(|call| call.frame.bounds.as_rows())
205            .collect::<Vec<_>>();
206        let super_rows_frame_bounds = merge_rows_frames(&rows_frames);
207        let range_frames = calls
208            .iter()
209            .filter_map(|call| call.frame.bounds.as_range())
210            .cloned()
211            .collect::<Vec<_>>();
212
213        let start_is_unbounded = calls
214            .iter()
215            .any(|call| call.frame.bounds.start_is_unbounded());
216        let end_is_unbounded = calls
217            .iter()
218            .any(|call| call.frame.bounds.end_is_unbounded());
219
220        let all_arg_indices = calls
221            .iter()
222            .flat_map(|call| call.args.val_indices().iter().copied())
223            .dedup()
224            .collect();
225
226        let numbering_only = calls.iter().all(|call| call.kind.is_numbering());
227        let has_rank = calls.iter().any(|call| call.kind.is_rank());
228
229        Self {
230            calls,
231            super_rows_frame_bounds,
232            range_frames,
233            start_is_unbounded,
234            end_is_unbounded,
235            all_arg_indices,
236            numbering_only,
237            has_rank,
238        }
239    }
240
241    pub(super) fn iter(&self) -> impl ExactSizeIterator<Item = &WindowFuncCall> {
242        self.calls.iter()
243    }
244
245    pub(super) fn len(&self) -> usize {
246        self.calls.len()
247    }
248}
249
250impl<S: StateStore> OverWindowExecutor<S> {
251    pub fn new(args: OverWindowExecutorArgs<S>) -> Self {
252        let calls = Calls::new(args.calls);
253
254        let input_info = args.input.info().clone();
255        let input_schema = &input_info.schema;
256
257        let has_unbounded_frame = calls.start_is_unbounded || calls.end_is_unbounded;
258        let cache_policy = if has_unbounded_frame {
259            // For unbounded frames, we finally need all entries of the partition in the cache,
260            // so for simplicity we just use full cache policy for these cases.
261            CachePolicy::Full
262        } else {
263            args.cache_policy
264        };
265
266        let order_key_data_types = args
267            .order_key_indices
268            .iter()
269            .map(|i| input_schema[*i].data_type())
270            .collect();
271
272        let state_key_to_table_sub_pk_proj = RowConverter::calc_state_key_to_table_sub_pk_proj(
273            &args.partition_key_indices,
274            &args.order_key_indices,
275            &input_info.stream_key,
276        );
277
278        let deduped_part_key_indices = {
279            let mut dedup = HashSet::new();
280            args.partition_key_indices
281                .iter()
282                .filter(|i| dedup.insert(**i))
283                .copied()
284                .collect()
285        };
286
287        let state_cleaning = if args.enable_state_cleaning {
288            let all_frames_bounded_rows = calls
289                .iter()
290                .all(|call| call.frame.bounds.is_rows() && !call.frame.bounds.is_unbounded());
291            let bounds = &calls.super_rows_frame_bounds;
292            match (bounds.n_preceding_rows(), bounds.n_following_rows()) {
293                (Some(n_preceding), Some(n_following))
294                    if all_frames_bounded_rows && !args.order_key_indices.is_empty() =>
295                {
296                    Some(StateCleaning {
297                        watermark_col_idx: args.order_key_indices[0],
298                        stale_rows_at_front: args.order_key_order_types[0].is_ascending(),
299                        n_retain: n_preceding.saturating_add(n_following),
300                    })
301                }
302                _ => {
303                    // The optimizer should never enable state cleaning in this case.
304                    tracing::warn!(
305                        "state cleaning is enabled for over window with unbounded or non-`ROWS` frames, ignoring"
306                    );
307                    None
308                }
309            }
310        } else {
311            None
312        };
313
314        Self {
315            input: args.input,
316            inner: ExecutorInner {
317                actor_ctx: args.actor_ctx,
318                schema: args.schema,
319                calls,
320                deduped_part_key_indices,
321                order_key_indices: args.order_key_indices,
322                order_key_data_types,
323                order_key_order_types: args.order_key_order_types,
324                input_stream_key: input_info.stream_key,
325                state_key_to_table_sub_pk_proj,
326                state_table: args.state_table,
327                watermark_sequence: args.watermark_epoch,
328                chunk_size: args.chunk_size,
329                cache_policy,
330                state_cleaning,
331            },
332        }
333    }
334
335    /// Merge changes by input pk in the given chunk, return a change iterator which guarantees that
336    /// each pk only appears once. This method also validates the consistency of the input
337    /// chunk.
338    ///
339    /// TODO(rc): We may want to optimize this by handling changes on the same pk during generating
340    /// partition [`Change`]s.
341    fn merge_changes_in_chunk<'a>(
342        this: &'_ ExecutorInner<S>,
343        chunk: &'a StreamChunk,
344    ) -> impl Iterator<Item = Record<RowRef<'a>>> {
345        let mut cb = ChangeBuffer::with_capacity(chunk.cardinality());
346        for record in chunk.records() {
347            cb.apply_record(record, |row| this.get_input_pk(row));
348        }
349        cb.into_records()
350    }
351
352    #[try_stream(ok = StreamChunk, error = StreamExecutorError)]
353    async fn apply_chunk<'a>(
354        this: &'a mut ExecutorInner<S>,
355        vars: &'a mut ExecutionVars<S>,
356        chunk: StreamChunk,
357        metrics: &'a OverWindowMetrics,
358    ) {
359        // (deduped) partition key => (
360        //   significant changes happened in the partition,
361        //   no-effect changes happened in the partition,
362        // )
363        let mut deltas: BTreeMap<DefaultOrdered<OwnedRow>, (PartitionDelta, PartitionDelta)> =
364            BTreeMap::new();
365        // input pk of update records of which the order key is changed.
366        let mut key_change_updated_pks = HashSet::new();
367
368        // Collect changes for each partition.
369        for record in Self::merge_changes_in_chunk(this, &chunk) {
370            match record {
371                Record::Insert { new_row } => {
372                    let part_key = this.get_partition_key(new_row).into();
373                    let (delta, _) = deltas.entry(part_key).or_default();
374                    delta.insert(
375                        this.row_to_cache_key(new_row)?,
376                        Change::Insert(new_row.into_owned_row()),
377                    );
378                }
379                Record::Delete { old_row } => {
380                    let part_key = this.get_partition_key(old_row).into();
381                    let (delta, _) = deltas.entry(part_key).or_default();
382                    delta.insert(this.row_to_cache_key(old_row)?, Change::Delete);
383                }
384                Record::Update { old_row, new_row } => {
385                    let old_part_key = this.get_partition_key(old_row).into();
386                    let new_part_key = this.get_partition_key(new_row).into();
387                    let old_state_key = this.row_to_cache_key(old_row)?;
388                    let new_state_key = this.row_to_cache_key(new_row)?;
389                    if old_part_key == new_part_key && old_state_key == new_state_key {
390                        // not a key-change update
391                        let (delta, no_effect_delta) = deltas.entry(old_part_key).or_default();
392                        if old_row.project(&this.calls.all_arg_indices)
393                            == new_row.project(&this.calls.all_arg_indices)
394                        {
395                            // partition key, order key and arguments are all the same
396                            no_effect_delta
397                                .insert(old_state_key, Change::Insert(new_row.into_owned_row()));
398                        } else {
399                            delta.insert(old_state_key, Change::Insert(new_row.into_owned_row()));
400                        }
401                    } else if old_part_key == new_part_key {
402                        // order-change update, split into delete + insert, will be merged after
403                        // building changes
404                        key_change_updated_pks.insert(this.get_input_pk(old_row));
405                        let (delta, _) = deltas.entry(old_part_key).or_default();
406                        delta.insert(old_state_key, Change::Delete);
407                        delta.insert(new_state_key, Change::Insert(new_row.into_owned_row()));
408                    } else {
409                        // partition-change update, split into delete + insert
410                        // NOTE(rc): Since we append partition key to logical pk, we can't merge the
411                        // delete + insert back to update later.
412                        // TODO: IMO this behavior is problematic. Deep discussion is needed.
413                        let (old_part_delta, _) = deltas.entry(old_part_key).or_default();
414                        old_part_delta.insert(old_state_key, Change::Delete);
415                        let (new_part_delta, _) = deltas.entry(new_part_key).or_default();
416                        new_part_delta
417                            .insert(new_state_key, Change::Insert(new_row.into_owned_row()));
418                    }
419                }
420            }
421        }
422
423        // `input pk` => `Record`
424        let mut key_change_update_buffer: BTreeMap<DefaultOrdered<OwnedRow>, Record<OwnedRow>> =
425            BTreeMap::new();
426        let mut chunk_builder = StreamChunkBuilder::new(this.chunk_size, this.schema.data_types());
427
428        // Build final changes partition by partition.
429        for (part_key, (delta, no_effect_delta)) in deltas {
430            vars.stats.cache_lookup += 1;
431            if !vars.cached_partitions.contains(&part_key.0) {
432                vars.stats.cache_miss += 1;
433                vars.cached_partitions
434                    .put(part_key.0.clone(), PartitionCache::new());
435            }
436            let mut cache = vars.cached_partitions.get_mut(&part_key).unwrap();
437
438            // First, handle `Update`s that don't affect window function outputs.
439            // Be careful that changes in `delta` may (though we believe unlikely) affect the
440            // window function outputs of rows in `no_effect_delta`, so before handling `delta`
441            // we need to write all changes to state table, range cache and chunk builder.
442            for (key, change) in no_effect_delta {
443                let new_row = change.into_insert().unwrap(); // new row of an `Update`
444
445                let (old_row, from_cache) = if let Some(old_row) = cache.inner().get(&key).cloned()
446                {
447                    // Got old row from range cache.
448                    (old_row, true)
449                } else {
450                    // Retrieve old row from state table.
451                    let table_pk = (&new_row).project(this.state_table.pk_indices());
452                    // The accesses to the state table is ordered by table PK, so ideally we
453                    // can leverage the block cache under the hood.
454                    if let Some(old_row) = this.state_table.get_row(table_pk).await? {
455                        (old_row, false)
456                    } else {
457                        consistency_panic!(?part_key, ?key, ?new_row, "updating non-existing row");
458                        continue;
459                    }
460                };
461
462                // concatenate old outputs
463                let input_len = new_row.len();
464                let new_row = OwnedRow::new(
465                    new_row
466                        .into_iter()
467                        .chain(old_row.as_inner().iter().skip(input_len).cloned()) // chain old outputs
468                        .collect(),
469                );
470
471                // apply & emit the change
472                let record = Record::Update {
473                    old_row: &old_row,
474                    new_row: &new_row,
475                };
476                if let Some(chunk) = chunk_builder.append_record(record.as_ref()) {
477                    yield chunk;
478                }
479                this.state_table.write_record(record);
480                if from_cache {
481                    cache.insert(key, new_row);
482                }
483            }
484
485            let mut partition = OverPartition::new(
486                &part_key,
487                &mut cache,
488                this.cache_policy,
489                &this.calls,
490                RowConverter {
491                    state_key_to_table_sub_pk_proj: &this.state_key_to_table_sub_pk_proj,
492                    order_key_indices: &this.order_key_indices,
493                    order_key_data_types: &this.order_key_data_types,
494                    order_key_order_types: &this.order_key_order_types,
495                    input_stream_key_indices: &this.input_stream_key,
496                },
497            );
498
499            if delta.is_empty() {
500                continue;
501            }
502
503            if this.state_cleaning.is_some() {
504                vars.touched_partitions.insert(part_key.0.clone());
505            }
506
507            // Build changes for current partition.
508            let (part_changes, accessed_range) =
509                partition.build_changes(&this.state_table, delta).await?;
510
511            for (key, record) in part_changes {
512                // Build chunk and yield if needed.
513                if !key_change_updated_pks.contains(&key.pk) {
514                    if let Some(chunk) = chunk_builder.append_record(record.as_ref()) {
515                        yield chunk;
516                    }
517                } else {
518                    // For key-change updates, we should wait for both `Delete` and `Insert` changes
519                    // and merge them together.
520                    let pk = key.pk.clone();
521                    let record = record.clone();
522                    if let Some(existed) = key_change_update_buffer.remove(&key.pk) {
523                        match (existed, record) {
524                            (Record::Insert { new_row }, Record::Delete { old_row })
525                            | (Record::Delete { old_row }, Record::Insert { new_row }) => {
526                                // merge `Delete` and `Insert` into `Update`
527                                if let Some(chunk) =
528                                    chunk_builder.append_record(Record::Update { old_row, new_row })
529                                {
530                                    yield chunk;
531                                }
532                            }
533                            (existed, record) => {
534                                // when stream is inconsistent, there may be an `Update` of which the old pk does not actually exist
535                                consistency_panic!(
536                                    ?existed,
537                                    ?record,
538                                    "other cases should not exist",
539                                );
540
541                                key_change_update_buffer.insert(pk, record);
542                                if let Some(chunk) = chunk_builder.append_record(existed) {
543                                    yield chunk;
544                                }
545                            }
546                        }
547                    } else {
548                        key_change_update_buffer.insert(pk, record);
549                    }
550                }
551
552                // Apply the change record.
553                partition.write_record(&mut this.state_table, key, record);
554            }
555
556            if !key_change_update_buffer.is_empty() {
557                consistency_panic!(
558                    ?key_change_update_buffer,
559                    "key-change update buffer should be empty after processing"
560                );
561                // if in non-strict mode, we can reach here, but we don't know the `StateKey`,
562                // so just ignore the buffer.
563            }
564
565            let cache_len = partition.cache_real_len();
566            let stats = partition.summarize();
567            metrics
568                .over_window_range_cache_entry_count
569                .set(cache_len as i64);
570            metrics
571                .over_window_range_cache_lookup_count
572                .inc_by(stats.lookup_count);
573            metrics
574                .over_window_range_cache_left_miss_count
575                .inc_by(stats.left_miss_count);
576            metrics
577                .over_window_range_cache_right_miss_count
578                .inc_by(stats.right_miss_count);
579            metrics
580                .over_window_accessed_entry_count
581                .inc_by(stats.accessed_entry_count);
582            metrics
583                .over_window_compute_count
584                .inc_by(stats.compute_count);
585            metrics
586                .over_window_same_output_count
587                .inc_by(stats.same_output_count);
588
589            // Update recently accessed range for later shrinking cache.
590            if !this.cache_policy.is_full()
591                && let Some(accessed_range) = accessed_range
592            {
593                match vars.recently_accessed_ranges.entry(part_key) {
594                    btree_map::Entry::Vacant(vacant) => {
595                        vacant.insert(accessed_range);
596                    }
597                    btree_map::Entry::Occupied(mut occupied) => {
598                        let recently_accessed_range = occupied.get_mut();
599                        let min_start = accessed_range
600                            .start()
601                            .min(recently_accessed_range.start())
602                            .clone();
603                        let max_end = accessed_range
604                            .end()
605                            .max(recently_accessed_range.end())
606                            .clone();
607                        *recently_accessed_range = min_start..=max_end;
608                    }
609                }
610            }
611        }
612
613        // Yield remaining changes to downstream.
614        if let Some(chunk) = chunk_builder.take() {
615            yield chunk;
616        }
617    }
618
619    /// Clean up stale rows of the partitions touched since the last barrier, according to the
620    /// latest watermark received.
621    /// Returns the number of rows deleted. See [`StateCleaning`].
622    async fn clean_state(
623        this: &mut ExecutorInner<S>,
624        vars: &mut ExecutionVars<S>,
625    ) -> StreamExecutorResult<usize> {
626        let touched = std::mem::take(&mut vars.touched_partitions);
627        let (Some(cleaning), Some(watermark)) = (&this.state_cleaning, &vars.cleaning_watermark)
628        else {
629            return Ok(0);
630        };
631        if touched.is_empty() {
632            return Ok(0);
633        }
634
635        let row_conv = RowConverter {
636            state_key_to_table_sub_pk_proj: &this.state_key_to_table_sub_pk_proj,
637            order_key_indices: &this.order_key_indices,
638            order_key_data_types: &this.order_key_data_types,
639            order_key_order_types: &this.order_key_order_types,
640            input_stream_key_indices: &this.input_stream_key,
641        };
642
643        let mut n_deleted = 0;
644        for part_key in touched {
645            let mut cache_guard = vars.cached_partitions.get_mut(&part_key);
646            // If the partition is not cached (e.g. evicted), use a temporary cache with only
647            // sentinels, so that `OverPartition` scans the state table for stale rows.
648            let mut temp_cache = PartitionCache::new();
649            let cache = match cache_guard.as_deref_mut() {
650                Some(cache) => cache,
651                None => &mut temp_cache,
652            };
653            let mut partition =
654                OverPartition::new(&part_key, cache, this.cache_policy, &this.calls, row_conv);
655            let (n, has_more) = partition
656                .clean_stale_rows(&mut this.state_table, cleaning, watermark)
657                .await?;
658            n_deleted += n;
659            if has_more {
660                // continue to clean this partition at the next barrier
661                vars.touched_partitions.insert(part_key.clone());
662            }
663        }
664        Ok(n_deleted)
665    }
666
667    #[try_stream(ok = Message, error = StreamExecutorError)]
668    async fn executor_inner(self) {
669        let OverWindowExecutor {
670            input,
671            inner: mut this,
672        } = self;
673
674        let metrics_info = MetricsInfo::new(
675            this.actor_ctx.streaming_metrics.clone(),
676            this.state_table.table_id(),
677            this.actor_ctx.id,
678            "OverWindow",
679        );
680
681        let metrics = metrics_info.metrics.new_over_window_metrics(
682            this.state_table.table_id(),
683            this.actor_ctx.id,
684            this.actor_ctx.fragment_id,
685        );
686
687        let mut vars = ExecutionVars {
688            cached_partitions: ManagedLruCache::unbounded(
689                this.watermark_sequence.clone(),
690                metrics_info,
691            ),
692            recently_accessed_ranges: Default::default(),
693            cleaning_watermark: None,
694            touched_partitions: Default::default(),
695            stats: Default::default(),
696            _phantom: PhantomData::<S>,
697        };
698
699        let mut input = input.execute();
700        let barrier = expect_first_barrier(&mut input).await?;
701        let first_epoch = barrier.epoch;
702        yield Message::Barrier(barrier);
703        this.state_table.init_epoch(first_epoch).await?;
704
705        #[for_await]
706        for msg in input {
707            let msg = msg?;
708            match msg {
709                Message::Watermark(watermark) => {
710                    if let Some(cleaning) = &this.state_cleaning
711                        && watermark.col_idx == cleaning.watermark_col_idx
712                        && vars
713                            .cleaning_watermark
714                            .as_ref()
715                            .is_none_or(|old| old.default_cmp(&watermark.val).is_lt())
716                    {
717                        // Only used for state cleaning at the next barrier.
718                        vars.cleaning_watermark = Some(watermark.val);
719                    }
720                    // TODO(rc): We don't propagate watermarks to downstream for now, because rows
721                    // below the watermark may still be updated by later rows if there's any
722                    // following frame bound, e.g. `lead`. We need to think about it carefully.
723                    continue;
724                }
725                Message::Chunk(chunk) => {
726                    #[for_await]
727                    for chunk in Self::apply_chunk(&mut this, &mut vars, chunk, &metrics) {
728                        yield Message::Chunk(chunk?);
729                    }
730                    this.state_table.try_flush().await?;
731
732                    // Also apply the LRU watermark at chunk boundaries, so that cold
733                    // partitions can be released without waiting for the next barrier,
734                    // which can be a long time away with large barrier intervals. This
735                    // is safe because the range cache is write-through: at this point
736                    // all changes have been applied to both the state table and the
737                    // cache, so an evicted partition can be reloaded from the state
738                    // table with identical content.
739                    vars.cached_partitions.evict();
740                }
741                Message::Barrier(barrier) => {
742                    let n_cleaned = Self::clean_state(&mut this, &mut vars).await?;
743                    metrics
744                        .over_window_state_cleaned_row_count
745                        .inc_by(n_cleaned as u64);
746
747                    let post_commit = this.state_table.commit(barrier.epoch).await?;
748
749                    let update_vnode_bitmap = barrier.as_update_vnode_bitmap(this.actor_ctx.id);
750                    yield Message::Barrier(barrier);
751
752                    vars.cached_partitions.evict();
753
754                    metrics
755                        .over_window_cached_entry_count
756                        .set(vars.cached_partitions.len() as _);
757                    metrics
758                        .over_window_cache_lookup_count
759                        .inc_by(std::mem::take(&mut vars.stats.cache_lookup));
760                    metrics
761                        .over_window_cache_miss_count
762                        .inc_by(std::mem::take(&mut vars.stats.cache_miss));
763
764                    if let Some((_, cache_may_stale)) =
765                        post_commit.post_yield_barrier(update_vnode_bitmap).await?
766                        && cache_may_stale
767                    {
768                        vars.cached_partitions.clear();
769                        vars.recently_accessed_ranges.clear();
770                        vars.touched_partitions.clear();
771                    }
772
773                    if !this.cache_policy.is_full() {
774                        for (part_key, recently_accessed_range) in
775                            std::mem::take(&mut vars.recently_accessed_ranges)
776                        {
777                            if let Some(mut range_cache) =
778                                vars.cached_partitions.get_mut(&part_key.0)
779                            {
780                                range_cache.shrink(
781                                    &part_key.0,
782                                    this.cache_policy,
783                                    recently_accessed_range,
784                                );
785                            }
786                        }
787                    }
788                }
789            }
790        }
791    }
792}
793
794/// A converter that helps convert [`StateKey`] to state table sub-PK and convert executor input/output
795/// row to [`StateKey`].
796///
797/// ## Notes
798///
799/// - [`StateKey`]: Over window range cache key type, containing order key and input pk.
800/// - State table sub-PK: State table PK = PK prefix (partition key) + sub-PK (order key + input pk).
801/// - Input/output row: Input schema is the prefix of output schema.
802///
803/// You can see that the content of [`StateKey`] is very similar to state table sub-PK. There's only
804/// one difference: the state table PK and sub-PK don't have duplicated columns, while in [`StateKey`],
805/// `order_key` and (input)`pk` may contain duplicated columns.
806#[derive(Debug, Clone, Copy)]
807pub(super) struct RowConverter<'a> {
808    state_key_to_table_sub_pk_proj: &'a [usize],
809    order_key_indices: &'a [usize],
810    order_key_data_types: &'a [DataType],
811    order_key_order_types: &'a [OrderType],
812    input_stream_key_indices: &'a [usize],
813}
814
815impl<'a> RowConverter<'a> {
816    /// Calculate the indices needed for projection from [`StateKey`] to state table sub-PK (used to do
817    /// prefixed table scanning). Ideally this function should be called only once by each executor instance.
818    /// The projection indices vec is the *selected column indices* in [`StateKey`].`order_key.chain(input_pk)`.
819    pub(super) fn calc_state_key_to_table_sub_pk_proj(
820        partition_key_indices: &[usize],
821        order_key_indices: &[usize],
822        input_stream_key_indices: &'a [usize],
823    ) -> Vec<usize> {
824        // This process is corresponding to `StreamOverWindow::infer_state_table`.
825        let mut projection =
826            Vec::with_capacity(order_key_indices.len() + input_stream_key_indices.len());
827        let mut col_dedup: HashSet<usize> = partition_key_indices.iter().copied().collect();
828        for (proj_idx, key_idx) in order_key_indices
829            .iter()
830            .chain(input_stream_key_indices.iter())
831            .enumerate()
832        {
833            if col_dedup.insert(*key_idx) {
834                projection.push(proj_idx);
835            }
836        }
837        projection.shrink_to_fit();
838        projection
839    }
840
841    /// Convert [`StateKey`] to sub-PK (table PK without partition key) as [`OwnedRow`].
842    pub(super) fn state_key_to_table_sub_pk(
843        &self,
844        key: &StateKey,
845    ) -> StreamExecutorResult<OwnedRow> {
846        Ok(memcmp_encoding::decode_row(
847            &key.order_key,
848            self.order_key_data_types,
849            self.order_key_order_types,
850        )?
851        .chain(key.pk.as_inner())
852        .project(self.state_key_to_table_sub_pk_proj)
853        .into_owned_row())
854    }
855
856    /// Convert full input/output row to [`StateKey`].
857    pub(super) fn row_to_state_key(
858        &self,
859        full_row: impl Row + Copy,
860    ) -> StreamExecutorResult<StateKey> {
861        Ok(StateKey {
862            order_key: memcmp_encoding::encode_row(
863                full_row.project(self.order_key_indices),
864                self.order_key_order_types,
865            )?,
866            pk: full_row
867                .project(self.input_stream_key_indices)
868                .into_owned_row()
869                .into(),
870        })
871    }
872}
873
874#[cfg(test)]
875mod tests {
876    use std::ops::Bound;
877
878    use futures::TryStreamExt;
879    use risingwave_common::catalog::{ColumnDesc, ColumnId, TableId};
880    use risingwave_common::util::epoch::{EpochPair, test_epoch};
881    use risingwave_storage::memory::MemoryStateStore;
882    use risingwave_storage::store::PrefetchOptions;
883
884    use super::*;
885    use crate::common::table::test_utils::gen_pbtable;
886
887    #[tokio::test]
888    async fn test_state_cleaning_large_retention() {
889        for order_type in [OrderType::ascending(), OrderType::descending()] {
890            for cached in [true, false] {
891                // Both are valid sums of two Int64 ROWS offsets on 64-bit targets.
892                // The first makes the old collection limit wrap to 1, so even three
893                // rows catch the erroneous deletion with overflow checks disabled.
894                for n_retain in [usize::MAX - 65_534, usize::MAX - 1] {
895                    let mut table = StateTable::from_table_catalog(
896                        &gen_pbtable(
897                            TableId::new(1),
898                            vec![ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64)],
899                            vec![order_type],
900                            vec![0],
901                            0,
902                        ),
903                        MemoryStateStore::new(),
904                        None,
905                    )
906                    .await;
907                    table
908                        .init_epoch(EpochPair::new_test_epoch(test_epoch(1)))
909                        .await
910                        .unwrap();
911                    let rows = [10i64, 20, 30].map(|value| OwnedRow::new(vec![Some(value.into())]));
912                    let row_conv = RowConverter {
913                        state_key_to_table_sub_pk_proj: &[0],
914                        order_key_indices: &[0],
915                        order_key_data_types: &[DataType::Int64],
916                        order_key_order_types: &[order_type],
917                        input_stream_key_indices: &[0],
918                    };
919                    let mut cache = if cached {
920                        PartitionCache::new_without_sentinels()
921                    } else {
922                        PartitionCache::new()
923                    };
924                    for row in &rows {
925                        table.insert(row.clone());
926                        if cached {
927                            cache.insert(
928                                CacheKey::from(row_conv.row_to_state_key(row).unwrap()),
929                                row.clone(),
930                            );
931                        }
932                    }
933                    table
934                        .commit_for_test(EpochPair::new_test_epoch(test_epoch(2)))
935                        .await
936                        .unwrap();
937
938                    let partition_key = OwnedRow::empty();
939                    let calls = Calls::new(vec![]);
940                    let mut partition = OverPartition::new(
941                        &partition_key,
942                        &mut cache,
943                        CachePolicy::Full,
944                        &calls,
945                        row_conv,
946                    );
947                    let cleaning = StateCleaning {
948                        watermark_col_idx: 0,
949                        stale_rows_at_front: order_type.is_ascending(),
950                        n_retain,
951                    };
952                    assert_eq!(
953                        partition
954                            .clean_stale_rows(&mut table, &cleaning, &100i64.into())
955                            .await
956                            .unwrap(),
957                        (0, false),
958                        "order={order_type:?}, cached={cached}, n_retain={n_retain}",
959                    );
960                    table
961                        .commit_for_test(EpochPair::new_test_epoch(test_epoch(3)))
962                        .await
963                        .unwrap();
964                    let range: (Bound<OwnedRow>, Bound<OwnedRow>) =
965                        (Bound::Unbounded, Bound::Unbounded);
966                    let remaining: Vec<OwnedRow> = table
967                        .iter_with_prefix(&partition_key, &range, PrefetchOptions::default())
968                        .await
969                        .unwrap()
970                        .try_collect()
971                        .await
972                        .unwrap();
973                    let mut expected = rows.to_vec();
974                    if !order_type.is_ascending() {
975                        expected.reverse();
976                    }
977                    assert_eq!(remaining, expected);
978                    assert_eq!(cache.normal_len(), if cached { rows.len() } else { 0 });
979                }
980            }
981        }
982    }
983}