Skip to main content

krabka_client_streams/dsl/
cogrouped.rs

1//! KIP-150 cogroup. It aggregates several co-partitioned input streams into one
2//! `KTable`. Each stream has its own value type `Vn`, but they share the key `K`
3//! and the output type `VOut`, and each input contributes an
4//! `Aggregator<K, Vn, VOut>`.
5//!
6//! The topology holds one aggregate processor per input. All of them write to a
7//! single shared store, and they fan into one passthrough merge node that the
8//! result `KTable` reads.
9//!
10//! `KGroupedStream::cogroup` and `CogroupedKStream::cogroup` capture each input's
11//! lineage and a **type-erased** `make_agg` thunk that closes over the concrete
12//! `Vn` and the aggregator. The terminal `aggregate` and `windowed_by*` supply
13//! the shared `Initializer`, and for sessions the `Merger`, as an internal
14//! `CogroupSpec`. `lower_cogroup` then records the per-input repartition and
15//! aggregate nodes and the merge node, and it registers the shared store exactly
16//! once in the merge thunk.
17
18use 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/// Which window flavor the terminal aggregation uses.
38///
39/// This enum carries the window spec. The shared init, and the session merger,
40/// sit beside it in [`CogroupSpec`]. Each cogroup terminal, `aggregate` or
41/// `windowed_by*`, builds its own variant, and [`make_agg_for_input`] matches on
42/// it.
43#[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
51/// The terminal aggregation spec. The code builds it once at `aggregate()` time
52/// and clones it per input.
53///
54/// `init` and `merger` are `Arc`-erased, so a per-input `make_agg` thunk can hold
55/// them. Such a thunk does not know the concrete closure type of `VOut`.
56type 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
76/// Given a [`CogroupSpec`], an input returns a node-lowering thunk that adds its
77/// per-window aggregate processor wired to `parent_name`, named `proc_name`,
78/// pointing at `store_name`, and returns the lowered processor's handle name.
79type 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/// One cogrouped input: its grouped lineage plus the erased per-window aggregate
83/// builder.
84#[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    /// The single source topic that this input traces to, when the input is not
91    /// key-changing. The code uses it to register a copartition group over all
92    /// cogroup inputs.
93    pub source_topic: Option<String>,
94}
95
96/// A handle that collects the cogrouped inputs. The terminal `aggregate` and
97/// `windowed_by*` consume it.
98///
99/// `builder` and `inputs` are `pub(crate)`, so the windowed-handle modules can
100/// move the inputs into their own handles. Build this type through the DSL entry
101/// points.
102pub 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    /// Build the handle from the builder and the collected inputs. It keeps the
110    /// `_pd` marker private, so no call site depends on the field set.
111    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
123/// Build the erased `make_agg` for one input. It closes over the concrete `Vn`
124/// and the aggregator.
125///
126/// The returned thunk matches on the window kind and attaches the right
127/// per-window processor. `KGroupedStream::cogroup` and the chained
128/// `CogroupedKStream::cogroup` share this function.
129pub(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                                    // Cogroup does not expose emit-final; default to emit-on-update.
194                                    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                                    // Cogroup does not expose emit-final; default to emit-on-update.
228                                    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                                    // Cogroup does not expose emit-final; default to emit-on-update.
268                                    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    /// Chain another co-partitioned input with its own aggregator.
291    #[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    /// Non-windowed terminal aggregation, which gives a `KTable<K, VOut>`.
309    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        // Store registrar: a non-windowed KV store, honoring Materialized logging.
337        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            // Per-input aggregators suppress their immediate forward when this
355            // store is cached; the merge passthrough then only relays the cache
356            // flush's deduped change. Honor Materialized::with_caching.
357            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    /// Non-windowed terminal aggregation with default serdes.
383    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
406/// Registers the shared cogroup store with the given per-input processor names.
407/// It is boxed, so each terminal supplies its own window-specific store type and
408/// serdes.
409pub(crate) type StoreRegistrarFn = Box<dyn FnOnce(&mut LowerState, Vec<String>) + Send>;
410
411/// Record the nodes in id order: each input's optional repartition, then its
412/// aggregate node against the shared store, and last the merge node.
413///
414/// The merge thunk attaches the passthrough processor, whose parents are all the
415/// aggregate handles, and runs `registrar` once to register the shared store. The
416/// function returns the merge node id, which is the result `KTable`'s source. It
417/// is generic over the merge output key `KOut`, which is `K` when non-windowed
418/// and `Windowed<K>` when windowed.
419pub(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    // Collect source topics for the copartition group declaration (only include
435    // inputs whose lineage traces to a single non-repartitioned source topic).
436    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                // Per-input nodes share the store; only the merge node owns the
461                // changelog (set `changelog: logging` below). The actual store
462                // registration happens once, in the merge thunk via `registrar`.
463                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        // Declare the copartition group when all inputs trace to a single source
505        // topic (i.e. none needed a repartition). The JVM cogroup does the same.
506        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    // Release the RefCell borrow so `KTable::new` can borrow the builder again.
514    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    /// The cogroup store is record-cached when the budget is positive and
526    /// caching is on, which is the default. Each per-input aggregator suppresses
527    /// its immediate forward when the store is cached. The merge passthrough
528    /// relays only the deduped flush change, so there is no double emit.
529    #[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            // A generous (default-sized) budget: a marked cogroup store must land
545            // in cache_owner.
546            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    /// Two co-grouped inputs aggregate into one cached KV store.
569    ///
570    /// Within a single batch, in1 adds `len(value)` and in2 adds 1. The cached
571    /// store is marked, so `cache_owner` is rooted, and both per-input forwards
572    /// are suppressed. The flush then emits ONE deduped record. Its value,
573    /// 3 = 2 + 1, proves that in2's aggregator read in1's buffered accumulator,
574    /// which is cross-input read-your-writes.
575    #[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        // in1: key "a" value "xx" (len 2) → acc 2 ; in2: key "a" value "z" → acc 3.
598        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        // Both per-input forwards suppressed until flush.
601        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        // 3 = in1(+2) then in2(+1) — in2 read in1's buffered accumulator.
608        check!(out[0].value.as_ref().unwrap().as_ref() == 3i64.to_be_bytes());
609    }
610
611    /// `with_caching(false)` keeps the cogroup store uncached even with a
612    /// budget.
613    #[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}