1use std::{
4 any::Any,
5 borrow::Borrow,
6 collections::{BTreeMap, HashMap, HashSet},
7 marker::PhantomData,
8};
9
10pub(crate) type StoreFactory = Box<
15 dyn Fn(
16 &str,
17 String,
18 Box<dyn crate::store::byte::ByteKeyValueStore>,
19 ) -> Box<dyn crate::store::api::StateStore>
20 + Send
21 + Sync,
22>;
23
24const CHANGELOG_ADDITIONAL_RETENTION: Time = days(1);
30
31use krabka_protocol::owned::streams_group_heartbeat_request::Topology as WireTopology;
32use krabka_units::prelude::*;
33
34use super::{
35 grouping::group_nodes,
36 node::{NodeKind, NodeRegistry},
37 wire::{changelog_topic_name, to_wire},
38};
39use crate::processor::{
40 api::ProcessorSupplier,
41 erased::ProcessorError,
42 factory::{MakeDeser, NodeFactory},
43 graph::{Graph, GraphSource},
44 node::{ErasedNode, ProcessorNode, SinkNode, SourceNode},
45 serde::{Consumed, DefaultSerde, Produced, Serde},
46};
47
48#[derive(Debug, thiserror::Error)]
59pub enum TopologyError {
60 #[error("duplicate node name: {0}")]
61 DuplicateNode(String),
62 #[error("node {node} references unknown predecessor {predecessor}")]
63 UnknownPredecessor { node: String, predecessor: String },
64 #[error("topology has no source nodes")]
65 Empty,
66}
67
68#[derive(Debug, Clone)]
70enum StoredError {
71 DuplicateNode(String),
72 UnknownPredecessor { node: String, predecessor: String },
73 Empty,
74}
75
76impl From<StoredError> for TopologyError {
77 fn from(e: StoredError) -> Self {
78 match e {
79 StoredError::DuplicateNode(n) => TopologyError::DuplicateNode(n),
80 StoredError::UnknownPredecessor { node, predecessor } => {
81 TopologyError::UnknownPredecessor { node, predecessor }
82 }
83 StoredError::Empty => TopologyError::Empty,
84 }
85 }
86}
87
88impl From<TopologyError> for StoredError {
89 fn from(e: TopologyError) -> Self {
90 match e {
91 TopologyError::DuplicateNode(n) => StoredError::DuplicateNode(n),
92 TopologyError::UnknownPredecessor { node, predecessor } => {
93 StoredError::UnknownPredecessor { node, predecessor }
94 }
95 TopologyError::Empty => StoredError::Empty,
96 }
97 }
98}
99
100#[derive(Default)]
109pub struct Topology {
110 reg: NodeRegistry,
111 error: Option<StoredError>,
112 factories: HashMap<String, NodeFactory>,
113 store_factories: HashMap<String, (Option<String>, StoreFactory)>,
116 global_store_factories: HashMap<String, (Option<String>, StoreFactory)>,
124 global_store_topics: HashMap<String, String>,
130 caching_stores: std::collections::HashSet<String>,
136}
137
138impl std::fmt::Debug for Topology {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.debug_struct("Topology")
141 .field("reg", &self.reg)
142 .field("error", &self.error)
143 .field(
144 "factories",
145 &format!("<{} factories>", self.factories.len()),
146 )
147 .field(
148 "store_factories",
149 &format!("<{} store_factories>", self.store_factories.len()),
150 )
151 .field(
152 "global_store_factories",
153 &format!(
154 "<{} global_store_factories>",
155 self.global_store_factories.len()
156 ),
157 )
158 .field("global_store_topics", &self.global_store_topics)
159 .field("caching_stores", &self.caching_stores)
160 .finish()
161 }
162}
163
164pub struct NodeHandle<K, V> {
179 name: String,
180 _pd: PhantomData<fn() -> (K, V)>,
181}
182
183impl<K, V> NodeHandle<K, V> {
184 fn new(name: String) -> Self {
185 Self {
186 name,
187 _pd: PhantomData,
188 }
189 }
190
191 pub(crate) fn from_name(name: String) -> Self {
198 Self::new(name)
199 }
200
201 #[must_use]
206 pub fn name(&self) -> &str {
207 &self.name
208 }
209}
210
211impl<K, V> Clone for NodeHandle<K, V> {
212 fn clone(&self) -> Self {
213 Self::new(self.name.clone())
214 }
215}
216
217impl<K, V> std::fmt::Debug for NodeHandle<K, V> {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 f.debug_struct("NodeHandle")
220 .field("name", &self.name)
221 .finish()
222 }
223}
224
225impl Topology {
226 #[must_use]
227 pub fn new() -> Self {
228 Self::default()
229 }
230
231 pub fn add_source<K, V>(
238 &mut self,
239 name: impl Into<String>,
240 topics: impl IntoIterator<Item = impl Into<String>>,
241 ) -> NodeHandle<K, V>
242 where
243 K: Any + Send + Sync + Clone + DefaultSerde,
244 V: Any + Send + Sync + Clone + DefaultSerde,
245 {
246 self.add_source_explicit::<K, V, K::Serde, V::Serde>(
247 name,
248 topics,
249 Consumed::with(K::Serde::default(), V::Serde::default()),
250 )
251 }
252
253 pub fn add_source_explicit<K, V, KS, VS>(
267 &mut self,
268 name: impl Into<String>,
269 topics: impl IntoIterator<Item = impl Into<String>>,
270 consumed: impl Into<Consumed<KS, VS>>,
271 ) -> NodeHandle<K, V>
272 where
273 K: Any + Send + Clone,
274 V: Any + Send + Clone,
275 KS: Serde<K> + Clone,
276 VS: Serde<V> + Clone,
277 {
278 let Consumed {
279 key_serde,
280 value_serde,
281 } = consumed.into();
282 let name: String = name.into();
283 let topics: Vec<String> = topics.into_iter().map(Into::into).collect();
284 for t in &topics {
287 key_serde.prepare(t, crate::processor::serde::SerdeRole::Key);
288 value_serde.prepare(t, crate::processor::serde::SerdeRole::Value);
289 }
290 let r = self.reg.add_source(&name, topics.clone());
291 self.record(r);
292
293 let make_deser: MakeDeser = {
295 let ks = key_serde;
296 let vs = value_serde;
297 let n = name.clone();
298 Box::new(move || {
299 let node = SourceNode::new(n.clone(), ks.clone(), vs.clone());
300 Box::new(move |k: Option<&[u8]>, v: &[u8], ts: i64| node.deserialize(k, v, ts))
301 as Box<dyn Fn(Option<&[u8]>, &[u8], i64) -> Result<_, ProcessorError> + Send>
302 })
303 };
304
305 self.factories.insert(
306 name.clone(),
307 NodeFactory {
308 make_node: None,
309 make_deser: Some(make_deser),
310 },
311 );
312 NodeHandle::new(name)
313 }
314
315 pub fn add_processor<KIn, VIn, KOut, VOut, S, P, I>(
325 &mut self,
326 name: impl Into<String>,
327 supplier: S,
328 parents: I,
329 ) -> NodeHandle<KOut, VOut>
330 where
331 KIn: Any + Send,
332 VIn: Any + Send,
333 KOut: Any + Send + Clone,
334 VOut: Any + Send + Clone,
335 S: ProcessorSupplier<KIn, VIn, KOut, VOut> + Clone,
336 I: IntoIterator<Item = P>,
337 P: Borrow<NodeHandle<KIn, VIn>>,
338 {
339 let name: String = name.into();
340 let preds: Vec<String> = parents
341 .into_iter()
342 .map(|p| p.borrow().name.clone())
343 .collect();
344 let r = self.reg.add_processor(&name, preds);
345 self.record(r);
346
347 let make_node: crate::processor::factory::MakeNode = {
348 let n = name.clone();
349 let s = supplier;
350 Box::new(move || {
351 Box::new(ProcessorNode::<KIn, VIn, KOut, VOut>::new(n.clone(), &s))
352 as Box<dyn ErasedNode>
353 })
354 };
355
356 self.factories.insert(
357 name.clone(),
358 NodeFactory {
359 make_node: Some(make_node),
360 make_deser: None,
361 },
362 );
363 NodeHandle::new(name)
364 }
365
366 pub fn add_sink<K, V>(
373 &mut self,
374 name: impl Into<String>,
375 topic: impl Into<String>,
376 parents: impl IntoIterator<Item = impl Borrow<NodeHandle<K, V>>>,
377 ) where
378 K: Any + Send + Sync + DefaultSerde,
379 V: Any + Send + Sync + DefaultSerde,
380 {
381 self.add_sink_explicit::<K, V, K::Serde, V::Serde, _, _>(
382 name,
383 topic,
384 parents,
385 Produced::with(K::Serde::default(), V::Serde::default()),
386 );
387 }
388
389 pub fn add_sink_explicit<K, V, KS, VS, P, I>(
404 &mut self,
405 name: impl Into<String>,
406 topic: impl Into<String>,
407 parents: I,
408 produced: impl Into<Produced<KS, VS>>,
409 ) where
410 K: Any + Send,
411 V: Any + Send,
412 KS: Serde<K> + Clone,
413 VS: Serde<V> + Clone,
414 I: IntoIterator<Item = P>,
415 P: Borrow<NodeHandle<K, V>>,
416 {
417 let Produced {
418 key_serde,
419 value_serde,
420 } = produced.into();
421 let name: String = name.into();
422 let topic: String = topic.into();
423 key_serde.prepare(&topic, crate::processor::serde::SerdeRole::Key);
425 value_serde.prepare(&topic, crate::processor::serde::SerdeRole::Value);
426 let preds: Vec<String> = parents
427 .into_iter()
428 .map(|p| p.borrow().name.clone())
429 .collect();
430 let r = self.reg.add_sink(&name, topic.clone(), preds);
431 self.record(r);
432
433 let make_node: crate::processor::factory::MakeNode = {
434 let n = name.clone();
435 let t = topic.clone();
436 let ks = key_serde;
437 let vs = value_serde;
438 Box::new(move || {
439 Box::new(SinkNode::<K, V, KS, VS>::new(
440 n.clone(),
441 t.clone(),
442 ks.clone(),
443 vs.clone(),
444 )) as Box<dyn ErasedNode>
445 })
446 };
447
448 self.factories.insert(
449 name,
450 NodeFactory {
451 make_node: Some(make_node),
452 make_deser: None,
453 },
454 );
455 }
456
457 pub fn add_state_store<K, V, KS, VS>(
466 &mut self,
467 name: impl Into<String>,
468 key_serde: KS,
469 value_serde: VS,
470 processors: impl IntoIterator<Item = impl Into<String>>,
471 ) -> &mut Self
472 where
473 K: Send + 'static,
474 V: Send + 'static,
475 KS: Serde<K> + Clone,
476 VS: Serde<V> + Clone,
477 {
478 self.add_state_store_inner::<K, V, KS, VS>(name, key_serde, value_serde, processors, None)
479 }
480
481 pub fn add_state_store_with_changelog<K, V, KS, VS>(
491 &mut self,
492 name: impl Into<String>,
493 key_serde: KS,
494 value_serde: VS,
495 processors: impl IntoIterator<Item = impl Into<String>>,
496 changelog_topic: impl Into<String>,
497 ) -> &mut Self
498 where
499 K: Send + 'static,
500 V: Send + 'static,
501 KS: Serde<K> + Clone,
502 VS: Serde<V> + Clone,
503 {
504 self.add_state_store_inner::<K, V, KS, VS>(
505 name,
506 key_serde,
507 value_serde,
508 processors,
509 Some(changelog_topic.into()),
510 )
511 }
512
513 fn add_state_store_inner<K, V, KS, VS>(
514 &mut self,
515 name: impl Into<String>,
516 key_serde: KS,
517 value_serde: VS,
518 processors: impl IntoIterator<Item = impl Into<String>>,
519 changelog_override: Option<String>,
520 ) -> &mut Self
521 where
522 K: Send + 'static,
523 V: Send + 'static,
524 KS: Serde<K> + Clone,
525 VS: Serde<V> + Clone,
526 {
527 let name: String = name.into();
528 let procs: Vec<String> = processors.into_iter().map(Into::into).collect();
529 self.reg.add_store(&name, procs, changelog_override.clone());
530 self.store_factories.insert(
531 name,
532 (
533 changelog_override,
534 Box::new(
535 move |store_name: &str,
536 changelog: String,
537 backend: Box<dyn crate::store::byte::ByteKeyValueStore>| {
538 Box::new(crate::store::kv::KeyValueBytesStore::<K, V>::new(
539 store_name.to_string(),
540 backend,
541 Box::new(key_serde.clone()),
542 Box::new(value_serde.clone()),
543 changelog,
544 )) as Box<dyn crate::store::api::StateStore>
545 },
546 ),
547 ),
548 );
549 self
550 }
551
552 pub fn add_window_store<K, V, KS, VS>(
572 &mut self,
573 name: impl Into<String>,
574 key_serde: KS,
575 value_serde: VS,
576 window: (Time, Time, Time),
577 processors: impl IntoIterator<Item = impl Into<String>>,
578 ) -> &mut Self
579 where
580 K: Send + 'static,
581 V: Send + 'static,
582 KS: Serde<K> + Clone,
583 VS: Serde<V> + Clone,
584 {
585 let (retention_basis, window_size, grace) = window;
586 let name: String = name.into();
587 let retention = retention_basis + grace + CHANGELOG_ADDITIONAL_RETENTION;
588 let procs: Vec<String> = processors.into_iter().map(Into::into).collect();
589 self.reg.add_window_store(&name, procs, None, retention);
590 self.store_factories.insert(
591 name.clone(),
592 (
593 None,
594 Box::new(
595 move |store_name: &str,
596 changelog: String,
597 backend: Box<dyn crate::store::byte::ByteKeyValueStore>| {
598 Box::new(crate::store::window::WindowBytesStore::<K, V>::new(
599 store_name.to_string(),
600 backend,
601 Box::new(key_serde.clone()),
602 Box::new(value_serde.clone()),
603 changelog,
604 window_size,
610 )) as Box<dyn crate::store::api::StateStore>
611 },
612 ),
613 ),
614 );
615 self
616 }
617
618 pub fn add_join_window_store<K, V, KS, VS>(
628 &mut self,
629 name: impl Into<String>,
630 key_serde: KS,
631 value_serde: VS,
632 window: (Time, Time, Time),
633 processors: impl IntoIterator<Item = impl Into<String>>,
634 ) -> &mut Self
635 where
636 K: Send + 'static,
637 V: Send + 'static,
638 KS: Serde<K> + Clone,
639 VS: Serde<V> + Clone,
640 {
641 let (before, after, grace) = window;
642 let name: String = name.into();
643 let retention = before + after + grace + CHANGELOG_ADDITIONAL_RETENTION;
644 let procs: Vec<String> = processors.into_iter().map(Into::into).collect();
645 self.reg
646 .add_join_window_store(&name, procs, None, retention);
647 self.store_factories.insert(
648 name.clone(),
649 (
650 None,
651 Box::new(
652 move |store_name: &str,
653 changelog: String,
654 backend: Box<dyn crate::store::byte::ByteKeyValueStore>| {
655 Box::new(crate::store::join_window::JoinWindowBytesStore::<K, V>::new(
656 store_name.to_string(),
657 backend,
658 Box::new(key_serde.clone()),
659 Box::new(value_serde.clone()),
660 changelog,
661 )) as Box<dyn crate::store::api::StateStore>
662 },
663 ),
664 ),
665 );
666 self
667 }
668
669 pub fn add_session_store<K, V, KS, VS>(
678 &mut self,
679 name: impl Into<String>,
680 key_serde: KS,
681 value_serde: VS,
682 gap: Time,
683 grace: Time,
684 processors: impl IntoIterator<Item = impl Into<String>>,
685 ) -> &mut Self
686 where
687 K: Send + 'static,
688 V: Send + 'static,
689 KS: Serde<K> + Clone,
690 VS: Serde<V> + Clone,
691 {
692 let name: String = name.into();
693 let retention = gap + grace + CHANGELOG_ADDITIONAL_RETENTION;
694 let procs: Vec<String> = processors.into_iter().map(Into::into).collect();
695 self.reg.add_window_store(&name, procs, None, retention);
698 self.store_factories.insert(
699 name.clone(),
700 (
701 None,
702 Box::new(
703 move |store_name: &str,
704 changelog: String,
705 backend: Box<dyn crate::store::byte::ByteKeyValueStore>| {
706 Box::new(crate::store::session::SessionBytesStore::<K, V>::new(
707 store_name.to_string(),
708 backend,
709 Box::new(key_serde.clone()),
710 Box::new(value_serde.clone()),
711 changelog,
712 )) as Box<dyn crate::store::api::StateStore>
713 },
714 ),
715 ),
716 );
717 self
718 }
719
720 pub fn add_versioned_store<K, V, KS, VS>(
728 &mut self,
729 name: impl Into<String>,
730 key_serde: KS,
731 value_serde: VS,
732 history_retention: Time,
733 processors: impl IntoIterator<Item = impl Into<String>>,
734 ) -> &mut Self
735 where
736 K: Send + Sync + 'static,
737 V: Send + 'static,
738 KS: Serde<K> + Clone,
739 VS: Serde<V> + Clone,
740 {
741 self.add_versioned_store_inner::<K, V, KS, VS>(
742 name,
743 key_serde,
744 value_serde,
745 history_retention,
746 processors,
747 None,
748 )
749 }
750
751 pub fn add_versioned_store_with_changelog<K, V, KS, VS>(
759 &mut self,
760 name: impl Into<String>,
761 key_serde: KS,
762 value_serde: VS,
763 history_retention: Time,
764 processors: impl IntoIterator<Item = impl Into<String>>,
765 changelog_topic: impl Into<String>,
766 ) -> &mut Self
767 where
768 K: Send + Sync + 'static,
769 V: Send + 'static,
770 KS: Serde<K> + Clone,
771 VS: Serde<V> + Clone,
772 {
773 self.add_versioned_store_inner::<K, V, KS, VS>(
774 name,
775 key_serde,
776 value_serde,
777 history_retention,
778 processors,
779 Some(changelog_topic.into()),
780 )
781 }
782
783 fn add_versioned_store_inner<K, V, KS, VS>(
784 &mut self,
785 name: impl Into<String>,
786 key_serde: KS,
787 value_serde: VS,
788 history_retention: Time,
789 processors: impl IntoIterator<Item = impl Into<String>>,
790 changelog_override: Option<String>,
791 ) -> &mut Self
792 where
793 K: Send + Sync + 'static,
794 V: Send + 'static,
795 KS: Serde<K> + Clone,
796 VS: Serde<V> + Clone,
797 {
798 let name: String = name.into();
799 let min_compaction_lag = history_retention + CHANGELOG_ADDITIONAL_RETENTION;
800 let procs: Vec<String> = processors.into_iter().map(Into::into).collect();
801 self.reg
802 .add_versioned_store(&name, procs, changelog_override.clone(), min_compaction_lag);
803 self.store_factories.insert(
804 name.clone(),
805 (
806 changelog_override,
807 Box::new(
808 move |store_name: &str,
809 changelog: String,
810 _backend: Box<dyn crate::store::byte::ByteKeyValueStore>| {
811 Box::new(
812 crate::store::versioned::VersionedBytesStore::<K, V>::new(
813 store_name.to_string(),
814 history_retention,
815 Box::new(key_serde.clone()),
816 Box::new(value_serde.clone()),
817 changelog,
818 ),
819 ) as Box<dyn crate::store::api::StateStore>
820 },
821 ),
822 ),
823 );
824 self
825 }
826
827 pub(crate) fn add_fk_subscription_store(
839 &mut self,
840 name: impl Into<String>,
841 processors: impl IntoIterator<Item = impl Into<String>>,
842 ) -> &mut Self {
843 let name: String = name.into();
844 let procs: Vec<String> = processors.into_iter().map(Into::into).collect();
845 self.reg.add_store(&name, procs, None); self.store_factories.insert(
847 name,
848 (
849 None,
850 Box::new(
851 move |store_name: &str,
852 changelog: String,
853 backend: Box<dyn crate::store::byte::ByteKeyValueStore>| {
854 Box::new(crate::store::fk_subscription::SubscriptionBytesStore::new(
855 store_name.to_string(),
856 backend,
857 changelog,
858 )) as Box<dyn crate::store::api::StateStore>
859 },
860 ),
861 ),
862 );
863 self
864 }
865
866 pub fn add_suppress_store<K, V, KS, VS>(
881 &mut self,
882 name: impl Into<String>,
883 key_serde: KS,
884 value_serde: VS,
885 logging: bool,
886 processors: impl IntoIterator<Item = impl Into<String>>,
887 ) -> &mut Self
888 where
889 K: Send + 'static,
890 V: Send + 'static,
891 KS: Serde<K> + Clone,
892 VS: Serde<V> + Clone,
893 {
894 let name: String = name.into();
895 let procs: Vec<String> = processors.into_iter().map(Into::into).collect();
896 if logging {
900 self.reg.add_store(&name, procs, None);
901 }
902 self.store_factories.insert(
903 name.clone(),
904 (
905 None,
906 Box::new(
907 move |store_name: &str,
908 changelog: String,
909 _backend: Box<dyn crate::store::byte::ByteKeyValueStore>| {
910 let cl = if logging { changelog } else { String::new() };
912 let mut store =
913 crate::store::suppress_store::SuppressBytesStore::<K, V>::new(
914 store_name.to_string(),
915 Box::new(key_serde.clone()),
916 Box::new(value_serde.clone()),
917 cl,
918 );
919 if !logging {
920 crate::store::api::StateStore::set_logging(&mut store, false);
921 }
922 Box::new(store) as Box<dyn crate::store::api::StateStore>
923 },
924 ),
925 ),
926 );
927 self
928 }
929
930 pub(crate) fn add_join_grace_store<K, V, KS, VS>(
946 &mut self,
947 name: impl Into<String>,
948 key_serde: KS,
949 value_serde: VS,
950 logging: bool,
951 processors: impl IntoIterator<Item = impl Into<String>>,
952 ) -> &mut Self
953 where
954 K: Send + 'static,
955 V: Send + 'static,
956 KS: Serde<K> + Clone,
957 VS: Serde<V> + Clone,
958 {
959 let name: String = name.into();
960 let procs: Vec<String> = processors.into_iter().map(Into::into).collect();
961 if logging {
965 self.reg.add_store(&name, procs, None);
966 }
967 self.store_factories.insert(
968 name.clone(),
969 (
970 None,
971 Box::new(
972 move |store_name: &str,
973 changelog: String,
974 _backend: Box<dyn crate::store::byte::ByteKeyValueStore>| {
975 let cl = if logging { changelog } else { String::new() };
977 let mut store =
978 crate::store::join_grace_buffer::JoinGraceBufferStore::<K, V>::new(
979 store_name.to_string(),
980 Box::new(key_serde.clone()),
981 Box::new(value_serde.clone()),
982 cl,
983 );
984 if !logging {
985 crate::store::api::StateStore::set_logging(&mut store, false);
986 }
987 Box::new(store) as Box<dyn crate::store::api::StateStore>
988 },
989 ),
990 ),
991 );
992 self
993 }
994
995 pub(crate) fn add_state_store_no_changelog<K, V, KS, VS>(
1001 &mut self,
1002 name: impl Into<String>,
1003 key_serde: KS,
1004 value_serde: VS,
1005 ) -> &mut Self
1006 where
1007 K: Send + 'static,
1008 V: Send + 'static,
1009 KS: Serde<K> + Clone,
1010 VS: Serde<V> + Clone,
1011 {
1012 let name: String = name.into();
1013 self.store_factories.insert(
1016 name,
1017 (
1018 None, Box::new(
1020 move |store_name: &str,
1021 _changelog: String,
1022 backend: Box<dyn crate::store::byte::ByteKeyValueStore>| {
1023 Box::new(crate::store::kv::KeyValueBytesStore::<K, V>::new(
1025 store_name.to_string(),
1026 backend,
1027 Box::new(key_serde.clone()),
1028 Box::new(value_serde.clone()),
1029 String::new(),
1030 )) as Box<dyn crate::store::api::StateStore>
1031 },
1032 ),
1033 ),
1034 );
1035 self
1036 }
1037
1038 pub fn add_global_store<K, V, KS, VS>(
1060 &mut self,
1061 store_name: impl Into<String>,
1062 source_name: impl Into<String>,
1063 topic: impl Into<String>,
1064 processor_name: impl Into<String>,
1065 consumed: impl Into<Consumed<KS, VS>>,
1066 ) -> &mut Self
1067 where
1068 K: Send + 'static,
1069 V: Send + 'static,
1070 KS: Serde<K> + Clone,
1071 VS: Serde<V> + Clone,
1072 {
1073 let store_name: String = store_name.into();
1074 let source_name: String = source_name.into();
1075 let topic: String = topic.into();
1076 let processor_name: String = processor_name.into();
1077 let Consumed {
1078 key_serde,
1079 value_serde,
1080 } = consumed.into();
1081
1082 let r = self.reg.add_source(&source_name, vec![topic.clone()]);
1084 self.record(r);
1085 let r = self
1087 .reg
1088 .add_processor(&processor_name, vec![source_name.clone()]);
1089 self.record(r);
1090 self.reg.add_global_source(&topic);
1092
1093 let factory: StoreFactory = Box::new(
1098 move |sn: &str,
1099 _changelog: String,
1100 backend: Box<dyn crate::store::byte::ByteKeyValueStore>| {
1101 Box::new(crate::store::kv::KeyValueBytesStore::<K, V>::new(
1102 sn.to_string(),
1103 backend,
1104 Box::new(key_serde.clone()),
1105 Box::new(value_serde.clone()),
1106 String::new(),
1107 )) as Box<dyn crate::store::api::StateStore>
1108 },
1109 );
1110 self.global_store_topics.insert(store_name.clone(), topic);
1111 self.global_store_factories
1112 .insert(store_name, (None, factory));
1113 self
1114 }
1115
1116 pub fn connect_processor_store(&mut self, processor: &str, store: &str) -> &mut Self {
1124 self.reg.connect_processor_store(processor, store);
1125 self
1126 }
1127
1128 pub(crate) fn mark_store_caching(&mut self, name: &str, on: bool) {
1135 if on {
1136 self.caching_stores.insert(name.to_string());
1137 }
1138 }
1139
1140 #[cfg(test)]
1142 pub(crate) fn store_entry_for_test(&self, store: &str) -> Option<Vec<String>> {
1143 self.reg
1144 .stores
1145 .iter()
1146 .find(|e| e.name == store)
1147 .map(|e| e.processors.clone())
1148 }
1149
1150 #[cfg(test)]
1152 pub(crate) fn has_global_store_for_test(&self, store: &str) -> bool {
1153 self.global_store_factories.contains_key(store)
1154 }
1155
1156 pub fn add_repartition_topic<S: Into<String>>(&mut self, name: S) -> &mut Self {
1158 self.reg.repartition_topics.insert(name.into());
1159 self
1160 }
1161
1162 pub fn add_copartition_group(
1170 &mut self,
1171 topics: impl IntoIterator<Item = impl Into<String>>,
1172 ) -> &mut Self {
1173 self.reg
1174 .add_copartition_group(topics.into_iter().map(Into::into).collect());
1175 self
1176 }
1177
1178 pub fn build<S: Into<String>>(
1190 mut self,
1191 application_id: S,
1192 ) -> Result<BuiltTopology, TopologyError> {
1193 if let Some(e) = self.error.take() {
1194 return Err(e.into());
1195 }
1196 self.reg.validate_predecessors()?;
1197 let groups = group_nodes(&self.reg);
1198 if groups.is_empty() {
1199 return Err(TopologyError::Empty);
1200 }
1201 let app = application_id.into();
1202 let wire = to_wire(&groups, &app);
1203
1204 let mut source_topics: BTreeMap<String, Vec<String>> = BTreeMap::new();
1206 for g in &groups {
1207 let mut all = g.source_topics.clone();
1208 all.extend(g.repartition_source_topics.iter().cloned());
1209 source_topics.insert(g.id.clone(), all);
1210 }
1211 let mut changelog_topics: Vec<(String, String)> = groups
1212 .iter()
1213 .flat_map(|g| g.changelog_stores.iter())
1214 .map(|(store, changelog_override, _kind)| {
1215 (
1216 store.clone(),
1217 changelog_topic_name(&app, store, changelog_override.as_deref()),
1218 )
1219 })
1220 .collect();
1221 changelog_topics.sort();
1222
1223 let mut global_nodes: std::collections::HashSet<&str> = std::collections::HashSet::new();
1233 for n in &self.reg.nodes {
1234 let is_global = match &n.kind {
1235 NodeKind::Source { topics } => topics
1236 .iter()
1237 .any(|t| self.reg.global_source_topics.contains(t)),
1238 NodeKind::Processor { predecessors } | NodeKind::Sink { predecessors, .. } => {
1239 predecessors
1240 .iter()
1241 .any(|p| global_nodes.contains(p.as_str()))
1242 }
1243 };
1244 if is_global {
1245 global_nodes.insert(n.name.as_str());
1246 }
1247 }
1248
1249 let subtopology_nodes: BTreeMap<String, HashSet<String>> = groups
1252 .iter()
1253 .map(|g| {
1254 let nodes = g
1255 .nodes
1256 .iter()
1257 .filter(|n| !global_nodes.contains(n.as_str()))
1258 .cloned()
1259 .collect();
1260 (g.id.clone(), nodes)
1261 })
1262 .collect();
1263
1264 let node_specs: Vec<NodeSpec> = self
1266 .reg
1267 .nodes
1268 .iter()
1269 .filter(|n| !global_nodes.contains(n.name.as_str()))
1270 .map(|n| {
1271 let (predecessors, kind_str, st, sink_t) = match &n.kind {
1272 NodeKind::Source { topics } => (Vec::new(), "source", topics.clone(), None),
1273 NodeKind::Processor { predecessors } => {
1274 (predecessors.clone(), "processor", Vec::new(), None)
1275 }
1276 NodeKind::Sink {
1277 predecessors,
1278 topic,
1279 } => (
1280 predecessors.clone(),
1281 "sink",
1282 Vec::new(),
1283 Some(topic.clone()),
1284 ),
1285 };
1286 NodeSpec {
1287 name: n.name.clone(),
1288 kind: kind_str,
1289 predecessors,
1290 source_topics: st,
1291 sink_topic: sink_t,
1292 }
1293 })
1294 .collect();
1295
1296 let store_processors: HashMap<String, Vec<String>> = self
1299 .reg
1300 .stores
1301 .iter()
1302 .map(|e| (e.name.clone(), e.processors.clone()))
1303 .collect();
1304
1305 Ok(BuiltTopology {
1306 wire,
1307 source_topics,
1308 changelog_topics,
1309 subtopology_nodes,
1310 application_id: app,
1311 factories: self.factories,
1312 node_specs,
1313 store_factories: self.store_factories,
1314 global_store_factories: self.global_store_factories,
1315 global_store_topics: self.global_store_topics,
1316 caching_stores: self.caching_stores,
1317 store_processors,
1318 })
1319 }
1320
1321 fn record(&mut self, r: Result<(), TopologyError>) {
1322 if self.error.is_none()
1323 && let Err(e) = r
1324 {
1325 self.error = Some(e.into());
1326 }
1327 }
1328}
1329
1330pub(crate) struct NodeSpec {
1338 pub name: String,
1339 pub kind: &'static str,
1341 pub predecessors: Vec<String>,
1343 pub source_topics: Vec<String>,
1345 pub sink_topic: Option<String>,
1347}
1348
1349pub struct BuiltTopology {
1360 wire: WireTopology,
1361 source_topics: BTreeMap<String, Vec<String>>,
1362 changelog_topics: Vec<(String, String)>,
1366 subtopology_nodes: BTreeMap<String, HashSet<String>>,
1370 application_id: String,
1371 factories: HashMap<String, NodeFactory>,
1372 node_specs: Vec<NodeSpec>,
1373 store_factories: HashMap<String, (Option<String>, StoreFactory)>,
1376 global_store_factories: HashMap<String, (Option<String>, StoreFactory)>,
1383 global_store_topics: HashMap<String, String>,
1387 caching_stores: std::collections::HashSet<String>,
1392 store_processors: HashMap<String, Vec<String>>,
1396}
1397
1398impl std::fmt::Debug for BuiltTopology {
1399 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1400 f.debug_struct("BuiltTopology")
1401 .field("application_id", &self.application_id)
1402 .field("source_topics", &self.source_topics)
1403 .finish_non_exhaustive()
1404 }
1405}
1406
1407impl BuiltTopology {
1408 #[must_use]
1411 pub fn to_wire(&self) -> super::wire::WireTopology {
1412 super::wire::WireTopology::from(&self.wire)
1413 }
1414
1415 #[must_use]
1420 pub fn to_wire_request(&self) -> WireTopology {
1421 self.wire.clone()
1422 }
1423
1424 #[must_use]
1429 pub fn subtopology_ids(&self) -> Vec<String> {
1430 self.wire
1431 .subtopologies
1432 .iter()
1433 .map(|s| s.subtopology_id.clone())
1434 .collect()
1435 }
1436
1437 #[must_use]
1443 pub fn repartition_topics(&self) -> Vec<String> {
1444 let mut topics: Vec<String> = self
1445 .wire
1446 .subtopologies
1447 .iter()
1448 .flat_map(|s| {
1449 s.repartition_sink_topics.iter().cloned().chain(
1450 s.repartition_source_topics
1451 .iter()
1452 .map(|info| info.name.clone()),
1453 )
1454 })
1455 .collect();
1456 topics.sort();
1457 topics.dedup();
1458 topics
1459 }
1460
1461 #[must_use]
1468 pub fn changelog_topics(&self) -> &[(String, String)] {
1469 &self.changelog_topics
1470 }
1471
1472 pub(crate) fn global_store_factories(
1481 &self,
1482 ) -> &HashMap<String, (Option<String>, StoreFactory)> {
1483 &self.global_store_factories
1484 }
1485
1486 #[must_use]
1495 pub fn global_store_topics(&self) -> HashMap<String, String> {
1496 self.global_store_topics.clone()
1497 }
1498
1499 #[must_use]
1502 pub fn source_topics_for(&self, subtopology_id: &str) -> &[String] {
1503 self.source_topics
1504 .get(subtopology_id)
1505 .map_or(&[], Vec::as_slice)
1506 }
1507
1508 #[must_use]
1510 pub fn application_id(&self) -> &str {
1511 &self.application_id
1512 }
1513
1514 #[must_use]
1517 pub fn list_source_topics(&self) -> Vec<String> {
1518 self.node_specs
1519 .iter()
1520 .filter(|s| s.kind == "source")
1521 .flat_map(|s| s.source_topics.iter().cloned())
1522 .collect()
1523 }
1524
1525 #[must_use]
1527 pub fn list_sink_topics(&self) -> Vec<String> {
1528 self.node_specs
1529 .iter()
1530 .filter_map(|s| s.sink_topic.clone())
1531 .collect()
1532 }
1533
1534 #[tracing::instrument(
1540 name = "streams.topology.instantiate",
1541 level = "info",
1542 skip_all,
1543 fields(app_id = %app_id, nodes = self.node_specs.len(), cache_max_bytes = cache_max_bytes.bytes_i64()),
1544 err,
1545 )]
1546 pub(crate) async fn instantiate(
1547 &self,
1548 backend: &crate::store::backend::StoreBackend,
1549 app_id: &str,
1550 cache_max_bytes: ByteSize,
1551 ) -> Result<Graph, ProcessorError> {
1552 self.instantiate_nodes(backend, app_id, cache_max_bytes, None)
1553 .await
1554 }
1555
1556 #[tracing::instrument(
1565 name = "streams.topology.instantiate_subtopology",
1566 level = "info",
1567 skip_all,
1568 fields(app_id = %app_id, subtopology_id = %subtopology_id, cache_max_bytes = cache_max_bytes.bytes_i64()),
1569 err,
1570 )]
1571 pub(crate) async fn instantiate_subtopology(
1572 &self,
1573 backend: &crate::store::backend::StoreBackend,
1574 app_id: &str,
1575 cache_max_bytes: ByteSize,
1576 subtopology_id: &str,
1577 ) -> Result<Graph, ProcessorError> {
1578 let scope = self.subtopology_nodes.get(subtopology_id).ok_or_else(|| {
1579 ProcessorError::UnknownSubtopology {
1580 id: subtopology_id.to_string(),
1581 }
1582 })?;
1583 self.instantiate_nodes(backend, app_id, cache_max_bytes, Some(scope))
1584 .await
1585 }
1586
1587 async fn instantiate_nodes(
1590 &self,
1591 backend: &crate::store::backend::StoreBackend,
1592 app_id: &str,
1593 cache_max_bytes: ByteSize,
1594 scope: Option<&HashSet<String>>,
1595 ) -> Result<Graph, ProcessorError> {
1596 let keep = |name: &str| scope.is_none_or(|set| set.contains(name));
1597 let non_source: Vec<&NodeSpec> = self
1600 .node_specs
1601 .iter()
1602 .filter(|s| s.kind != "source" && keep(&s.name))
1603 .collect();
1604
1605 let name_to_idx: HashMap<&str, usize> = non_source
1606 .iter()
1607 .enumerate()
1608 .map(|(i, s)| (s.name.as_str(), i))
1609 .collect();
1610
1611 let nodes: Vec<Box<dyn ErasedNode>> = non_source
1612 .iter()
1613 .map(|s| {
1614 let factory = self
1615 .factories
1616 .get(&s.name)
1617 .ok_or_else(|| ProcessorError::Serde {
1618 node: s.name.clone(),
1619 message: "factory missing".into(),
1620 })?;
1621 let make = factory
1622 .make_node
1623 .as_ref()
1624 .ok_or_else(|| ProcessorError::Serde {
1625 node: s.name.clone(),
1626 message: "make_node missing".into(),
1627 })?;
1628 Ok((make)())
1629 })
1630 .collect::<Result<Vec<_>, ProcessorError>>()?;
1631
1632 let mut children: Vec<Vec<usize>> = vec![Vec::new(); nodes.len()];
1634 for (child_idx, s) in non_source.iter().enumerate() {
1635 for parent_name in &s.predecessors {
1636 if let Some(&parent_idx) = name_to_idx.get(parent_name.as_str()) {
1638 children[parent_idx].push(child_idx);
1639 }
1640 }
1641 }
1642
1643 let sources: Vec<GraphSource> = self
1645 .node_specs
1646 .iter()
1647 .filter(|s| s.kind == "source" && keep(&s.name))
1648 .flat_map(|src_spec| {
1649 let src_name = src_spec.name.as_str();
1651 let src_children: Vec<usize> = non_source
1652 .iter()
1653 .enumerate()
1654 .filter(|(_, s)| s.predecessors.iter().any(|p| p == src_name))
1655 .map(|(i, _)| i)
1656 .collect();
1657
1658 let Some(factory) = self.factories.get(src_spec.name.as_str()) else {
1659 return vec![];
1660 };
1661 let Some(make_deser) = &factory.make_deser else {
1662 return vec![];
1663 };
1664
1665 src_spec
1666 .source_topics
1667 .iter()
1668 .map(|topic| {
1669 let deser = (make_deser)();
1670 GraphSource {
1671 topic: topic.clone(),
1672 deserialize: deser,
1673 children: src_children.clone(),
1674 }
1675 })
1676 .collect()
1677 })
1678 .collect();
1679
1680 let mut store_registry = crate::store::registry::StoreRegistry::default();
1682 for (store_name, (changelog_override, factory)) in &self.store_factories {
1683 let connected = scope.is_none_or(|_| {
1684 self.store_processors
1685 .get(store_name)
1686 .is_some_and(|procs| procs.iter().any(|p| keep(p)))
1687 });
1688 if !connected {
1689 continue;
1690 }
1691 let changelog = changelog_override
1692 .clone()
1693 .unwrap_or_else(|| format!("{app_id}-{store_name}-changelog"));
1694 let bytes = backend.open(app_id, store_name).await;
1695 store_registry.insert(factory(store_name, changelog, bytes));
1696 }
1697
1698 let (cache, cache_owner) =
1701 self.wire_record_caches(&mut store_registry, &name_to_idx, cache_max_bytes);
1702
1703 Ok(Graph {
1704 nodes,
1705 children,
1706 sources,
1707 output: Vec::new(),
1708 stores: store_registry,
1709 globals: crate::runtime::global::GlobalStateManager::default(),
1713 schedules: Vec::new(),
1714 stream_time: i64::MIN,
1715 wall_clock: 0,
1716 cache_max_bytes,
1717 cache_owner,
1718 cache,
1719 })
1720 }
1721
1722 fn wire_record_caches(
1739 &self,
1740 store_registry: &mut crate::store::registry::StoreRegistry,
1741 name_to_idx: &HashMap<&str, usize>,
1742 cache_max_bytes: ByteSize,
1743 ) -> (
1744 crate::store::cache::thread::ThreadCache,
1745 HashMap<String, usize>,
1746 ) {
1747 let mut cache = crate::store::cache::thread::ThreadCache::new(cache_max_bytes);
1748 let mut cache_owner: HashMap<String, usize> = HashMap::new();
1749 if cache_max_bytes <= ByteSize::ZERO {
1750 return (cache, cache_owner);
1751 }
1752 for store_name in &self.caching_stores {
1753 let owner_idx = self
1755 .store_processors
1756 .get(store_name)
1757 .and_then(|procs| procs.first())
1758 .and_then(|proc_name| name_to_idx.get(proc_name.as_str()).copied());
1759 let Some(owner_idx) = owner_idx else {
1760 continue;
1763 };
1764 let nc = cache.register(store_name);
1765 if store_registry.enable_cache(store_name, nc) {
1771 cache_owner.insert(store_name.clone(), owner_idx);
1772 }
1773 }
1774 (cache, cache_owner)
1775 }
1776}
1777
1778#[cfg(test)]
1783mod tests {
1784 use assert2::check;
1785 use async_trait::async_trait;
1786
1787 use super::*;
1788 use crate::processor::{
1789 api::{Processor, ProcessorContext},
1790 record::Record,
1791 serde::StringSerde,
1792 };
1793
1794 struct Upper;
1795 #[async_trait]
1796 impl Processor<String, String, String, String> for Upper {
1797 async fn process(
1798 &mut self,
1799 ctx: &mut ProcessorContext<'_, '_, String, String>,
1800 r: Record<String, String>,
1801 ) {
1802 ctx.forward(Record::new(r.key, r.value.to_uppercase(), r.timestamp));
1803 }
1804 }
1805
1806 #[test]
1807 fn copartition_group_emitted_in_wire() {
1808 let mut t = Topology::new();
1809 let a: NodeHandle<bytes::Bytes, bytes::Bytes> = t.add_source("sa", ["left"]);
1810 let b: NodeHandle<bytes::Bytes, bytes::Bytes> = t.add_source("sb", ["right"]);
1811 t.add_sink("snk", "out", [&a, &b]); t.add_copartition_group(["left", "right"]);
1813 let wire = t.build("app").unwrap().to_wire();
1814 let sub = &wire.subtopologies[0];
1815 check!(sub.copartition_groups.len() == 1);
1816 check!(sub.copartition_groups[0].source_topics == vec![0i16, 1i16]); }
1818
1819 #[test]
1820 fn global_store_is_invisible_and_bumps_stream_index() {
1821 let mut t = Topology::new();
1826 t.add_global_store::<String, String, _, _>(
1828 "global-store",
1829 "gsrc",
1830 "global",
1831 "gproc",
1832 Consumed::with(StringSerde, StringSerde),
1833 );
1834 let src: NodeHandle<String, String> = t.add_source("src", ["in"]);
1836 t.add_sink("snk", "out", [&src]);
1837 check!(t.has_global_store_for_test("global-store"));
1838 let built = t.build("app").unwrap();
1839 let wire = built.to_wire();
1840 check!(wire.subtopologies.len() == 1);
1841 check!(wire.subtopologies[0].subtopology_id == "1");
1842 check!(wire.subtopologies[0].source_topics == vec!["in".to_string()]);
1843 check!(
1845 wire.subtopologies
1846 .iter()
1847 .all(|s| s.state_changelog_topics.is_empty())
1848 );
1849 check!(
1851 !wire.subtopologies[0]
1852 .source_topics
1853 .contains(&"global".to_string())
1854 );
1855 }
1856
1857 #[test]
1858 fn add_source_and_sink_can_use_default_serdes() {
1859 let mut t = Topology::new();
1860 let src: NodeHandle<String, String> = t.add_source("src", ["in"]);
1861 t.add_sink("out", "out-topic", [&src]);
1862 let built = t.build("app").unwrap();
1863 let wire = built.to_wire();
1864
1865 check!(wire.subtopologies[0].source_topics == vec!["in".to_string()]);
1866 check!(built.source_topics_for("0") == ["in".to_string()]);
1867 }
1868
1869 #[test]
1870 fn build_single_source_sink_wire_unchanged() {
1871 let mut t = Topology::new();
1872 let src: NodeHandle<String, String> = t.add_source("src", ["in"]);
1873 let up = t.add_processor("up", || Upper, [&src]);
1874 t.add_sink("out", "out-topic", [&up]);
1875 let built = t.build("app").unwrap();
1876 let wire = built.to_wire();
1877 check!(wire.epoch == 0);
1878 check!(wire.subtopologies[0].subtopology_id == "0");
1879 check!(wire.subtopologies[0].source_topics == vec!["in".to_string()]);
1880 check!(built.source_topics_for("0") == ["in".to_string()]);
1881 }
1882
1883 #[test]
1884 fn topology_error_converts_to_stored_error() {
1885 check!(matches!(
1889 StoredError::from(TopologyError::Empty),
1890 StoredError::Empty
1891 ));
1892 check!(matches!(
1893 StoredError::from(TopologyError::DuplicateNode("n".into())),
1894 StoredError::DuplicateNode(_)
1895 ));
1896 check!(matches!(
1897 StoredError::from(TopologyError::UnknownPredecessor {
1898 node: "a".into(),
1899 predecessor: "b".into(),
1900 }),
1901 StoredError::UnknownPredecessor { .. }
1902 ));
1903 }
1904
1905 #[test]
1906 fn node_handle_clone_debug_and_owned_wiring() {
1907 let mut t = Topology::new();
1910 let src: NodeHandle<String, String> = t.add_source("src", ["in"]);
1911 let src2 = src.clone();
1912 check!(src2.name() == "src");
1913 check!(format!("{src2:?}").contains("src"));
1914 t.add_sink("out", "out", [src2]);
1916 check!(t.build("app").is_ok());
1917 }
1918
1919 #[test]
1920 fn handle_from_another_topology_is_rejected_at_build() {
1921 let mut a = Topology::new();
1926 let foreign: NodeHandle<String, String> = a.add_source("src", ["in"]);
1927
1928 let mut b = Topology::new();
1929 b.add_sink("out", "o", [&foreign]);
1930 check!(b.build("app").is_err());
1931 }
1932
1933 #[test]
1934 fn instantiate_runs_records() {
1935 let mut t = Topology::new();
1936 let src: NodeHandle<String, String> = t.add_source("src", ["in"]);
1937 let up = t.add_processor("up", || Upper, [&src]);
1938 t.add_sink("out", "out-topic", [&up]);
1939 let built = t.build("app").unwrap();
1940 let mut g = pollster::block_on(built.instantiate(
1941 &crate::store::backend::StoreBackend::InMemory,
1942 "app",
1943 ByteSize::ZERO,
1944 ))
1945 .unwrap();
1946 pollster::block_on(g.pipe("in", 0, 0, Some(b"k"), b"hi", 0)).unwrap();
1947 let out = g.take_output();
1948 check!(out.len() == 1);
1949 check!(out[0].value.as_ref().unwrap().as_ref() == b"HI");
1950 }
1951
1952 #[test]
1953 fn empty_topology_is_rejected() {
1954 let topo = Topology::new();
1955 check!(topo.build("app").is_err());
1956 }
1957
1958 #[test]
1959 fn build_with_processor_store_and_repartition() {
1960 let mut t = Topology::new();
1961 t.add_repartition_topic("rp");
1962 let src: NodeHandle<String, String> = t.add_source("src", ["in"]);
1963 let proc = t.add_processor("proc", || Upper, [&src]);
1964 t.add_state_store("store", StringSerde, StringSerde, [proc.name()]);
1965 t.add_sink("rsink", "rp", [&proc]);
1966 let rsrc: NodeHandle<String, String> = t.add_source("rsrc", ["rp"]);
1967 t.add_sink("out", "out-topic", [&rsrc]);
1968 let built = t.build("my-app").unwrap();
1969 check!(built.application_id() == "my-app");
1970 let wire = built.to_wire();
1971 check!(wire.subtopologies.len() >= 2);
1973 let has_changelog = wire.subtopologies.iter().any(|s| {
1975 s.state_changelog_topics
1976 .iter()
1977 .any(|c| c.name == "my-app-store-changelog")
1978 });
1979 check!(has_changelog);
1980 }
1981
1982 #[test]
1983 fn application_id_accessor() {
1984 let mut t = Topology::new();
1985 let src: NodeHandle<String, String> = t.add_source("src", ["in"]);
1986 t.add_sink("snk", "out", [&src]);
1987 let built = t.build("my-streams-app").unwrap();
1988 check!(built.application_id() == "my-streams-app");
1989 }
1990
1991 #[test]
1992 fn source_topics_for_unknown_id_returns_empty() {
1993 let mut t = Topology::new();
1994 let src: NodeHandle<String, String> = t.add_source("src", ["in"]);
1995 t.add_sink("snk", "out", [&src]);
1996 let built = t.build("app").unwrap();
1997 check!(built.source_topics_for("99").is_empty());
1998 }
1999
2000 #[test]
2001 fn connect_processor_store_adds_processor_to_store() {
2002 let mut t = Topology::new();
2003 t.add_state_store::<String, String, _, _>("s", StringSerde, StringSerde, ["p1"]);
2005 t.connect_processor_store("p2", "s");
2006 let entry = t.store_entry_for_test("s").unwrap();
2008 check!(entry == vec!["p1".to_string(), "p2".to_string()]);
2009 }
2010
2011 #[test]
2012 fn connect_processor_store_is_idempotent() {
2013 let mut t = Topology::new();
2014 t.add_state_store::<String, String, _, _>("s", StringSerde, StringSerde, ["p1"]);
2015 t.connect_processor_store("p1", "s"); let entry = t.store_entry_for_test("s").unwrap();
2017 check!(entry == vec!["p1".to_string()]);
2018 }
2019
2020 #[test]
2021 fn connect_processor_store_unknown_store_is_noop() {
2022 let mut t = Topology::new();
2023 t.add_state_store::<String, String, _, _>("s", StringSerde, StringSerde, ["p1"]);
2024 t.connect_processor_store("p2", "no_such_store"); let entry = t.store_entry_for_test("s").unwrap();
2026 check!(entry == vec!["p1".to_string()]); }
2028
2029 #[test]
2030 fn duplicate_node_name_propagates_error() {
2031 let mut t = Topology::new();
2032 let _: NodeHandle<String, String> = t.add_source("src", ["in"]);
2033 let _: NodeHandle<String, String> = t.add_source("src", ["other"]); check!(t.build("app").is_err());
2035 }
2036
2037 #[test]
2038 fn instantiate_repartition_topology_lists_topics() {
2039 let mut t = Topology::new();
2040 t.add_repartition_topic("rp");
2041 let s1: NodeHandle<String, String> = t.add_source("s1", ["in"]);
2042 let p = t.add_processor("p", || Upper, [&s1]);
2043 t.add_sink("to_rp", "rp", [&p]);
2044 let s2: NodeHandle<String, String> = t.add_source("s2", ["rp"]);
2045 t.add_sink("out", "out", [&s2]);
2046 let built = t.build("app").unwrap();
2047 let mut srcs = built.list_source_topics();
2048 srcs.sort();
2049 check!(srcs == vec!["in".to_string(), "rp".to_string()]);
2050 let mut sinks = built.list_sink_topics();
2051 sinks.sort();
2052 check!(sinks == vec!["out".to_string(), "rp".to_string()]);
2053 let mut g = pollster::block_on(built.instantiate(
2055 &crate::store::backend::StoreBackend::InMemory,
2056 "app",
2057 ByteSize::ZERO,
2058 ))
2059 .unwrap();
2060 pollster::block_on(g.pipe("in", 0, 0, None, b"hi", 0)).unwrap();
2061 let out1 = g.take_output();
2062 check!(out1.iter().any(|o| o.topic == "rp"));
2063 }
2064
2065 #[test]
2066 fn instantiate_builds_stores_and_processes_statefully() {
2067 use crate::processor::serde::I64Serde;
2068 struct Counter;
2069 #[async_trait]
2070 impl Processor<String, String, String, i64> for Counter {
2071 async fn process(
2072 &mut self,
2073 ctx: &mut ProcessorContext<'_, '_, String, i64>,
2074 r: Record<String, String>,
2075 ) {
2076 let n = {
2077 let s = ctx.get_state_store::<String, i64>("counts").unwrap();
2078 let n = s.get(&r.value).await.unwrap_or(0) + 1;
2079 s.put(r.value.clone(), n).await;
2080 n
2081 };
2082 ctx.forward(Record::new(Some(r.value), n, r.timestamp));
2083 }
2084 }
2085 let mut t = Topology::new();
2086 let src: NodeHandle<String, String> = t.add_source("src", ["in"]);
2087 let c = t.add_processor("c", || Counter, [&src]);
2088 t.add_state_store("counts", StringSerde, I64Serde, [c.name()]);
2089 t.add_sink("out", "out", [&c]);
2090 let built = t.build("app").unwrap();
2091 check!(built.to_wire().subtopologies.iter().any(|s| {
2093 s.state_changelog_topics
2094 .iter()
2095 .any(|c| c.name == "app-counts-changelog")
2096 }));
2097 let mut g = pollster::block_on(built.instantiate(
2098 &crate::store::backend::StoreBackend::InMemory,
2099 "app",
2100 ByteSize::ZERO,
2101 ))
2102 .unwrap();
2103 pollster::block_on(g.pipe("in", 0, 0, None, b"x", 0)).unwrap();
2104 pollster::block_on(g.pipe("in", 0, 1, None, b"x", 1)).unwrap();
2105 check!(
2107 g.take_output()
2108 .last()
2109 .unwrap()
2110 .value
2111 .as_ref()
2112 .unwrap()
2113 .as_ref()
2114 == [0, 0, 0, 0, 0, 0, 0, 2]
2115 );
2116 }
2117
2118 struct CountingMaterializer;
2124 #[async_trait]
2125 impl Processor<String, String, String, crate::dsl::processors::change::Change<i64>>
2126 for CountingMaterializer
2127 {
2128 async fn process(
2129 &mut self,
2130 ctx: &mut ProcessorContext<'_, '_, String, crate::dsl::processors::change::Change<i64>>,
2131 r: Record<String, String>,
2132 ) {
2133 use crate::dsl::processors::change::Change;
2134 let (old, new) = {
2135 let s = ctx.get_state_store::<String, i64>("counts").unwrap();
2136 let old = s.get(&r.value).await;
2137 let new = old.unwrap_or(0) + 1;
2138 s.put(r.value.clone(), new).await;
2139 (old, Some(new))
2140 };
2141 ctx.forward(Record::new(Some(r.value), Change { old, new }, r.timestamp));
2142 }
2143 }
2144
2145 fn counting_topology(caching: bool) -> BuiltTopology {
2148 use crate::{dsl::processors::change::Change, processor::serde::I64Serde};
2149 let mut t = Topology::new();
2150 let src: NodeHandle<String, String> = t.add_source("src", ["in"]);
2151 let c = t.add_processor("c", || CountingMaterializer, [&src]);
2152 t.add_state_store("counts", StringSerde, I64Serde, [c.name()]);
2153 t.add_sink_explicit::<String, Change<i64>, _, _, _, _>(
2156 "out",
2157 "out",
2158 [&c],
2159 crate::processor::serde::Produced::with(StringSerde, ChangeI64Serde),
2160 );
2161 if caching {
2162 t.mark_store_caching("counts", true);
2163 }
2164 t.build("app").unwrap()
2165 }
2166
2167 #[derive(Clone)]
2170 struct ChangeI64Serde;
2171 impl crate::processor::serde::Serde<crate::dsl::processors::change::Change<i64>>
2172 for ChangeI64Serde
2173 {
2174 fn serialize(
2175 &self,
2176 _topic: &str,
2177 v: &crate::dsl::processors::change::Change<i64>,
2178 ) -> bytes::Bytes {
2179 bytes::Bytes::copy_from_slice(&v.new.unwrap_or(0).to_be_bytes())
2181 }
2182 fn deserialize(
2183 &self,
2184 _topic: &str,
2185 _bytes: &[u8],
2186 ) -> Result<crate::dsl::processors::change::Change<i64>, crate::processor::serde::SerdeError>
2187 {
2188 unreachable!("Change<i64> sink is never deserialized in these tests")
2189 }
2190 }
2191
2192 #[test]
2193 fn instantiate_caches_materialized_store_when_budget_positive() {
2194 let built = counting_topology(true);
2198 let mut g = pollster::block_on(built.instantiate(
2199 &crate::store::backend::StoreBackend::InMemory,
2200 "app",
2201 kibibytes(1),
2202 ))
2203 .unwrap();
2204 check!(g.cache_owner.get("counts") == Some(&0));
2205 check!(g.stores.kv_is_cached("counts"));
2206
2207 pollster::block_on(g.pipe("in", 0, 0, None, b"x", 0)).unwrap();
2211 pollster::block_on(g.pipe("in", 0, 1, None, b"x", 1)).unwrap();
2212 check!(
2214 g.drain_changelogs(&std::collections::HashSet::new())
2215 .is_empty()
2216 );
2217 pollster::block_on(g.flush_caches()).unwrap();
2218 let cl = g.drain_changelogs(&std::collections::HashSet::new());
2219 check!(cl.len() == 1); check!(cl[0].2.as_ref().unwrap().as_ref() == [0, 0, 0, 0, 0, 0, 0, 2]);
2222 }
2223
2224 #[test]
2225 fn instantiate_does_not_cache_when_budget_zero() {
2226 let built = counting_topology(true);
2230 let mut g = pollster::block_on(built.instantiate(
2231 &crate::store::backend::StoreBackend::InMemory,
2232 "app",
2233 ByteSize::ZERO,
2234 ))
2235 .unwrap();
2236 check!(g.cache_owner.is_empty());
2237 check!(!g.stores.kv_is_cached("counts"));
2238
2239 pollster::block_on(g.pipe("in", 0, 0, None, b"x", 0)).unwrap();
2240 pollster::block_on(g.pipe("in", 0, 1, None, b"x", 1)).unwrap();
2241 let cl = g.drain_changelogs(&std::collections::HashSet::new());
2243 check!(cl.len() == 2);
2244 }
2245
2246 #[test]
2247 fn instantiate_does_not_cache_when_marking_opted_out() {
2248 let built = counting_topology(false);
2251 let g = pollster::block_on(built.instantiate(
2252 &crate::store::backend::StoreBackend::InMemory,
2253 "app",
2254 kibibytes(1),
2255 ))
2256 .unwrap();
2257 check!(g.cache_owner.is_empty());
2258 check!(!g.stores.kv_is_cached("counts"));
2259 }
2260
2261 #[test]
2262 fn add_versioned_store_registers_wire_spec() {
2263 use crate::processor::serde::{I64Serde, StringSerde};
2264 let mut t = Topology::new();
2265 let src: NodeHandle<String, String> = t.add_source("src", ["in"]);
2266 let proc = t.add_processor("proc", || Upper, [&src]);
2267 t.add_versioned_store::<String, i64, _, _>(
2268 "vstore",
2269 StringSerde,
2270 I64Serde,
2271 minutes(10),
2272 [proc.name()],
2273 );
2274 t.add_sink("out", "out-topic", [&proc]);
2275 let wire = t.build("app").unwrap().to_wire();
2276 let blob = serde_json::to_value(&wire).unwrap().to_string();
2277 check!(blob.contains("vstore"), "changelog topic name not in wire");
2278 check!(
2279 blob.contains("min.compaction.lag.ms"),
2280 "min.compaction.lag.ms key not in wire"
2281 );
2282 check!(blob.contains("87000000"), "lag value not in wire");
2284 }
2285
2286 #[tokio::test]
2287 async fn add_window_store_uses_window_size_not_retention_for_cached_key_end() {
2288 use std::sync::{Arc, Mutex};
2295
2296 use crate::{
2297 dsl::{
2298 processors::change::Change,
2299 windows::{Window, Windowed},
2300 },
2301 processor::{record::RecordContext, serde::I64Serde},
2302 store::{
2303 api::StateStore,
2304 byte::InMemoryBytes,
2305 cache::named::NamedCache,
2306 window::{WindowBytesStore, WindowStore},
2307 },
2308 };
2309
2310 const D: Time = millis(10);
2311 let mut t = Topology::new();
2312 t.add_window_store::<String, i64, _, _>(
2314 "sw",
2315 StringSerde,
2316 I64Serde,
2317 (D * 2.0, D, Time::ZERO),
2318 ["p"],
2319 );
2320
2321 let (_changelog, factory) = t.store_factories.get("sw").expect("factory registered");
2323 let backend: Box<dyn crate::store::byte::ByteKeyValueStore> =
2324 Box::new(InMemoryBytes::default());
2325 let mut store: Box<dyn crate::store::api::StateStore> =
2326 factory("sw", "sw-changelog".to_string(), backend);
2327
2328 let typed = store
2330 .as_any_mut()
2331 .downcast_mut::<WindowBytesStore<String, i64>>()
2332 .expect("window store downcast");
2333 typed.enable_cache(Arc::new(Mutex::new(NamedCache::new("sw".into()))));
2334 typed.set_record_context(RecordContext {
2335 topic: "t".into(),
2336 partition: 0,
2337 offset: 0,
2338 timestamp: 7,
2339 });
2340 typed.put("a".into(), 0, 1, 7).await;
2341
2342 let mut buffer = std::collections::VecDeque::new();
2343 store.flush_cache_into(&mut buffer, &[0]).await;
2344 assert_eq!(buffer.len(), 1);
2345 let (_child, rec) = &buffer[0];
2346 let key = rec
2347 .key
2348 .as_ref()
2349 .unwrap()
2350 .downcast_ref::<Windowed<String>>()
2351 .unwrap();
2352 assert_eq!(key.key, "a");
2353 check!(
2355 key.window
2356 == Window {
2357 start: 0,
2358 end: D.millis_i64()
2359 }
2360 );
2361 let change = rec.value.downcast_ref::<Change<i64>>().unwrap();
2362 assert_eq!(change.new, Some(1));
2363 }
2364}