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 itertools::Itertools;
21use risingwave_common::array::stream_record::Record;
22use risingwave_common::array::{ArrayRef, Op};
23use risingwave_common::row::RowExt;
24use risingwave_common::types::{ToDatumRef, ToOwnedDatum};
25use risingwave_common::util::iter_util::{ZipEqDebug, ZipEqFast};
26use risingwave_common::util::memcmp_encoding::{self, MemcmpEncoded};
27use risingwave_common::util::row_serde::OrderedRowSerde;
28use risingwave_common::util::sort_util::OrderType;
29use risingwave_common::{must_match, row};
30use risingwave_common_estimate_size::EstimateSize;
31use risingwave_common_estimate_size::collections::EstimatedVecDeque;
32use risingwave_expr::window_function::{
33    StateEvictHint, StateKey, WindowFuncCall, WindowStateSnapshot, WindowStates,
34    create_window_state,
35};
36use risingwave_pb::window_function::{
37    StateKey as PbStateKey, WindowStateSnapshot as PbWindowStateSnapshot,
38};
39use risingwave_storage::store::PrefetchOptions;
40use tracing::debug;
41
42use crate::cache::ManagedLruCache;
43use crate::common::metrics::MetricsInfo;
44use crate::executor::prelude::*;
45
46struct Partition {
47    states: WindowStates,
48    curr_row_buffer: EstimatedVecDeque<OwnedRow>,
49    /// Cached intermediate state row for this partition, used for upsert operations.
50    /// `None` means no prior row exists in the intermediate state table for this partition.
51    intermediate_state_row: Option<OwnedRow>,
52}
53
54impl EstimateSize for Partition {
55    fn estimated_heap_size(&self) -> usize {
56        let mut total_size = self.curr_row_buffer.estimated_heap_size();
57        for state in self.states.iter() {
58            total_size += state.estimated_heap_size();
59        }
60        if let Some(row) = &self.intermediate_state_row {
61            total_size += row.estimated_heap_size();
62        }
63        total_size
64    }
65}
66
67type PartitionCache = ManagedLruCache<MemcmpEncoded, Partition>; // TODO(rc): use `K: HashKey` as key like in hash agg?
68
69/// Encode a [`WindowStateSnapshot`] to bytes for persistence using protobuf.
70fn encode_snapshot(snapshot: &WindowStateSnapshot, pk_ser: &OrderedRowSerde) -> Vec<u8> {
71    use prost::Message;
72    let pb = PbWindowStateSnapshot {
73        last_output_key: snapshot.last_output_key.as_ref().map(|key| PbStateKey {
74            order_key: key.order_key.to_vec(),
75            pk: key.pk.as_inner().memcmp_serialize(pk_ser),
76        }),
77        function_state: Some(snapshot.function_state.clone()),
78    };
79    pb.encode_to_vec()
80}
81
82/// Decode a [`WindowStateSnapshot`] from bytes during recovery using protobuf.
83fn decode_snapshot(
84    bytes: &[u8],
85    pk_deser: &OrderedRowSerde,
86) -> StreamExecutorResult<WindowStateSnapshot> {
87    use prost::Message;
88    let pb = PbWindowStateSnapshot::decode(bytes).context("failed to decode snapshot")?;
89    let last_output_key = pb
90        .last_output_key
91        .map(|key| {
92            let pk = pk_deser
93                .deserialize(&key.pk)
94                .context("failed to deserialize pk")?;
95            Ok::<_, anyhow::Error>(StateKey {
96                order_key: key.order_key.into(),
97                pk: pk.into(),
98            })
99        })
100        .transpose()?;
101    let function_state = pb
102        .function_state
103        .context("snapshot missing function_state")?;
104    Ok(WindowStateSnapshot {
105        last_output_key,
106        function_state,
107    })
108}
109
110/// [`EowcOverWindowExecutor`] consumes ordered input (on order key column with watermark in
111/// ascending order) and outputs window function results. One [`EowcOverWindowExecutor`] can handle
112/// one combination of partition key and order key.
113///
114/// The reason not to use [`SortBuffer`] is that the table schemas of [`EowcOverWindowExecutor`] and
115/// [`SortBuffer`] are different, since we don't have something like a _grouped_ sort buffer.
116///
117/// [`SortBuffer`]: crate::executor::eowc::SortBuffer
118///
119/// Basic idea:
120///
121/// ```text
122/// ──────────────┬────────────────────────────────────────────────────── curr evict row
123///               │ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING
124///        (1)    │ ─┬─
125///               │  │RANGE BETWEEN '1hr' PRECEDING AND '1hr' FOLLOWING
126///        ─┬─    │  │ ─┬─
127///   LAG(1)│        │  │
128/// ────────┴──┬─────┼──┼──────────────────────────────────────────────── curr output row
129///     LEAD(1)│     │  │GROUPS 1 PRECEDING AND 1 FOLLOWING
130///                  │
131///                  │ (2)
132/// ─────────────────┴─────────────────────────────────────────────────── curr input row
133/// (1): additional buffered input (unneeded) for some window
134/// (2): additional delay (already able to output) for some window
135/// ```
136///
137/// - State table schema = input schema, state table pk = `partition key | order key | input pk`.
138/// - Output schema = input schema + window function results.
139/// - Rows in range (`curr evict row`, `curr input row`] are in state table.
140/// - `curr evict row` <= min(last evict rows of all `WindowState`s).
141/// - `WindowState` should output agg result for `curr output row`.
142/// - Recover: iterate through state table, push rows to `WindowState`, ignore ready windows.
143pub struct EowcOverWindowExecutor<S: StateStore> {
144    input: Executor,
145    inner: ExecutorInner<S>,
146}
147
148struct ExecutorInner<S: StateStore> {
149    actor_ctx: ActorContextRef,
150
151    schema: Schema,
152    calls: Vec<WindowFuncCall>,
153    input_stream_key: Vec<usize>,
154    partition_key_indices: Vec<usize>,
155    order_key_index: usize, // no `OrderType` here, cuz we expect the input is ascending
156    state_table: StateTable<S>,
157    /// For each column of the state table, its index in
158    /// `partition key | order key | input stream key | NULL`, to rebuild the row of an evicted key.
159    evict_row_indices: Vec<usize>,
160    watermark_sequence: AtomicU64Ref,
161    /// Optional state table for persisting window function intermediate states.
162    /// See `StreamEowcOverWindow::infer_intermediate_state_table` for schema definition.
163    intermediate_state_table: Option<StateTable<S>>,
164    /// Serde for input stream key (pk), used for encoding/decoding `StateKey` in snapshots.
165    /// Only initialized when `intermediate_state_table` is present.
166    pk_serde: Option<OrderedRowSerde>,
167}
168
169struct ExecutionVars<S: StateStore> {
170    partitions: PartitionCache,
171    _phantom: PhantomData<S>,
172}
173
174impl<S: StateStore> Execute for EowcOverWindowExecutor<S> {
175    fn execute(self: Box<Self>) -> BoxedMessageStream {
176        self.executor_inner().boxed()
177    }
178}
179
180pub struct EowcOverWindowExecutorArgs<S: StateStore> {
181    pub actor_ctx: ActorContextRef,
182
183    pub input: Executor,
184
185    pub schema: Schema,
186    pub calls: Vec<WindowFuncCall>,
187    pub partition_key_indices: Vec<usize>,
188    pub order_key_index: usize,
189    pub state_table: StateTable<S>,
190    pub watermark_epoch: AtomicU64Ref,
191    /// Optional state table for persisting window function intermediate states.
192    /// See `StreamEowcOverWindow::infer_intermediate_state_table` for schema definition.
193    pub intermediate_state_table: Option<StateTable<S>>,
194}
195
196impl<S: StateStore> EowcOverWindowExecutor<S> {
197    pub fn new(args: EowcOverWindowExecutorArgs<S>) -> Self {
198        let input_info = args.input.info().clone();
199
200        // Evictions rebuild state table rows from these columns only.
201        debug_assert!(
202            args.state_table.pk_indices().iter().all(|i| {
203                args.partition_key_indices.contains(i)
204                    || *i == args.order_key_index
205                    || input_info.stream_key.contains(i)
206            }),
207            "state table pk must consist of partition key, order key and input stream key columns"
208        );
209        let num_partition_key_cols = args.partition_key_indices.len();
210        let evict_row_indices = (0..input_info.schema.len())
211            .map(|i| {
212                if let Some(j) = args.partition_key_indices.iter().position(|&k| k == i) {
213                    j
214                } else if i == args.order_key_index {
215                    num_partition_key_cols
216                } else if let Some(j) = input_info.stream_key.iter().position(|&k| k == i) {
217                    num_partition_key_cols + 1 + j
218                } else {
219                    // NULL
220                    num_partition_key_cols + 1 + input_info.stream_key.len()
221                }
222            })
223            .collect_vec();
224
225        // Build pk_serde if intermediate_state_table is present
226        let pk_serde = args.intermediate_state_table.as_ref().map(|_| {
227            let pk_data_types: Vec<_> = input_info
228                .stream_key
229                .iter()
230                .map(|&i| args.schema[i].data_type())
231                .collect();
232            let pk_order_types: Vec<_> = input_info
233                .stream_key
234                .iter()
235                .map(|_| OrderType::ascending())
236                .collect();
237            OrderedRowSerde::new(pk_data_types, pk_order_types)
238        });
239
240        Self {
241            input: args.input,
242            inner: ExecutorInner {
243                actor_ctx: args.actor_ctx,
244                schema: args.schema,
245                calls: args.calls,
246                input_stream_key: input_info.stream_key,
247                partition_key_indices: args.partition_key_indices,
248                order_key_index: args.order_key_index,
249                state_table: args.state_table,
250                evict_row_indices,
251                watermark_sequence: args.watermark_epoch,
252                intermediate_state_table: args.intermediate_state_table,
253                pk_serde,
254            },
255        }
256    }
257
258    /// Load intermediate state snapshots from the state table and restore into partition states.
259    async fn load_intermediate_state(
260        this: &ExecutorInner<S>,
261        partition: &mut Partition,
262        partition_key: impl Row,
263        encoded_partition_key: &MemcmpEncoded,
264    ) -> StreamExecutorResult<()> {
265        let Some(intermediate_state_table) = &this.intermediate_state_table else {
266            return Ok(());
267        };
268        let pk_serde = this
269            .pk_serde
270            .as_ref()
271            .expect("pk_serde must be set when intermediate_state_table is present");
272
273        for state in partition.states.iter_mut() {
274            state.enable_persistence();
275        }
276
277        let partition_key_owned = partition_key.to_owned_row();
278        if let Some(row) = intermediate_state_table
279            .get_row(&partition_key_owned)
280            .await?
281        {
282            let num_partition_key_cols = this.partition_key_indices.len();
283            let num_calls = this.calls.len();
284
285            for call_index in 0..num_calls {
286                let state_col = num_partition_key_cols + call_index;
287                if state_col < row.len() {
288                    if let Some(state_bytes) = row.datum_at(state_col) {
289                        let snapshot = decode_snapshot(state_bytes.into_bytea(), pk_serde)?;
290                        debug!(
291                            "Restoring intermediate state for partition {:?}, call_index {}, has_last_key: {}",
292                            encoded_partition_key,
293                            call_index,
294                            snapshot.last_output_key.is_some()
295                        );
296                        partition
297                            .states
298                            .get_mut(call_index)
299                            .unwrap()
300                            .restore(snapshot)?;
301                    }
302                } else {
303                    return Err(anyhow::anyhow!(
304                        "intermediate state row has fewer columns ({}) than expected ({}) \
305                        at call_index {}, state may be corrupted",
306                        row.len(),
307                        num_partition_key_cols + num_calls,
308                        call_index
309                    )
310                    .into());
311                }
312            }
313            partition.intermediate_state_row = Some(row);
314        }
315        Ok(())
316    }
317
318    /// Persist intermediate state snapshots to the state table.
319    fn persist_intermediate_state(
320        this: &mut ExecutorInner<S>,
321        partition: &mut Partition,
322        partition_key: impl Row,
323    ) {
324        let Some(intermediate_state_table) = &mut this.intermediate_state_table else {
325            return;
326        };
327        let pk_serde = this
328            .pk_serde
329            .as_ref()
330            .expect("pk_serde must be set when intermediate_state_table is present");
331
332        let num_calls = partition.states.len();
333        let num_partition_key_cols = partition_key.len();
334
335        // Build the new row: partition_key columns + state_0..state_{n-1}
336        let mut new_row_values = Vec::with_capacity(num_partition_key_cols + num_calls);
337        for datum in partition_key.iter() {
338            new_row_values.push(datum.to_owned_datum());
339        }
340
341        // For each call, encode snapshot or preserve previous value
342        for (call_index, state) in partition.states.iter().enumerate() {
343            if let Some(snapshot) = state.snapshot() {
344                let snapshot_bytes = encode_snapshot(&snapshot, pk_serde);
345                new_row_values.push(Some(snapshot_bytes.into_boxed_slice().into()));
346            } else if let Some(ref old_row) = partition.intermediate_state_row {
347                let state_col = num_partition_key_cols + call_index;
348                if state_col < old_row.len() {
349                    new_row_values.push(old_row.datum_at(state_col).to_owned_datum());
350                } else {
351                    new_row_values.push(None);
352                }
353            } else {
354                new_row_values.push(None);
355            }
356        }
357        let new_row = OwnedRow::new(new_row_values);
358
359        // Upsert: update if old row exists, otherwise insert
360        if let Some(old_row) = partition.intermediate_state_row.take() {
361            intermediate_state_table.update(old_row, new_row.clone());
362        } else {
363            intermediate_state_table.insert(new_row.clone());
364        }
365        partition.intermediate_state_row = Some(new_row);
366    }
367
368    async fn ensure_key_in_cache(
369        this: &ExecutorInner<S>,
370        cache: &mut PartitionCache,
371        partition_key: impl Row,
372        encoded_partition_key: &MemcmpEncoded,
373    ) -> StreamExecutorResult<()> {
374        if cache.contains(encoded_partition_key) {
375            return Ok(());
376        }
377
378        let mut partition = Partition {
379            states: WindowStates::new(this.calls.iter().map(create_window_state).try_collect()?),
380            curr_row_buffer: Default::default(),
381            intermediate_state_row: None,
382        };
383
384        // If intermediate state table exists, load and restore intermediate state snapshots
385        Self::load_intermediate_state(this, &mut partition, &partition_key, encoded_partition_key)
386            .await?;
387
388        let sub_range: &(Bound<OwnedRow>, Bound<OwnedRow>) = &(Bound::Unbounded, Bound::Unbounded);
389        // Recover states from state table.
390        let table_iter = this
391            .state_table
392            .iter_with_prefix(&partition_key, sub_range, PrefetchOptions::default())
393            .await?;
394
395        #[for_await]
396        for keyed_row in table_iter {
397            let row = keyed_row?.into_owned_row();
398            let order_key_enc = memcmp_encoding::encode_row(
399                row::once(Some(
400                    row.datum_at(this.order_key_index)
401                        .expect("order key column must be non-NULL")
402                        .into_scalar_impl(),
403                )),
404                &[OrderType::ascending()],
405            )?;
406            let pk = (&row).project(&this.input_stream_key).into_owned_row();
407            let key = StateKey {
408                order_key: order_key_enc,
409                pk: pk.into(),
410            };
411            for (call, state) in this.calls.iter().zip_eq_fast(partition.states.iter_mut()) {
412                state.append(
413                    key.clone(),
414                    (&row)
415                        .project(call.args.val_indices())
416                        .into_owned_row()
417                        .as_inner()
418                        .into(),
419                );
420            }
421            partition.curr_row_buffer.push_back(row);
422        }
423
424        // Ensure states correctness.
425        assert!(partition.states.are_aligned());
426
427        // Ignore ready windows (all ready windows were outputted before).
428        // Use just_slide which calls slide_no_output and respects recovery skip logic.
429        while partition.states.are_ready() {
430            partition.states.just_slide()?;
431            partition.curr_row_buffer.pop_front();
432        }
433
434        cache.put(encoded_partition_key.clone(), partition);
435        Ok(())
436    }
437
438    async fn apply_chunk(
439        this: &mut ExecutorInner<S>,
440        vars: &mut ExecutionVars<S>,
441        chunk: StreamChunk,
442    ) -> StreamExecutorResult<Option<StreamChunk>> {
443        let mut builders = this.schema.create_array_builders(chunk.capacity()); // just an estimate
444        // Track partitions that produced output during this chunk, so we persist
445        // intermediate state only once per partition at the end.
446        let mut dirty_partitions: HashMap<MemcmpEncoded, OwnedRow> = HashMap::new();
447
448        // We assume that the input is sorted by order key.
449        for record in chunk.records() {
450            let input_row = must_match!(record, Record::Insert { new_row } => new_row);
451
452            let partition_key = input_row
453                .project(&this.partition_key_indices)
454                .into_owned_row();
455            let encoded_partition_key = memcmp_encoding::encode_row(
456                &partition_key,
457                &vec![OrderType::ascending(); this.partition_key_indices.len()],
458            )?;
459
460            // Get the partition.
461            Self::ensure_key_in_cache(
462                this,
463                &mut vars.partitions,
464                &partition_key,
465                &encoded_partition_key,
466            )
467            .await?;
468            let partition: &mut Partition =
469                &mut vars.partitions.get_mut(&encoded_partition_key).unwrap();
470
471            // Materialize input to state table.
472            this.state_table.insert(input_row);
473
474            // Feed the row to all window states.
475            let order_key_enc = memcmp_encoding::encode_row(
476                row::once(Some(
477                    input_row
478                        .datum_at(this.order_key_index)
479                        .expect("order key column must be non-NULL")
480                        .into_scalar_impl(),
481                )),
482                &[OrderType::ascending()],
483            )?;
484            let pk = input_row.project(&this.input_stream_key).into_owned_row();
485            let key = StateKey {
486                order_key: order_key_enc,
487                pk: pk.into(),
488            };
489            for (call, state) in this.calls.iter().zip_eq_fast(partition.states.iter_mut()) {
490                state.append(
491                    key.clone(),
492                    input_row
493                        .project(call.args.val_indices())
494                        .into_owned_row()
495                        .as_inner()
496                        .into(),
497                );
498            }
499            partition
500                .curr_row_buffer
501                .push_back(input_row.into_owned_row());
502
503            let mut has_output = false;
504            while partition.states.are_ready() {
505                has_output = true;
506                // The partition is ready to output, so we can produce a row.
507
508                // Get all outputs.
509                let (ret_values, evict_hint) = partition.states.slide()?;
510                let curr_row = partition
511                    .curr_row_buffer
512                    .pop_front()
513                    .expect("ready window must have corresponding current row");
514
515                // Append to output builders.
516                for (builder, datum) in builders.iter_mut().zip_eq_debug(
517                    curr_row
518                        .iter()
519                        .chain(ret_values.iter().map(|v| v.to_datum_ref())),
520                ) {
521                    builder.append(datum);
522                }
523
524                // Evict unneeded rows from state table.
525                if let StateEvictHint::CanEvict(keys_to_evict) = evict_hint {
526                    for key in keys_to_evict {
527                        let order_key = memcmp_encoding::decode_row(
528                            &key.order_key,
529                            &[this.schema[this.order_key_index].data_type()],
530                            &[OrderType::ascending()],
531                        )?;
532                        // FIXME(rc): quite hacky here, we may need `state_table.delete_by_pk`
533                        let state_row = (&partition_key)
534                            .chain(&order_key)
535                            .chain(key.pk.as_inner())
536                            .chain(row::once(Datum::None))
537                            .project(&this.evict_row_indices);
538                        // NOTE: We don't know the value of the row here, so the table must allow
539                        // inconsistent ops.
540                        this.state_table.delete(state_row);
541                    }
542                }
543            }
544
545            if has_output && this.intermediate_state_table.is_some() {
546                dirty_partitions
547                    .entry(encoded_partition_key)
548                    .or_insert(partition_key);
549            }
550        }
551
552        // Persist intermediate state snapshots once per dirty partition at the end of the chunk.
553        for (encoded_partition_key, partition_key) in &dirty_partitions {
554            let partition = &mut *vars.partitions.get_mut(encoded_partition_key).unwrap();
555            Self::persist_intermediate_state(this, partition, partition_key);
556        }
557
558        let columns: Vec<ArrayRef> = builders.into_iter().map(|b| b.finish().into()).collect();
559        let chunk_size = columns[0].len();
560        Ok(if chunk_size > 0 {
561            Some(StreamChunk::new(vec![Op::Insert; chunk_size], columns))
562        } else {
563            None
564        })
565    }
566
567    #[try_stream(ok = Message, error = StreamExecutorError)]
568    async fn executor_inner(self) {
569        let EowcOverWindowExecutor {
570            input,
571            inner: mut this,
572        } = self;
573
574        let metrics_info = MetricsInfo::new(
575            this.actor_ctx.streaming_metrics.clone(),
576            this.state_table.table_id(),
577            this.actor_ctx.id,
578            "EowcOverWindow",
579        );
580
581        let mut vars = ExecutionVars {
582            partitions: ManagedLruCache::unbounded(this.watermark_sequence.clone(), metrics_info),
583            _phantom: PhantomData::<S>,
584        };
585
586        let mut input = input.execute();
587        let barrier = expect_first_barrier(&mut input).await?;
588        let first_epoch = barrier.epoch;
589        yield Message::Barrier(barrier);
590        this.state_table.init_epoch(first_epoch).await?;
591        if let Some(intermediate_state_table) = &mut this.intermediate_state_table {
592            intermediate_state_table.init_epoch(first_epoch).await?;
593        }
594
595        #[for_await]
596        for msg in input {
597            let msg = msg?;
598            match msg {
599                Message::Watermark(_) => {
600                    continue;
601                }
602                Message::Chunk(chunk) => {
603                    let output_chunk = Self::apply_chunk(&mut this, &mut vars, chunk).await?;
604                    if let Some(chunk) = output_chunk {
605                        yield Message::Chunk(chunk);
606                    }
607                    this.state_table.try_flush().await?;
608                    if let Some(intermediate_state_table) = &mut this.intermediate_state_table {
609                        intermediate_state_table.try_flush().await?;
610                    }
611                }
612                Message::Barrier(barrier) => {
613                    let post_commit = this.state_table.commit(barrier.epoch).await?;
614                    let intermediate_post_commit = if let Some(intermediate_state_table) =
615                        &mut this.intermediate_state_table
616                    {
617                        Some(intermediate_state_table.commit(barrier.epoch).await?)
618                    } else {
619                        None
620                    };
621
622                    vars.partitions.evict();
623
624                    let update_vnode_bitmap = barrier.as_update_vnode_bitmap(this.actor_ctx.id);
625                    yield Message::Barrier(barrier);
626
627                    let mut cache_may_stale = false;
628                    if let Some((_, stale)) = post_commit
629                        .post_yield_barrier(update_vnode_bitmap.clone())
630                        .await?
631                    {
632                        cache_may_stale = cache_may_stale || stale;
633                    }
634                    if let Some(intermediate_post_commit) = intermediate_post_commit
635                        && let Some((_, stale)) = intermediate_post_commit
636                            .post_yield_barrier(update_vnode_bitmap)
637                            .await?
638                    {
639                        cache_may_stale = cache_may_stale || stale;
640                    }
641                    if cache_may_stale {
642                        vars.partitions.clear();
643                    }
644                }
645            }
646        }
647    }
648}