1use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
21
22use crate::{
23 dsl::{
24 builder::InternalStreamsBuilder,
25 config::Materialized,
26 emit::EmitStrategy,
27 graph::{GraphNodeKind, LowerState, NodeId},
28 kgrouped::{KGroupedStream, RepartitionLowerFn, mint_store_name},
29 ktable::{KTable, SuppressStoreFactory},
30 names,
31 processors::window_aggregate::{
32 KStreamWindowAggregateProcessor, KStreamWindowReduceProcessor,
33 },
34 windows::{TimeWindowedSerde, TimeWindows, Windowed},
35 },
36 processor::serde::{DefaultSerde, Serde},
37 topology::NodeHandle,
38};
39
40pub struct TimeWindowedKGroupedStream<K, V> {
45 builder: Rc<RefCell<InternalStreamsBuilder>>,
46 parent: NodeId,
49 key_changing_upstream: bool,
52 #[allow(dead_code)]
54 grouped_name: Option<String>,
55 repartition_lower: Option<RepartitionLowerFn>,
57 windows: TimeWindows,
60 emit: EmitStrategy,
62 _pd: PhantomData<fn() -> (K, V)>,
63}
64
65impl<K, V> TimeWindowedKGroupedStream<K, V>
66where
67 K: Any + Send + Sync + Clone,
68 V: Any + Send + Clone,
69{
70 pub(crate) fn new(
71 builder: Rc<RefCell<InternalStreamsBuilder>>,
72 parent: NodeId,
73 key_changing_upstream: bool,
74 grouped_name: Option<String>,
75 repartition_lower: Option<RepartitionLowerFn>,
76 windows: TimeWindows,
77 ) -> Self {
78 Self {
79 builder,
80 parent,
81 key_changing_upstream,
82 grouped_name,
83 repartition_lower,
84 windows,
85 emit: EmitStrategy::default(),
86 _pd: PhantomData,
87 }
88 }
89
90 #[must_use]
96 pub fn emit_strategy(mut self, emit: EmitStrategy) -> Self {
97 self.emit = emit;
98 self
99 }
100
101 pub fn count_explicit<KS, VS>(
104 self,
105 materialized: impl Into<Materialized<KS, VS>>,
106 ) -> KTable<Windowed<K>, i64, TimeWindowedSerde<KS>, VS>
107 where
108 KS: Serde<K> + Clone + 'static,
109 VS: Serde<i64> + Clone + 'static,
110 {
111 self.aggregate_inner_windowed(
112 materialized.into(),
113 names::AGGREGATE_STORE,
114 || 0i64,
115 |_k: &K, _v: &V, acc: i64| acc + 1,
116 )
117 }
118
119 pub fn reduce_explicit<KS, VS, R>(
125 self,
126 reducer: R,
127 materialized: impl Into<Materialized<KS, VS>>,
128 ) -> KTable<Windowed<K>, V, TimeWindowedSerde<KS>, VS>
129 where
130 KS: Serde<K> + Clone + 'static,
131 VS: Serde<V> + Clone + 'static,
132 R: Fn(&V, &V) -> V + Clone + Send + Sync + 'static,
133 {
134 let materialized = materialized.into();
135 let store_name = mint_store_name(&self.builder, &materialized, names::REDUCE_STORE);
136 self.lower_reduce_windowed::<KS, VS, R>(materialized, store_name, reducer)
137 }
138
139 pub fn aggregate_explicit<KS, VS, VA, I, A>(
142 self,
143 init: I,
144 agg: A,
145 materialized: impl Into<Materialized<KS, VS>>,
146 ) -> KTable<Windowed<K>, VA, TimeWindowedSerde<KS>, VS>
147 where
148 VA: Any + Send + Clone,
149 KS: Serde<K> + Clone + 'static,
150 VS: Serde<VA> + Clone + 'static,
151 I: Fn() -> VA + Clone + Send + Sync + 'static,
152 A: Fn(&K, &V, VA) -> VA + Clone + Send + Sync + 'static,
153 {
154 self.aggregate_inner_windowed(materialized.into(), names::AGGREGATE_STORE, init, agg)
155 }
156
157 pub fn count(
158 self,
159 store_name: impl Into<String>,
160 ) -> KTable<
161 Windowed<K>,
162 i64,
163 TimeWindowedSerde<<K as DefaultSerde>::Serde>,
164 crate::processor::serde::I64Serde,
165 >
166 where
167 K: DefaultSerde,
168 <K as DefaultSerde>::Serde: Serde<K> + Clone,
169 {
170 self.count_explicit(
171 Materialized::with(
172 <K as DefaultSerde>::Serde::default(),
173 crate::processor::serde::I64Serde,
174 )
175 .as_store(store_name),
176 )
177 }
178
179 pub fn reduce<R>(
180 self,
181 reducer: R,
182 store_name: impl Into<String>,
183 ) -> KTable<
184 Windowed<K>,
185 V,
186 TimeWindowedSerde<<K as DefaultSerde>::Serde>,
187 <V as DefaultSerde>::Serde,
188 >
189 where
190 K: DefaultSerde,
191 V: DefaultSerde,
192 <K as DefaultSerde>::Serde: Serde<K> + Clone,
193 <V as DefaultSerde>::Serde: Serde<V> + Clone,
194 R: Fn(&V, &V) -> V + Clone + Send + Sync + 'static,
195 {
196 self.reduce_explicit(
197 reducer,
198 Materialized::with(
199 <K as DefaultSerde>::Serde::default(),
200 <V as DefaultSerde>::Serde::default(),
201 )
202 .as_store(store_name),
203 )
204 }
205
206 pub fn aggregate<VA, I, A>(
207 self,
208 init: I,
209 agg: A,
210 store_name: impl Into<String>,
211 ) -> KTable<
212 Windowed<K>,
213 VA,
214 TimeWindowedSerde<<K as DefaultSerde>::Serde>,
215 <VA as DefaultSerde>::Serde,
216 >
217 where
218 VA: DefaultSerde + Any + Send + Clone,
219 K: DefaultSerde,
220 <K as DefaultSerde>::Serde: Serde<K> + Clone,
221 <VA as DefaultSerde>::Serde: Serde<VA> + Clone,
222 I: Fn() -> VA + Clone + Send + Sync + 'static,
223 A: Fn(&K, &V, VA) -> VA + Clone + Send + Sync + 'static,
224 {
225 self.aggregate_explicit(
226 init,
227 agg,
228 Materialized::with(
229 <K as DefaultSerde>::Serde::default(),
230 <VA as DefaultSerde>::Serde::default(),
231 )
232 .as_store(store_name),
233 )
234 }
235
236 fn aggregate_inner_windowed<KS, VS, VA, I, A>(
246 self,
247 materialized: Materialized<KS, VS>,
248 store_prefix: &'static str,
249 init: I,
250 agg: A,
251 ) -> KTable<Windowed<K>, VA, TimeWindowedSerde<KS>, VS>
252 where
253 VA: Any + Send + Clone,
254 KS: Serde<K> + Clone + 'static,
255 VS: Serde<VA> + Clone + 'static,
256 I: Fn() -> VA + Clone + Send + Sync + 'static,
257 A: Fn(&K, &V, VA) -> VA + Clone + Send + Sync + 'static,
258 {
259 let store_name = mint_store_name(&self.builder, &materialized, store_prefix);
260 self.lower_aggregate_windowed::<KS, VS, VA, I, A>(materialized, store_name, init, agg)
261 }
262
263 fn lower_aggregate_windowed<KS, VS, VA, I, A>(
269 mut self,
270 materialized: Materialized<KS, VS>,
271 store_name: String,
272 init: I,
273 agg: A,
274 ) -> KTable<Windowed<K>, VA, TimeWindowedSerde<KS>, VS>
275 where
276 VA: Any + Send + Clone,
277 KS: Serde<K> + Clone + 'static,
278 VS: Serde<VA> + Clone + 'static,
279 I: Fn() -> VA + Clone + Send + Sync + 'static,
280 A: Fn(&K, &V, VA) -> VA + Clone + Send + Sync + 'static,
281 {
282 let Materialized {
283 key_serde,
284 value_serde,
285 caching,
286 ..
287 } = materialized;
288 let suppress_factory = windowed_suppress_factory::<K, VA, KS, VS>(
292 key_serde.clone(),
293 value_serde.clone(),
294 self.windows,
295 );
296 let parent = self.parent;
297 let key_changing = self.key_changing_upstream;
298 let rp_lower = self.repartition_lower.take();
299 let windows = self.windows;
300 let emit = self.emit;
301 let mut g = self.builder.borrow_mut();
302 let agg_parent = KGroupedStream::<K, V>::record_repartition(
303 &mut g,
304 &store_name,
305 parent,
306 key_changing,
307 rp_lower,
308 );
309
310 let agg_name = g.new_processor_name(names::AGGREGATE);
311 let agg_id = g.graph.add(
312 agg_name.clone(),
313 GraphNodeKind::Aggregate {
314 store_name: store_name.clone(),
315 changelog: true,
316 },
317 vec![agg_parent],
318 );
319 let store_for_thunk = store_name.clone();
320 let key_serde_for_lower = key_serde.clone();
321 let value_serde_for_lower = value_serde.clone();
322 g.graph.nodes[agg_id].lower = Some(Box::new(move |state: &mut LowerState| {
323 let parent = NodeHandle::<K, V>::from_name(state.handle_name[&agg_parent].clone());
324 let store_for_proc = store_for_thunk.clone();
325 let h = state
327 .topology
328 .add_processor::<K, V, Windowed<K>, crate::dsl::processors::change::Change<VA>, _, _, _>(
329 agg_name.clone(),
330 move || KStreamWindowAggregateProcessor {
331 store_name: store_for_proc.clone(),
332 windows,
333 init: init.clone(),
334 agg: agg.clone(),
335 emit,
336 stream_time: i64::MIN,
337 last_emitted_close: i64::MIN,
338 forwarder: crate::dsl::processors::tuple_forwarder::TupleForwarder::default(),
339 _pd: PhantomData,
340 },
341 [parent],
342 );
343 state.topology.add_window_store::<K, VA, KS, VS>(
345 store_for_thunk.clone(),
346 key_serde_for_lower.clone(),
347 value_serde_for_lower.clone(),
348 (windows.size, windows.size, windows.grace),
350 [h.name().to_string()],
351 );
352 state
356 .topology
357 .mark_store_caching(&store_for_thunk, caching && emit.is_on_update());
358 state.handle_name.insert(agg_id, h.name().to_string());
359 }));
360
361 drop(g);
362 KTable::new(
363 Rc::clone(&self.builder),
364 agg_id,
365 Some(store_name),
366 None,
367 TimeWindowedSerde::new(key_serde.clone(), windows.size),
368 value_serde.clone(),
369 )
370 .with_window_grace(Some(windows.grace))
371 .with_suppress_factory(Some(suppress_factory))
372 }
373
374 fn lower_reduce_windowed<KS, VS, R>(
378 mut self,
379 materialized: Materialized<KS, VS>,
380 store_name: String,
381 reducer: R,
382 ) -> KTable<Windowed<K>, V, TimeWindowedSerde<KS>, VS>
383 where
384 KS: Serde<K> + Clone + 'static,
385 VS: Serde<V> + Clone + 'static,
386 R: Fn(&V, &V) -> V + Clone + Send + Sync + 'static,
387 {
388 let Materialized {
389 key_serde,
390 value_serde,
391 caching,
392 ..
393 } = materialized;
394 let suppress_factory = windowed_suppress_factory::<K, V, KS, VS>(
395 key_serde.clone(),
396 value_serde.clone(),
397 self.windows,
398 );
399 let parent = self.parent;
400 let key_changing = self.key_changing_upstream;
401 let rp_lower = self.repartition_lower.take();
402 let windows = self.windows;
403 let emit = self.emit;
404 let mut g = self.builder.borrow_mut();
405 let agg_parent = KGroupedStream::<K, V>::record_repartition(
406 &mut g,
407 &store_name,
408 parent,
409 key_changing,
410 rp_lower,
411 );
412
413 let red_name = g.new_processor_name(names::REDUCE);
414 let red_id = g.graph.add(
415 red_name.clone(),
416 GraphNodeKind::Aggregate {
417 store_name: store_name.clone(),
418 changelog: true,
419 },
420 vec![agg_parent],
421 );
422 let store_for_thunk = store_name.clone();
423 let key_serde_for_lower = key_serde.clone();
424 let value_serde_for_lower = value_serde.clone();
425 g.graph.nodes[red_id].lower = Some(Box::new(move |state: &mut LowerState| {
426 let parent = NodeHandle::<K, V>::from_name(state.handle_name[&agg_parent].clone());
427 let store_for_proc = store_for_thunk.clone();
428 let reducer = reducer.clone();
429 let h = state
431 .topology
432 .add_processor::<K, V, Windowed<K>, crate::dsl::processors::change::Change<V>, _, _, _>(
433 red_name.clone(),
434 move || KStreamWindowReduceProcessor {
435 store_name: store_for_proc.clone(),
436 windows,
437 reducer: reducer.clone(),
438 emit,
439 stream_time: i64::MIN,
440 last_emitted_close: i64::MIN,
441 forwarder: crate::dsl::processors::tuple_forwarder::TupleForwarder::default(),
442 _pd: PhantomData,
443 },
444 [parent],
445 );
446 state.topology.add_window_store::<K, V, KS, VS>(
447 store_for_thunk.clone(),
448 key_serde_for_lower.clone(),
449 value_serde_for_lower.clone(),
450 (windows.size, windows.size, windows.grace),
452 [h.name().to_string()],
453 );
454 state
456 .topology
457 .mark_store_caching(&store_for_thunk, caching && emit.is_on_update());
458 state.handle_name.insert(red_id, h.name().to_string());
459 }));
460
461 drop(g);
462 KTable::new(
463 Rc::clone(&self.builder),
464 red_id,
465 Some(store_name),
466 None,
467 TimeWindowedSerde::new(key_serde.clone(), windows.size),
468 value_serde.clone(),
469 )
470 .with_window_grace(Some(windows.grace))
471 .with_suppress_factory(Some(suppress_factory))
472 }
473}
474
475pub(crate) fn windowed_suppress_factory<K, VA, KS, VS>(
480 key_serde: KS,
481 value_serde: VS,
482 windows: TimeWindows,
483) -> SuppressStoreFactory
484where
485 K: Any + Send + Sync + Clone,
486 VA: Any + Send + Clone,
487 KS: Serde<K> + Clone + 'static,
488 VS: Serde<VA> + Clone + 'static,
489{
490 std::sync::Arc::new(
491 move |state: &mut LowerState, store_name: &str, proc_name: &str, logging: bool| {
492 state
495 .topology
496 .add_suppress_store::<Windowed<K>, VA, TimeWindowedSerde<KS>, VS>(
497 store_name.to_string(),
498 TimeWindowedSerde::new(key_serde.clone(), windows.size),
499 value_serde.clone(),
500 logging,
501 [proc_name.to_string()],
502 );
503 },
504 )
505}
506
507#[cfg(test)]
508mod tests {
509 use assert2::check;
510 use krabka_units::prelude::*;
511
512 use crate::{
513 dsl::{
514 StreamsBuilder,
515 emit::EmitStrategy,
516 windows::{TimeWindowedSerde, TimeWindows, Window, Windowed},
517 },
518 processor::serde::{Consumed, I64Serde, Produced, StringSerde},
519 test_driver::TopologyTestDriver,
520 };
521
522 #[test]
525 fn emit_on_update_window_store_is_cached() {
526 let b = StreamsBuilder::new();
527 b.stream::<String, String>(["in"])
528 .group_by_key()
529 .windowed_by(TimeWindows::of_size(millis(10)))
530 .count("w");
531 let built = b.build("app").unwrap();
532 let g = pollster::block_on(built.instantiate(
533 &crate::store::backend::StoreBackend::InMemory,
534 "app",
535 mebibytes(10),
536 ))
537 .unwrap();
538 check!(
539 g.cache_owner.contains_key("w"),
540 "emit-on-update window store must be cached, cache_owner = {:?}",
541 g.cache_owner
542 );
543 }
544
545 #[test]
550 fn emit_final_window_store_is_not_cached() {
551 let b = StreamsBuilder::new();
552 b.stream::<String, String>(["in"])
553 .group_by_key()
554 .windowed_by(TimeWindows::of_size(millis(10)))
555 .emit_strategy(EmitStrategy::on_window_close())
556 .count("w");
557 let built = b.build("app").unwrap();
558 let g = pollster::block_on(built.instantiate(
559 &crate::store::backend::StoreBackend::InMemory,
560 "app",
561 mebibytes(10),
562 ))
563 .unwrap();
564 check!(
565 !g.cache_owner.contains_key("w"),
566 "emit-final window store must NOT be cached, cache_owner = {:?}",
567 g.cache_owner
568 );
569 }
570
571 #[test]
579 fn dsl_windowed_count_emit_final_emits_once_on_close() {
580 let b = StreamsBuilder::new();
581 b.stream::<String, String>(["in"])
582 .group_by_key()
583 .windowed_by(TimeWindows::of_size(millis(10)))
584 .emit_strategy(EmitStrategy::on_window_close())
585 .count("w")
586 .to_stream()
587 .to_explicit(
588 "out",
589 Produced::with(TimeWindowedSerde::new(StringSerde, millis(10)), I64Serde),
590 );
591 let built = b.build("app").unwrap();
592 let mut d = TopologyTestDriver::new(&built).unwrap();
593 for ts in [1, 4, 12] {
594 d.pipe_input(
595 "in",
596 Consumed::with(StringSerde, StringSerde),
597 Some("a".to_string()),
598 "x".to_string(),
599 ts,
600 );
601 }
602 let p = || Produced::with(TimeWindowedSerde::new(StringSerde, millis(10)), I64Serde);
603 assert_eq!(
604 d.read_output("out", p()),
605 Some((
606 Some(Windowed {
607 key: "a".into(),
608 window: Window { start: 0, end: 10 }
609 }),
610 2
611 )),
612 "emit-final forwards window [0,10) with final count 2 on close"
613 );
614 assert_eq!(
616 d.read_output("out", p()),
617 None,
618 "exactly one emit-final record"
619 );
620 }
621}