1use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc, sync::Arc};
19
20use crate::{
21 dsl::{
22 builder::InternalStreamsBuilder,
23 config::Materialized,
24 graph::{GraphNodeKind, LowerState, NodeId},
25 kgrouped::{KGroupedStream, RepartitionLowerFn, mint_store_name},
26 ktable::KTable,
27 names,
28 processors::{
29 aggregate::KStreamAggregateProcessor, change::Change,
30 cogroup_merge::KStreamPassThrough, tuple_forwarder::TupleForwarder,
31 },
32 },
33 processor::serde::{DefaultSerde, Serde},
34 topology::NodeHandle,
35};
36
37#[derive(Clone)]
44pub(crate) enum CogroupKind {
45 NonWindowed,
46 Time(crate::dsl::windows::TimeWindows),
47 Sliding(crate::dsl::windows::SlidingWindows),
48 Session(crate::dsl::windows::SessionWindows),
49}
50
51type CogroupInitializer<VOut> = Arc<dyn Fn() -> VOut + Send + Sync>;
57type CogroupMerger<K, VOut> = Arc<dyn Fn(&K, VOut, VOut) -> VOut + Send + Sync>;
58
59#[allow(dead_code)]
60pub(crate) struct CogroupSpec<K, VOut> {
61 pub kind: CogroupKind,
62 pub init: CogroupInitializer<VOut>,
63 pub merger: Option<CogroupMerger<K, VOut>>,
64}
65
66impl<K, VOut> Clone for CogroupSpec<K, VOut> {
67 fn clone(&self) -> Self {
68 Self {
69 kind: self.kind.clone(),
70 init: self.init.clone(),
71 merger: self.merger.clone(),
72 }
73 }
74}
75
76type AggNodeThunk = Box<dyn FnOnce(&mut LowerState, String, String, String) -> String + Send>;
80pub(crate) type MakeAggFn<K, VOut> = Box<dyn FnOnce(CogroupSpec<K, VOut>) -> AggNodeThunk + Send>;
81
82#[allow(dead_code)]
85pub(crate) struct CogroupInput<K, VOut> {
86 pub parent: NodeId,
87 pub key_changing_upstream: bool,
88 pub repartition_lower: Option<RepartitionLowerFn>,
89 pub make_agg: MakeAggFn<K, VOut>,
90 pub source_topic: Option<String>,
94}
95
96pub struct CogroupedKStream<K, VOut> {
103 pub(crate) builder: Rc<RefCell<InternalStreamsBuilder>>,
104 pub(crate) inputs: Vec<CogroupInput<K, VOut>>,
105 _pd: PhantomData<fn() -> (K, VOut)>,
106}
107
108impl<K, VOut> CogroupedKStream<K, VOut> {
109 pub(crate) fn new(
112 builder: Rc<RefCell<InternalStreamsBuilder>>,
113 inputs: Vec<CogroupInput<K, VOut>>,
114 ) -> Self {
115 Self {
116 builder,
117 inputs,
118 _pd: PhantomData,
119 }
120 }
121}
122
123pub(crate) fn make_agg_for_input<K, Vn, VOut, A>(agg: A) -> MakeAggFn<K, VOut>
130where
131 K: Any + Send + Sync + Clone,
132 Vn: Any + Send + Sync + Clone,
133 VOut: Any + Send + Sync + Clone,
134 A: Fn(&K, &Vn, VOut) -> VOut + Send + Sync + 'static,
135{
136 let agg = Arc::new(agg);
137 Box::new(move |spec: CogroupSpec<K, VOut>| -> AggNodeThunk {
138 Box::new(
139 move |state: &mut LowerState,
140 parent_name: String,
141 proc_name: String,
142 store_name: String|
143 -> String {
144 let parent = NodeHandle::<K, Vn>::from_name(parent_name);
145 let init = spec.init.clone();
146 match spec.kind {
147 CogroupKind::NonWindowed => {
148 let agg = agg.clone();
149 let store = store_name.clone();
150 let h = state
151 .topology
152 .add_processor::<K, Vn, K, Change<VOut>, _, _, _>(
153 proc_name,
154 move || KStreamAggregateProcessor {
155 store_name: store.clone(),
156 init: {
157 let i = init.clone();
158 move || i()
159 },
160 agg: {
161 let a = agg.clone();
162 move |k: &K, v: &Vn, acc: VOut| a(k, v, acc)
163 },
164 forwarder: TupleForwarder::default(),
165 _pd: PhantomData,
166 },
167 [parent],
168 );
169 h.name().to_string()
170 }
171 CogroupKind::Time(w) => {
172 use crate::dsl::{
173 processors::window_aggregate::KStreamWindowAggregateProcessor,
174 windows::Windowed,
175 };
176 let agg = agg.clone();
177 let store = store_name.clone();
178 let h = state
179 .topology
180 .add_processor::<K, Vn, Windowed<K>, Change<VOut>, _, _, _>(
181 proc_name,
182 move || KStreamWindowAggregateProcessor {
183 store_name: store.clone(),
184 windows: w,
185 init: {
186 let i = init.clone();
187 move || i()
188 },
189 agg: {
190 let a = agg.clone();
191 move |k: &K, v: &Vn, acc: VOut| a(k, v, acc)
192 },
193 emit: crate::dsl::emit::EmitStrategy::default(),
195 stream_time: i64::MIN,
196 last_emitted_close: i64::MIN,
197 forwarder: TupleForwarder::default(),
198 _pd: PhantomData,
199 },
200 [parent],
201 );
202 h.name().to_string()
203 }
204 CogroupKind::Sliding(w) => {
205 use crate::dsl::{
206 processors::sliding_window_aggregate::KStreamSlidingWindowAggregateProcessor,
207 windows::Windowed,
208 };
209 let agg = agg.clone();
210 let store = store_name.clone();
211 let h = state
212 .topology
213 .add_processor::<K, Vn, Windowed<K>, Change<VOut>, _, _, _>(
214 proc_name,
215 move || KStreamSlidingWindowAggregateProcessor {
216 store_name: store.clone(),
217 windows: w,
218 init: {
219 let i = init.clone();
220 move || i()
221 },
222 agg: {
223 let a = agg.clone();
224 move |k: &K, v: &Vn, acc: VOut| a(k, v, acc)
225 },
226 stream_time: i64::MIN,
227 emit: crate::dsl::emit::EmitStrategy::default(),
229 last_emitted_close: i64::MIN,
230 forwarder: TupleForwarder::default(),
231 _pd: PhantomData,
232 },
233 [parent],
234 );
235 h.name().to_string()
236 }
237 CogroupKind::Session(w) => {
238 use crate::dsl::{
239 processors::session_aggregate::KStreamSessionAggregateProcessor,
240 windows::Windowed,
241 };
242 let agg = agg.clone();
243 let store = store_name.clone();
244 let merger = spec
245 .merger
246 .clone()
247 .expect("session cogroup requires a merger");
248 let h = state
249 .topology
250 .add_processor::<K, Vn, Windowed<K>, Change<VOut>, _, _, _>(
251 proc_name,
252 move || KStreamSessionAggregateProcessor {
253 store_name: store.clone(),
254 gap: w.gap,
255 init: {
256 let i = init.clone();
257 move || i()
258 },
259 agg: {
260 let a = agg.clone();
261 move |k: &K, v: &Vn, acc: VOut| a(k, v, acc)
262 },
263 merger: {
264 let m = merger.clone();
265 move |k: &K, a: VOut, b: VOut| m(k, a, b)
266 },
267 emit: crate::dsl::emit::EmitStrategy::default(),
269 grace: w.grace,
270 stream_time: i64::MIN,
271 last_emitted_close: i64::MIN,
272 forwarder: TupleForwarder::default(),
273 _pd: PhantomData,
274 },
275 [parent],
276 );
277 h.name().to_string()
278 }
279 }
280 },
281 )
282 })
283}
284
285impl<K, VOut> CogroupedKStream<K, VOut>
286where
287 K: Any + Send + Sync + Clone,
288 VOut: Any + Send + Sync + Clone,
289{
290 #[must_use]
292 pub fn cogroup<Vn, A>(mut self, grouped: KGroupedStream<K, Vn>, agg: A) -> Self
293 where
294 Vn: Any + Send + Sync + Clone,
295 A: Fn(&K, &Vn, VOut) -> VOut + Send + Sync + 'static,
296 {
297 let (parent, key_changing, rp_lower, source_topic) = grouped.into_cogroup_parts();
298 self.inputs.push(CogroupInput {
299 parent,
300 key_changing_upstream: key_changing,
301 repartition_lower: rp_lower,
302 make_agg: make_agg_for_input::<K, Vn, VOut, A>(agg),
303 source_topic,
304 });
305 self
306 }
307
308 pub fn aggregate_explicit<KS, VS, I>(
310 self,
311 init: I,
312 materialized: impl Into<Materialized<KS, VS>>,
313 ) -> KTable<K, VOut, KS, VS>
314 where
315 KS: Serde<K> + Clone + 'static,
316 VS: Serde<VOut> + Clone + 'static,
317 I: Fn() -> VOut + Send + Sync + 'static,
318 {
319 let materialized = materialized.into();
320 let store_name = mint_store_name(&self.builder, &materialized, names::AGGREGATE_STORE);
321 let Materialized {
322 key_serde,
323 value_serde,
324 logging,
325 caching,
326 ..
327 } = materialized;
328 let spec = CogroupSpec::<K, VOut> {
329 kind: CogroupKind::NonWindowed,
330 init: Arc::new(init),
331 merger: None,
332 };
333 let ks = key_serde.clone();
334 let vs = value_serde.clone();
335 let store_for_reg = store_name.clone();
336 let registrar: StoreRegistrarFn = Box::new(move |state, procs| {
338 if logging {
339 state.topology.add_state_store::<K, VOut, KS, VS>(
340 store_for_reg.clone(),
341 ks.clone(),
342 vs.clone(),
343 procs,
344 );
345 } else {
346 state
347 .topology
348 .add_state_store_no_changelog::<K, VOut, KS, VS>(
349 store_for_reg.clone(),
350 ks.clone(),
351 vs.clone(),
352 );
353 }
354 state.topology.mark_store_caching(&store_for_reg, caching);
358 });
359 let suppress = crate::dsl::ktable::kv_suppress_factory::<K, VOut, KS, VS>(
360 key_serde.clone(),
361 value_serde.clone(),
362 );
363 let merge_id = lower_cogroup::<K, VOut, K>(
364 &self.builder,
365 self.inputs,
366 &store_name,
367 &spec,
368 logging,
369 registrar,
370 );
371 KTable::new(
372 Rc::clone(&self.builder),
373 merge_id,
374 Some(store_name),
375 None,
376 key_serde,
377 value_serde,
378 )
379 .with_suppress_factory(Some(suppress))
380 }
381
382 pub fn aggregate<I>(
384 self,
385 init: I,
386 store_name: impl Into<String>,
387 ) -> KTable<K, VOut, <K as DefaultSerde>::Serde, <VOut as DefaultSerde>::Serde>
388 where
389 K: DefaultSerde,
390 VOut: DefaultSerde,
391 <K as DefaultSerde>::Serde: Serde<K> + Clone,
392 <VOut as DefaultSerde>::Serde: Serde<VOut> + Clone,
393 I: Fn() -> VOut + Send + Sync + 'static,
394 {
395 self.aggregate_explicit(
396 init,
397 Materialized::with(
398 <K as DefaultSerde>::Serde::default(),
399 <VOut as DefaultSerde>::Serde::default(),
400 )
401 .as_store(store_name),
402 )
403 }
404}
405
406pub(crate) type StoreRegistrarFn = Box<dyn FnOnce(&mut LowerState, Vec<String>) + Send>;
410
411pub(crate) fn lower_cogroup<K, VOut, KOut>(
420 builder: &Rc<RefCell<InternalStreamsBuilder>>,
421 inputs: Vec<CogroupInput<K, VOut>>,
422 store_name: &str,
423 spec: &CogroupSpec<K, VOut>,
424 logging: bool,
425 registrar: StoreRegistrarFn,
426) -> NodeId
427where
428 K: Any + Send + Sync + Clone,
429 VOut: Any + Send + Sync + Clone,
430 KOut: Any + Send + Clone,
431{
432 let mut g = builder.borrow_mut();
433 let mut agg_ids: Vec<NodeId> = Vec::with_capacity(inputs.len());
434 let copartition_sources: Vec<String> = inputs
437 .iter()
438 .filter_map(|i| i.source_topic.clone())
439 .collect();
440 for input in inputs {
441 let CogroupInput {
442 parent,
443 key_changing_upstream,
444 repartition_lower,
445 make_agg,
446 source_topic: _,
447 } = input;
448 let agg_parent = KGroupedStream::<K, ()>::record_repartition(
449 &mut g,
450 store_name,
451 parent,
452 key_changing_upstream,
453 repartition_lower,
454 );
455 let proc_name = g.new_processor_name(names::COGROUP_AGGREGATE);
456 let agg_id = g.graph.add(
457 proc_name.clone(),
458 GraphNodeKind::Aggregate {
459 store_name: store_name.to_string(),
460 changelog: false,
464 },
465 vec![agg_parent],
466 );
467 let thunk = make_agg(spec.clone());
468 let store_for = store_name.to_string();
469 let pn = proc_name.clone();
470 g.graph.nodes[agg_id].lower = Some(Box::new(move |state: &mut LowerState| {
471 let parent_name = state.handle_name[&agg_parent].clone();
472 let handle = thunk(state, parent_name, pn, store_for);
473 state.handle_name.insert(agg_id, handle);
474 }));
475 agg_ids.push(agg_id);
476 }
477
478 let merge_name = g.new_processor_name(names::COGROUP_MERGE);
479 let merge_id = g.graph.add(
480 merge_name.clone(),
481 GraphNodeKind::Aggregate {
482 store_name: store_name.to_string(),
483 changelog: logging,
484 },
485 agg_ids.clone(),
486 );
487 g.graph.nodes[merge_id].lower = Some(Box::new(move |state: &mut LowerState| {
488 let parents: Vec<NodeHandle<KOut, Change<VOut>>> = agg_ids
489 .iter()
490 .map(|id| NodeHandle::<KOut, Change<VOut>>::from_name(state.handle_name[id].clone()))
491 .collect();
492 let h = state
493 .topology
494 .add_processor::<KOut, Change<VOut>, KOut, Change<VOut>, _, _, _>(
495 merge_name.clone(),
496 || KStreamPassThrough::<KOut, Change<VOut>> { _pd: PhantomData },
497 parents,
498 );
499 let proc_names: Vec<String> = agg_ids
500 .iter()
501 .map(|id| state.handle_name[id].clone())
502 .collect();
503 registrar(state, proc_names);
504 if copartition_sources.len() >= 2 {
507 state
508 .topology
509 .add_copartition_group(copartition_sources.clone());
510 }
511 state.handle_name.insert(merge_id, h.name().to_string());
512 }));
513 drop(g);
515 merge_id
516}
517
518#[cfg(test)]
519mod tests {
520 use assert2::check;
521 use krabka_units::prelude::*;
522
523 use crate::dsl::StreamsBuilder;
524
525 #[test]
530 fn cogroup_store_is_cached_with_positive_budget() {
531 let b = StreamsBuilder::new();
532 let g1 = b.stream::<String, String>(["in1"]).group_by_key();
533 let g2 = b.stream::<String, String>(["in2"]).group_by_key();
534 g1.cogroup::<i64, _>(|_k, v: &String, acc| {
535 acc + i64::try_from(v.len()).unwrap_or(i64::MAX)
536 })
537 .cogroup(g2, |_k, _v: &String, acc| acc + 1)
538 .aggregate(|| 0i64, "co");
539 let built = b.build("app").unwrap();
540
541 let g = pollster::block_on(built.instantiate(
542 &crate::store::backend::StoreBackend::InMemory,
543 "app",
544 mebibytes(10),
547 ))
548 .unwrap();
549 check!(
550 g.cache_owner.contains_key("co"),
551 "cogroup store must be cached when budget > 0 and caching enabled, \
552 cache_owner = {:?}",
553 g.cache_owner
554 );
555 }
556}
557
558#[cfg(test)]
559mod cogroup_caching_tests {
560 use assert2::check;
561 use krabka_units::prelude::*;
562
563 use crate::{
564 I64Serde, Materialized, Produced, StringSerde, dsl::StreamsBuilder,
565 store::backend::StoreBackend,
566 };
567
568 #[test]
576 fn cogroup_caches_marks_and_dedups_cross_input() {
577 let b = StreamsBuilder::new();
578 let g1 = b.stream::<String, String>(["in1"]).group_by_key();
579 let g2 = b.stream::<String, String>(["in2"]).group_by_key();
580 g1.cogroup::<i64, _>(|_k, v: &String, acc| {
581 acc + i64::try_from(v.len()).unwrap_or(i64::MAX)
582 })
583 .cogroup(g2, |_k, _v: &String, acc| acc + 1)
584 .aggregate_explicit(
585 || 0i64,
586 Materialized::with(StringSerde, I64Serde).as_store("cg"),
587 )
588 .to_stream()
589 .to_explicit("out", Produced::with(StringSerde, I64Serde));
590 let built = b.build("app").unwrap();
591 let mut g =
592 pollster::block_on(built.instantiate(&StoreBackend::InMemory, "app", kibibytes(1)))
593 .unwrap();
594 check!(g.cache_owner.contains_key("cg"));
595 pollster::block_on(g.init_processors()).unwrap();
596
597 pollster::block_on(g.pipe("in1", 0, 0, Some(b"a"), b"xx", 0)).unwrap();
599 pollster::block_on(g.pipe("in2", 0, 0, Some(b"a"), b"z", 1)).unwrap();
600 check!(g.take_output().is_empty());
602
603 pollster::block_on(g.flush_caches()).unwrap();
604 let out = g.take_output();
605 check!(out.len() == 1);
606 check!(out[0].topic == "out");
607 check!(out[0].value.as_ref().unwrap().as_ref() == 3i64.to_be_bytes());
609 }
610
611 #[test]
614 fn cogroup_uncached_when_caching_off() {
615 let b = StreamsBuilder::new();
616 let g1 = b.stream::<String, String>(["in1"]).group_by_key();
617 let g2 = b.stream::<String, String>(["in2"]).group_by_key();
618 g1.cogroup::<i64, _>(|_k, v: &String, acc| {
619 acc + i64::try_from(v.len()).unwrap_or(i64::MAX)
620 })
621 .cogroup(g2, |_k, _v: &String, acc| acc + 1)
622 .aggregate_explicit(
623 || 0i64,
624 Materialized::with(StringSerde, I64Serde)
625 .as_store("cg")
626 .with_caching(false),
627 )
628 .to_stream()
629 .to_explicit("out", Produced::with(StringSerde, I64Serde));
630 let built = b.build("app").unwrap();
631 let g = pollster::block_on(built.instantiate(&StoreBackend::InMemory, "app", kibibytes(1)))
632 .unwrap();
633 check!(!g.cache_owner.contains_key("cg"));
634 }
635}