Skip to main content

krabka_client_streams/dsl/
sliding_windowed_kgrouped.rs

1//! `SlidingWindowedKGroupedStream<K,V>`: the handle between
2//! `KGroupedStream::windowed_by_sliding(SlidingWindows)` and a terminal sliding
3//! (KIP-450) aggregation.
4//!
5//! This type is a sibling of
6//! [`crate::dsl::windowed_kgrouped::TimeWindowedKGroupedStream`]. It holds the
7//! same grouped lineage and window store. Its aggregate processor implements the
8//! KIP-450 left-window and right-window algorithm, and its windows are
9//! data-defined inclusive windows of size `time_difference`.
10use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
11
12use crate::{
13    dsl::{
14        builder::InternalStreamsBuilder,
15        config::Materialized,
16        emit::EmitStrategy,
17        graph::{GraphNodeKind, LowerState, NodeId},
18        kgrouped::{KGroupedStream, RepartitionLowerFn, mint_store_name},
19        ktable::{KTable, SuppressStoreFactory},
20        names,
21        processors::sliding_window_aggregate::{
22            KStreamSlidingWindowAggregateProcessor, KStreamSlidingWindowReduceProcessor,
23        },
24        windows::{SlidingWindows, TimeWindowedSerde, Windowed},
25    },
26    processor::serde::{DefaultSerde, Serde},
27    topology::NodeHandle,
28};
29
30/// Handle that [`KGroupedStream::windowed_by_sliding`] produces. Terminal sliding
31/// aggregations consume it.
32///
33/// [`KGroupedStream::windowed_by_sliding`]: crate::dsl::kgrouped::KGroupedStream::windowed_by_sliding
34pub struct SlidingWindowedKGroupedStream<K, V> {
35    builder: Rc<RefCell<InternalStreamsBuilder>>,
36    parent: NodeId,
37    key_changing_upstream: bool,
38    #[allow(dead_code)]
39    grouped_name: Option<String>,
40    repartition_lower: Option<RepartitionLowerFn>,
41    windows: SlidingWindows,
42    emit: EmitStrategy,
43    _pd: PhantomData<fn() -> (K, V)>,
44}
45
46impl<K, V> SlidingWindowedKGroupedStream<K, V>
47where
48    K: Any + Send + Sync + Clone,
49    V: Any + Send + Sync + Clone,
50{
51    pub(crate) fn new(
52        builder: Rc<RefCell<InternalStreamsBuilder>>,
53        parent: NodeId,
54        key_changing_upstream: bool,
55        grouped_name: Option<String>,
56        repartition_lower: Option<RepartitionLowerFn>,
57        windows: SlidingWindows,
58    ) -> Self {
59        Self {
60            builder,
61            parent,
62            key_changing_upstream,
63            grouped_name,
64            repartition_lower,
65            windows,
66            emit: EmitStrategy::default(),
67            _pd: PhantomData,
68        }
69    }
70
71    /// Emit on every update (default) or only on window close (KIP-825).
72    ///
73    /// In `on_window_close`, the operator drops a record whose window has already
74    /// closed, so it emits no duplicate final. The session variant differs: it
75    /// may re-emit under `grace < gap`.
76    #[must_use]
77    pub fn emit_strategy(mut self, emit: EmitStrategy) -> Self {
78        self.emit = emit;
79        self
80    }
81
82    pub fn count_explicit<KS, VS>(
83        self,
84        materialized: impl Into<Materialized<KS, VS>>,
85    ) -> KTable<Windowed<K>, i64, TimeWindowedSerde<KS>, VS>
86    where
87        KS: Serde<K> + Clone + 'static,
88        VS: Serde<i64> + Clone + 'static,
89    {
90        self.aggregate_inner(
91            materialized.into(),
92            names::AGGREGATE_STORE,
93            || 0i64,
94            |_k: &K, _v: &V, acc: i64| acc + 1,
95        )
96    }
97
98    pub fn reduce_explicit<KS, VS, R>(
99        self,
100        reducer: R,
101        materialized: impl Into<Materialized<KS, VS>>,
102    ) -> KTable<Windowed<K>, V, TimeWindowedSerde<KS>, VS>
103    where
104        KS: Serde<K> + Clone + 'static,
105        VS: Serde<V> + Clone + 'static,
106        R: Fn(&V, &V) -> V + Clone + Send + Sync + 'static,
107    {
108        let materialized = materialized.into();
109        let store_name = mint_store_name(&self.builder, &materialized, names::REDUCE_STORE);
110        self.lower_reduce::<KS, VS, R>(materialized, store_name, reducer)
111    }
112
113    pub fn aggregate_explicit<KS, VS, VA, I, A>(
114        self,
115        init: I,
116        agg: A,
117        materialized: impl Into<Materialized<KS, VS>>,
118    ) -> KTable<Windowed<K>, VA, TimeWindowedSerde<KS>, VS>
119    where
120        VA: Any + Send + Sync + Clone,
121        KS: Serde<K> + Clone + 'static,
122        VS: Serde<VA> + Clone + 'static,
123        I: Fn() -> VA + Clone + Send + Sync + 'static,
124        A: Fn(&K, &V, VA) -> VA + Clone + Send + Sync + 'static,
125    {
126        self.aggregate_inner(materialized.into(), names::AGGREGATE_STORE, init, agg)
127    }
128
129    pub fn count(
130        self,
131        store_name: impl Into<String>,
132    ) -> KTable<
133        Windowed<K>,
134        i64,
135        TimeWindowedSerde<<K as DefaultSerde>::Serde>,
136        crate::processor::serde::I64Serde,
137    >
138    where
139        K: DefaultSerde,
140        <K as DefaultSerde>::Serde: Serde<K> + Clone,
141    {
142        self.count_explicit(
143            Materialized::with(
144                <K as DefaultSerde>::Serde::default(),
145                crate::processor::serde::I64Serde,
146            )
147            .as_store(store_name),
148        )
149    }
150
151    pub fn reduce<R>(
152        self,
153        reducer: R,
154        store_name: impl Into<String>,
155    ) -> KTable<
156        Windowed<K>,
157        V,
158        TimeWindowedSerde<<K as DefaultSerde>::Serde>,
159        <V as DefaultSerde>::Serde,
160    >
161    where
162        K: DefaultSerde,
163        V: DefaultSerde,
164        <K as DefaultSerde>::Serde: Serde<K> + Clone,
165        <V as DefaultSerde>::Serde: Serde<V> + Clone,
166        R: Fn(&V, &V) -> V + Clone + Send + Sync + 'static,
167    {
168        self.reduce_explicit(
169            reducer,
170            Materialized::with(
171                <K as DefaultSerde>::Serde::default(),
172                <V as DefaultSerde>::Serde::default(),
173            )
174            .as_store(store_name),
175        )
176    }
177
178    pub fn aggregate<VA, I, A>(
179        self,
180        init: I,
181        agg: A,
182        store_name: impl Into<String>,
183    ) -> KTable<
184        Windowed<K>,
185        VA,
186        TimeWindowedSerde<<K as DefaultSerde>::Serde>,
187        <VA as DefaultSerde>::Serde,
188    >
189    where
190        VA: DefaultSerde + Any + Send + Clone,
191        K: DefaultSerde,
192        <K as DefaultSerde>::Serde: Serde<K> + Clone,
193        <VA as DefaultSerde>::Serde: Serde<VA> + Clone,
194        I: Fn() -> VA + Clone + Send + Sync + 'static,
195        A: Fn(&K, &V, VA) -> VA + Clone + Send + Sync + 'static,
196    {
197        self.aggregate_explicit(
198            init,
199            agg,
200            Materialized::with(
201                <K as DefaultSerde>::Serde::default(),
202                <VA as DefaultSerde>::Serde::default(),
203            )
204            .as_store(store_name),
205        )
206    }
207
208    fn aggregate_inner<KS, VS, VA, I, A>(
209        self,
210        materialized: Materialized<KS, VS>,
211        store_prefix: &'static str,
212        init: I,
213        agg: A,
214    ) -> KTable<Windowed<K>, VA, TimeWindowedSerde<KS>, VS>
215    where
216        VA: Any + Send + Sync + Clone,
217        KS: Serde<K> + Clone + 'static,
218        VS: Serde<VA> + Clone + 'static,
219        I: Fn() -> VA + Clone + Send + Sync + 'static,
220        A: Fn(&K, &V, VA) -> VA + Clone + Send + Sync + 'static,
221    {
222        let store_name = mint_store_name(&self.builder, &materialized, store_prefix);
223        self.lower_aggregate::<KS, VS, VA, I, A>(materialized, store_name, init, agg)
224    }
225
226    fn lower_aggregate<KS, VS, VA, I, A>(
227        mut self,
228        materialized: Materialized<KS, VS>,
229        store_name: String,
230        init: I,
231        agg: A,
232    ) -> KTable<Windowed<K>, VA, TimeWindowedSerde<KS>, VS>
233    where
234        VA: Any + Send + Sync + Clone,
235        KS: Serde<K> + Clone + 'static,
236        VS: Serde<VA> + Clone + 'static,
237        I: Fn() -> VA + Clone + Send + Sync + 'static,
238        A: Fn(&K, &V, VA) -> VA + Clone + Send + Sync + 'static,
239    {
240        let Materialized {
241            key_serde,
242            value_serde,
243            caching,
244            ..
245        } = materialized;
246        let suppress_factory = sliding_suppress_factory::<K, VA, KS, VS>(
247            key_serde.clone(),
248            value_serde.clone(),
249            self.windows,
250        );
251        let parent = self.parent;
252        let key_changing = self.key_changing_upstream;
253        let rp_lower = self.repartition_lower.take();
254        let windows = self.windows;
255        let emit = self.emit;
256        let mut g = self.builder.borrow_mut();
257        let agg_parent = KGroupedStream::<K, V>::record_repartition(
258            &mut g,
259            &store_name,
260            parent,
261            key_changing,
262            rp_lower,
263        );
264
265        let agg_name = g.new_processor_name(names::AGGREGATE);
266        let agg_id = g.graph.add(
267            agg_name.clone(),
268            GraphNodeKind::Aggregate {
269                store_name: store_name.clone(),
270                changelog: true,
271            },
272            vec![agg_parent],
273        );
274        let store_for_thunk = store_name.clone();
275        let key_serde_for_lower = key_serde.clone();
276        let value_serde_for_lower = value_serde.clone();
277        g.graph.nodes[agg_id].lower = Some(Box::new(move |state: &mut LowerState| {
278            let parent = NodeHandle::<K, V>::from_name(state.handle_name[&agg_parent].clone());
279            let store_for_proc = store_for_thunk.clone();
280            let h = state
281                .topology
282                .add_processor::<K, V, Windowed<K>, crate::dsl::processors::change::Change<VA>, _, _, _>(
283                    agg_name.clone(),
284                    move || KStreamSlidingWindowAggregateProcessor {
285                        store_name: store_for_proc.clone(),
286                        windows,
287                        init: init.clone(),
288                        agg: agg.clone(),
289                        stream_time: i64::MIN,
290                        emit,
291                        last_emitted_close: i64::MIN,
292                        forwarder: crate::dsl::processors::tuple_forwarder::TupleForwarder::default(),
293                        _pd: PhantomData,
294                    },
295                    [parent],
296                );
297            // Sliding window retention formula (JVM-exact):
298            // timeDifferenceMs + timeDifferenceMs + gracePeriodMs + 86_400_000
299            // (a sliding window spans [t - timeDiff, t + timeDiff] so the effective
300            // window size for changelog retention is 2 * timeDifferenceMs).
301            state.topology.add_window_store::<K, VA, KS, VS>(
302                store_for_thunk.clone(),
303                key_serde_for_lower.clone(),
304                value_serde_for_lower.clone(),
305                // Retention basis = 2 * timeDiff (the [t-timeDiff, t+timeDiff] span);
306                // the true window size for the key end is 1 * timeDiff.
307                (
308                    windows.time_difference * 2.0,
309                    windows.time_difference,
310                    windows.grace,
311                ),
312                [h.name().to_string()],
313            );
314            // Cache only emit-on-update sliding aggregates: emit-final stays
315            // uncached or the flush would emit the per-update changes emit-final
316            // suppresses.
317            state
318                .topology
319                .mark_store_caching(&store_for_thunk, caching && emit.is_on_update());
320            state.handle_name.insert(agg_id, h.name().to_string());
321        }));
322
323        drop(g);
324        KTable::new(
325            Rc::clone(&self.builder),
326            agg_id,
327            Some(store_name),
328            None,
329            TimeWindowedSerde::new(key_serde.clone(), windows.time_difference),
330            value_serde.clone(),
331        )
332        .with_window_grace(Some(windows.grace))
333        .with_suppress_factory(Some(suppress_factory))
334    }
335
336    fn lower_reduce<KS, VS, R>(
337        mut self,
338        materialized: Materialized<KS, VS>,
339        store_name: String,
340        reducer: R,
341    ) -> KTable<Windowed<K>, V, TimeWindowedSerde<KS>, VS>
342    where
343        KS: Serde<K> + Clone + 'static,
344        VS: Serde<V> + Clone + 'static,
345        R: Fn(&V, &V) -> V + Clone + Send + Sync + 'static,
346    {
347        let Materialized {
348            key_serde,
349            value_serde,
350            caching,
351            ..
352        } = materialized;
353        let suppress_factory = sliding_suppress_factory::<K, V, KS, VS>(
354            key_serde.clone(),
355            value_serde.clone(),
356            self.windows,
357        );
358        let parent = self.parent;
359        let key_changing = self.key_changing_upstream;
360        let rp_lower = self.repartition_lower.take();
361        let windows = self.windows;
362        let emit = self.emit;
363        let mut g = self.builder.borrow_mut();
364        let agg_parent = KGroupedStream::<K, V>::record_repartition(
365            &mut g,
366            &store_name,
367            parent,
368            key_changing,
369            rp_lower,
370        );
371
372        let red_name = g.new_processor_name(names::REDUCE);
373        let red_id = g.graph.add(
374            red_name.clone(),
375            GraphNodeKind::Aggregate {
376                store_name: store_name.clone(),
377                changelog: true,
378            },
379            vec![agg_parent],
380        );
381        let store_for_thunk = store_name.clone();
382        let key_serde_for_lower = key_serde.clone();
383        let value_serde_for_lower = value_serde.clone();
384        g.graph.nodes[red_id].lower = Some(Box::new(move |state: &mut LowerState| {
385            let parent = NodeHandle::<K, V>::from_name(state.handle_name[&agg_parent].clone());
386            let store_for_proc = store_for_thunk.clone();
387            let reducer = reducer.clone();
388            let h = state
389                .topology
390                .add_processor::<K, V, Windowed<K>, crate::dsl::processors::change::Change<V>, _, _, _>(
391                    red_name.clone(),
392                    move || KStreamSlidingWindowReduceProcessor {
393                        store_name: store_for_proc.clone(),
394                        windows,
395                        reducer: reducer.clone(),
396                        stream_time: i64::MIN,
397                        emit,
398                        last_emitted_close: i64::MIN,
399                        forwarder: crate::dsl::processors::tuple_forwarder::TupleForwarder::default(),
400                        _pd: PhantomData,
401                    },
402                    [parent],
403                );
404            // Sliding window retention formula (JVM-exact): 2 * timeDifferenceMs + grace + 1day.
405            state.topology.add_window_store::<K, V, KS, VS>(
406                store_for_thunk.clone(),
407                key_serde_for_lower.clone(),
408                value_serde_for_lower.clone(),
409                // Retention basis = 2 * timeDiff (the [t-timeDiff, t+timeDiff] span);
410                // the true window size for the key end is 1 * timeDiff.
411                (
412                    windows.time_difference * 2.0,
413                    windows.time_difference,
414                    windows.grace,
415                ),
416                [h.name().to_string()],
417            );
418            // Cache only emit-on-update sliding reduces (see aggregate lower).
419            state
420                .topology
421                .mark_store_caching(&store_for_thunk, caching && emit.is_on_update());
422            state.handle_name.insert(red_id, h.name().to_string());
423        }));
424
425        drop(g);
426        KTable::new(
427            Rc::clone(&self.builder),
428            red_id,
429            Some(store_name),
430            None,
431            TimeWindowedSerde::new(key_serde.clone(), windows.time_difference),
432            value_serde.clone(),
433        )
434        .with_window_grace(Some(windows.grace))
435        .with_suppress_factory(Some(suppress_factory))
436    }
437}
438
439pub(crate) fn sliding_suppress_factory<K, VA, KS, VS>(
440    key_serde: KS,
441    value_serde: VS,
442    windows: SlidingWindows,
443) -> SuppressStoreFactory
444where
445    K: Any + Send + Sync + Clone,
446    VA: Any + Send + Clone,
447    KS: Serde<K> + Clone + 'static,
448    VS: Serde<VA> + Clone + 'static,
449{
450    std::sync::Arc::new(
451        move |state: &mut LowerState, store_name: &str, proc_name: &str, logging: bool| {
452            state
453                .topology
454                .add_suppress_store::<Windowed<K>, VA, TimeWindowedSerde<KS>, VS>(
455                    store_name.to_string(),
456                    TimeWindowedSerde::new(key_serde.clone(), windows.time_difference),
457                    value_serde.clone(),
458                    logging,
459                    [proc_name.to_string()],
460                );
461        },
462    )
463}