LCOV - code coverage report
Current view: top level - antares-api/src - mirror.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 94.6 % 295 279
Test Date: 2026-09-21 10:31:06 Functions: 49.6 % 123 61

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! The tenant-keyed document mirrors the notification pipeline and the
       3              : //! bus wiring share: a registration mirror, a subscription mirror with
       4              : //! its inverted candidate index and sweep clocks, and the change event
       5              : //! every write emits. A leaf: nothing here names another module, so the
       6              : //! application state can hold these types without depending on the
       7              : //! pipeline that consumes them.
       8              : 
       9              : use serde_json::Value;
      10              : 
      11              : /// One change queue event: tenant, before-image, after-image.
      12              : pub type Change = (String, Option<Value>, Option<Value>);
      13              : 
      14              : /// A per-instance tenant-keyed document mirror (bus=nats). One
      15              : /// instance holds subscriptions (fed by the KV watcher), another holds
      16              : /// registrations (fed by `ANTARES_REGISTRY` deltas). Postgres stays the
      17              : /// system of record — this map is a cache with exactly one writer (the
      18              : /// watcher task); readers only snapshot.
      19              : ///
      20              : /// A snapshot hands out `Arc`s, not copies. When this mirror is installed
      21              : /// it IS the federation read path's registration source ([`Self::matching`]
      22              : /// replaces the store's `matching_registrations`), and a broker at the
      23              : /// 100 000-registration target that deep-copied every document per
      24              : /// distributed request paid 84 ms and a hundred megabytes for a set it only
      25              : /// ever reads. The documents are immutable once applied — a changed
      26              : /// registration arrives as a whole new document — so sharing them costs a
      27              : /// refcount and nothing else.
      28              : #[derive(Default)]
      29              : pub struct DocMirror {
      30              :     map: std::sync::RwLock<std::collections::HashMap<String, RegIndex>>,
      31              : }
      32              : 
      33              : /// One tenant's registrations, plus the two dimensions a distributed read
      34              : /// narrows on before it evaluates 5.12 matching per registration.
      35              : ///
      36              : /// The buckets are the mirror's answer to the same question
      37              : /// `matching_registrations` pushes into the store's `csource_index`, and
      38              : /// they obey the same contract: a bucket may only ever drop a registration
      39              : /// `reg_candidate` would reject anyway. So a registration lands in `any_*`
      40              : /// — the set no key can exclude — whenever the dimension cannot decide it:
      41              : /// a `RegistrationInfo` with no `entities` at all, an `EntityInfo` with no
      42              : /// `type` (5.12 restricts such an entry by id alone), or an `EntityInfo`
      43              : /// carrying an `idPattern` (5.12 condition 5 forwards on a pattern whatever
      44              : /// ids were asked for).
      45              : ///
      46              : /// The membership is per REGISTRATION, not per `EntityInfo` — a
      47              : /// registration whose one entry carries the asked-for type and whose other
      48              : /// carries the asked-for id is kept, where the store's per-row `WHERE` drops
      49              : /// it. Wider than the store, which is the safe direction for a prefilter.
      50              : #[derive(Default)]
      51              : struct RegIndex {
      52              :     docs: std::collections::HashMap<String, std::sync::Arc<Value>>,
      53              :     by_type: std::collections::HashMap<String, std::collections::HashSet<String>>,
      54              :     by_id: std::collections::HashMap<String, std::collections::HashSet<String>>,
      55              :     any_type: std::collections::HashSet<String>,
      56              :     any_id: std::collections::HashSet<String>,
      57              : }
      58              : 
      59              : /// The type and id keys one registration document occupies. `None` for a
      60              : /// dimension means no key of it can exclude this registration.
      61          490 : fn reg_keys(doc: &Value) -> (Option<Vec<String>>, Option<Vec<String>>) {
      62          490 :     let (mut types, mut ids) = (Vec::new(), Vec::new());
      63          490 :     let (mut any_type, mut any_id) = (false, false);
      64              :     // No `information` at all is not a shape 5.2.9 allows, and nothing can
      65              :     // be read off it — the registration stays in both broad sets rather
      66              :     // than being narrowed out on a document this code does not understand.
      67          490 :     let Some(infos) = doc.get("information").and_then(Value::as_array) else {
      68            8 :         return (None, None);
      69              :     };
      70          482 :     if infos.is_empty() {
      71            0 :         return (None, None);
      72          482 :     }
      73          484 :     for info in infos {
      74              :         // 5.2.9: a RegistrationInfo may name only propertyNames /
      75              :         // relationshipNames, which restricts attributes and no entity.
      76          484 :         let Some(entities) = info.get("entities").and_then(Value::as_array) else {
      77           90 :             return (None, None);
      78              :         };
      79          394 :         if entities.is_empty() {
      80            0 :             return (None, None);
      81          394 :         }
      82          394 :         for ei in entities {
      83          394 :             match crate::registry::ei_types(ei) {
      84          394 :                 ts if ts.is_empty() => any_type = true,
      85          228 :                 ts => types.extend(ts.into_iter().map(str::to_owned)),
      86              :             }
      87          394 :             if ei.get("idPattern").is_some() {
      88           90 :                 any_id = true;
      89           90 :             } else {
      90          304 :                 match ei.get("id").and_then(Value::as_str) {
      91          184 :                     Some(id) => ids.push(id.to_owned()),
      92          120 :                     None => any_id = true,
      93              :                 }
      94              :             }
      95              :         }
      96              :     }
      97          392 :     ((!any_type).then_some(types), (!any_id).then_some(ids))
      98          490 : }
      99              : 
     100              : /// The registrations a set of keys can reach: the ones filed under a named
     101              : /// key, plus every one the dimension cannot decide. `None` in, `None` out —
     102              : /// the caller asked nothing of this dimension, so it narrows nothing.
     103          272 : fn bucketed<'a>(
     104          272 :     keys: Option<&[String]>,
     105          272 :     by: &'a std::collections::HashMap<String, std::collections::HashSet<String>>,
     106          272 :     any: &'a std::collections::HashSet<String>,
     107          272 : ) -> Option<std::collections::HashSet<&'a str>> {
     108          272 :     let keys = keys?;
     109          212 :     let mut out: std::collections::HashSet<&str> = any.iter().map(String::as_str).collect();
     110          212 :     for k in keys {
     111          212 :         if let Some(ids) = by.get(k) {
     112          198 :             out.extend(ids.iter().map(String::as_str));
     113          198 :         }
     114              :     }
     115          212 :     Some(out)
     116          272 : }
     117              : 
     118              : impl RegIndex {
     119              :     /// Drop a registration from the documents and from every bucket it is
     120              :     /// filed under. The keys come from the document being removed, so the
     121              :     /// index never needs a second copy of them.
     122          488 :     fn remove(&mut self, id: &str) {
     123          488 :         let Some(old) = self.docs.remove(id) else {
     124          480 :             return;
     125              :         };
     126            8 :         let (types, ids) = reg_keys(&old);
     127            8 :         let unfile =
     128              :             |keys: Option<Vec<String>>,
     129              :              by: &mut std::collections::HashMap<String, std::collections::HashSet<String>>,
     130           16 :              any: &mut std::collections::HashSet<String>| {
     131           16 :                 match keys {
     132           10 :                     None => {
     133           10 :                         any.remove(id);
     134           10 :                     }
     135            6 :                     Some(ks) => {
     136            6 :                         for k in ks {
     137            6 :                             if let Some(set) = by.get_mut(&k) {
     138            6 :                                 set.remove(id);
     139            6 :                                 if set.is_empty() {
     140            6 :                                     by.remove(&k);
     141            6 :                                 }
     142            0 :                             }
     143              :                         }
     144              :                     }
     145              :                 }
     146           16 :             };
     147            8 :         unfile(types, &mut self.by_type, &mut self.any_type);
     148            8 :         unfile(ids, &mut self.by_id, &mut self.any_id);
     149          488 :     }
     150              : 
     151          482 :     fn insert(&mut self, id: &str, doc: Value) {
     152          482 :         self.remove(id);
     153          482 :         let (types, ids) = reg_keys(&doc);
     154          482 :         let file =
     155              :             |keys: Option<Vec<String>>,
     156              :              by: &mut std::collections::HashMap<String, std::collections::HashSet<String>>,
     157          964 :              any: &mut std::collections::HashSet<String>| {
     158          964 :                 match keys {
     159          562 :                     None => {
     160          562 :                         any.insert(id.to_owned());
     161          562 :                     }
     162          402 :                     Some(ks) => {
     163          406 :                         for k in ks {
     164          406 :                             by.entry(k).or_default().insert(id.to_owned());
     165          406 :                         }
     166              :                     }
     167              :                 }
     168          964 :             };
     169          482 :         file(types, &mut self.by_type, &mut self.any_type);
     170          482 :         file(ids, &mut self.by_id, &mut self.any_id);
     171          482 :         self.docs.insert(id.to_owned(), std::sync::Arc::new(doc));
     172          482 :     }
     173              : 
     174            6 :     fn is_empty(&self) -> bool {
     175            6 :         self.docs.is_empty()
     176            6 :     }
     177              : }
     178              : 
     179              : /// Both mirror flavours accept `{tenant, id, doc|null}` deltas — the seam
     180              : /// wiring's hydrate/watch loops program against.
     181              : pub trait Mirror: Send + Sync {
     182              :     fn apply(&self, tenant: &str, id: &str, doc: Option<Value>);
     183              : 
     184              :     /// A Context Source Registration Subscription was written somewhere.
     185              :     /// Mirrors that serve no interval sweep have nothing to do.
     186            0 :     fn csub_written(&self) {}
     187              : }
     188              : 
     189              : impl Mirror for DocMirror {
     190           52 :     fn apply(&self, tenant: &str, id: &str, doc: Option<Value>) {
     191           52 :         DocMirror::apply(self, tenant, id, doc);
     192           52 :     }
     193              : }
     194              : 
     195              : /// The subscription mirror — docs plus the inverted candidate index.
     196              : ///
     197              : /// Bucketing per subscription (conservative, union-of-buckets = candidates):
     198              : /// - has an `entities` selector whose every entry names a plain expanded
     199              : ///   type IRI → `by_type[iri]` (idPattern/watchedAttributes narrow FURTHER,
     200              : ///   so type is the widest exact key);
     201              : /// - no selector but `watchedAttributes` → `by_attr[iri]` (such a sub can
     202              : ///   only fire when a watched attribute changed — 5.8.6);
     203              : /// - anything else (4.17 selection expressions, shapes the index cannot
     204              : ///   prove) → `broad`, evaluated on every change.
     205              : ///
     206              : /// The mirror also carries the interval sweep's clocks: the earliest instant
     207              : /// at which a periodic subscription it holds can be due (5.8.6 sends that
     208              : /// Notification "when the time interval (in seconds) specified in such value
     209              : /// field is reached"), so ticks that cannot fire anything never read the
     210              : /// store. Per instance, not global — one broker process, one clock pair.
     211              : #[derive(Default)]
     212              : pub struct SubMirror {
     213              :     map: std::sync::RwLock<std::collections::HashMap<String, TenantIndex>>,
     214              :     /// Epoch millis; `0` = sweep at the next tick. Set by every sweep from
     215              :     /// the subscriptions it saw, and zeroed again by `apply` whenever a
     216              :     /// periodic subscription is written, so a new one is never waited out.
     217              :     pub(crate) next_sub_sweep_ms: std::sync::atomic::AtomicI64,
     218              :     /// The same clock for Context Source Registration Subscriptions
     219              :     /// (5.11.7). They are not mirrored as documents — the sweep reads them
     220              :     /// from the store — so a write signals this clock through
     221              :     /// [`SubMirror::csub_written`] instead, and a sweep falls back to
     222              :     /// `CSUB_SWEEP_BACKSTOP_MS` in case a signal was lost.
     223              :     pub(crate) next_csub_sweep_ms: std::sync::atomic::AtomicI64,
     224              : }
     225              : 
     226              : /// The longest a sweep parks the Context Source Registration Subscription
     227              : /// half when nothing it saw is due.
     228              : ///
     229              : /// A write clears the clock, so this is not the path a newly created
     230              : /// periodic subscription waits on: it is the repair time for a lost signal,
     231              : /// which on the bus is a KV put that exhausted its retries. Between sweeps
     232              : /// the half costs one `list` per tenant, and the tenant target is 10 000, so
     233              : /// polling it at the tick rate is the broker's whole idle cost with nothing
     234              : /// periodic configured — which is the state most deployments are in.
     235              : ///
     236              : /// Table 5.2.12-1 bounds `timeInterval` only by "greater than 0", so a
     237              : /// sub-second interval is legal; the tick period bounds how closely any of
     238              : /// them can be served, signal or no signal.
     239              : // ponytail: one `list` per tenant per backstop, cross-tenant enumeration of
     240              : // the periodic rows would replace it — that needs the RLS service escape
     241              : // (`antares.service`) extended to the two subscription tables.
     242              : pub(crate) const CSUB_SWEEP_BACKSTOP_MS: i64 = 60_000;
     243              : 
     244              : #[derive(Default)]
     245              : struct TenantIndex {
     246              :     // Shared, not owned: `candidates` runs once per change and hands back
     247              :     // every subscription that could fire. Cloning the documents there deep-
     248              :     // copied each one — every string and every nested map — on the hot path,
     249              :     // so the cost of a change grew with the size of the subscriptions it was
     250              :     // evaluated against rather than with the work it caused. An `Arc` clone
     251              :     // is a refcount bump, and the registration mirror above already holds
     252              :     // its documents this way.
     253              :     docs: std::collections::HashMap<String, std::sync::Arc<Value>>,
     254              :     by_type: std::collections::HashMap<String, std::collections::HashSet<String>>,
     255              :     by_attr: std::collections::HashMap<String, std::collections::HashSet<String>>,
     256              :     broad: std::collections::HashSet<String>,
     257              : }
     258              : 
     259              : /// Which index bucket(s) one stored subscription doc belongs in.
     260              : pub(crate) enum Keys {
     261              :     Types(Vec<String>),
     262              :     Attrs(Vec<String>),
     263              :     Broad,
     264              : }
     265              : 
     266         1868 : pub(crate) fn index_keys(doc: &Value) -> Keys {
     267         1868 :     if let Some(entities) = doc.get("entities").and_then(Value::as_array) {
     268         1658 :         let mut types = Vec::new();
     269         1662 :         for e in entities {
     270         1662 :             match e.get("type").and_then(Value::as_str) {
     271              :                 // 4.17 selection expressions (and any wildcard) are evaluated
     272              :                 // at match time — the index cannot prove them, so the whole
     273              :                 // sub goes broad (entries are OR-ed: one opaque entry taints
     274              :                 // the union).
     275         1638 :                 Some(t) if !t.contains(['|', ',', ';', '(', ')', '*']) => {
     276         1502 :                     types.push(t.to_owned());
     277         1502 :                 }
     278          160 :                 _ => return Keys::Broad,
     279              :             }
     280              :         }
     281         1498 :         if types.is_empty() {
     282            0 :             return Keys::Broad;
     283         1498 :         }
     284         1498 :         return Keys::Types(types);
     285          210 :     }
     286          210 :     if let Some(watched) = doc.get("watchedAttributes").and_then(Value::as_array) {
     287          110 :         let attrs: Vec<String> = watched
     288          110 :             .iter()
     289          110 :             .filter_map(Value::as_str)
     290          110 :             .map(str::to_owned)
     291          110 :             .collect();
     292          110 :         if attrs.len() == watched.len() && !attrs.is_empty() {
     293          110 :             return Keys::Attrs(attrs);
     294            0 :         }
     295          100 :     }
     296          100 :     Keys::Broad
     297         1868 : }
     298              : 
     299              : impl SubMirror {
     300              :     /// Apply one KV delta: `None` doc = deleted. Rekeys the index from the
     301              :     /// old doc before inserting the new one.
     302         1146 :     pub fn apply(&self, tenant: &str, id: &str, doc: Option<Value>) {
     303         1146 :         let mut map = self
     304         1146 :             .map
     305         1146 :             .write()
     306         1146 :             .unwrap_or_else(std::sync::PoisonError::into_inner);
     307         1146 :         let t = map.entry(tenant.to_owned()).or_default();
     308         1146 :         if let Some(old) = t.docs.remove(id) {
     309          402 :             match index_keys(&old) {
     310          400 :                 Keys::Types(ts) => {
     311          400 :                     for ty in ts {
     312          400 :                         if let Some(s) = t.by_type.get_mut(&ty) {
     313          400 :                             s.remove(id);
     314          400 :                             if s.is_empty() {
     315          348 :                                 t.by_type.remove(&ty);
     316          348 :                             }
     317            0 :                         }
     318              :                     }
     319              :                 }
     320            0 :                 Keys::Attrs(ats) => {
     321            0 :                     for a in ats {
     322            0 :                         if let Some(s) = t.by_attr.get_mut(&a) {
     323            0 :                             s.remove(id);
     324            0 :                             if s.is_empty() {
     325            0 :                                 t.by_attr.remove(&a);
     326            0 :                             }
     327            0 :                         }
     328              :                     }
     329              :                 }
     330            2 :                 Keys::Broad => {
     331            2 :                     t.broad.remove(id);
     332            2 :                 }
     333              :             }
     334          744 :         }
     335         1146 :         if let Some(d) = doc {
     336         1086 :             if d.get("timeInterval").is_some() {
     337           24 :                 // A periodic subscription just appeared or changed its
     338           24 :                 // anchor: the sweep clock computed without it must not hold
     339           24 :                 // it back (5.8.6).
     340           24 :                 self.next_sub_sweep_ms
     341           24 :                     .store(0, std::sync::atomic::Ordering::Relaxed);
     342         1062 :             }
     343         1086 :             match index_keys(&d) {
     344          762 :                 Keys::Types(ts) => {
     345          766 :                     for ty in ts {
     346          766 :                         t.by_type.entry(ty).or_default().insert(id.to_owned());
     347          766 :                     }
     348              :                 }
     349          106 :                 Keys::Attrs(ats) => {
     350          106 :                     for a in ats {
     351          106 :                         t.by_attr.entry(a).or_default().insert(id.to_owned());
     352          106 :                     }
     353              :                 }
     354          218 :                 Keys::Broad => {
     355          218 :                     t.broad.insert(id.to_owned());
     356          218 :                 }
     357              :             }
     358         1086 :             t.docs.insert(id.to_owned(), std::sync::Arc::new(d));
     359           60 :         }
     360         1146 :         if map.get(tenant).is_some_and(|t| t.docs.is_empty()) {
     361           56 :             map.remove(tenant);
     362         1090 :         }
     363         1146 :     }
     364              : 
     365              :     /// 5.11.7: a Context Source Registration Subscription was written. Only
     366              :     /// the clock moves — these are matched against registrations rather than
     367              :     /// entities, so they are not carried in the candidate index, and the
     368              :     /// sweep reads the tenant's rows from the store once it wakes.
     369            8 :     pub(crate) fn csub_written(&self) {
     370            8 :         self.next_csub_sweep_ms
     371            8 :             .store(0, std::sync::atomic::Ordering::Relaxed);
     372            8 :     }
     373              : 
     374              :     /// The hot path: subscriptions that could possibly fire for a change
     375              :     /// touching these entity types and these changed attributes. Union of
     376              :     /// the type hits, the attr hits and the broad bucket — a superset of
     377              :     /// the firing set, never a subset.
     378         2511 :     pub fn candidates(
     379         2511 :         &self,
     380         2511 :         tenant: &str,
     381         2511 :         types: &[&str],
     382         2511 :         changed_attrs: &[&str],
     383         2511 :     ) -> Vec<std::sync::Arc<Value>> {
     384         2511 :         let map = self
     385         2511 :             .map
     386         2511 :             .read()
     387         2511 :             .unwrap_or_else(std::sync::PoisonError::into_inner);
     388         2511 :         let Some(t) = map.get(tenant) else {
     389         2081 :             return Vec::new();
     390              :         };
     391          430 :         let mut ids: std::collections::HashSet<&str> = t.broad.iter().map(String::as_str).collect();
     392          432 :         for ty in types {
     393          432 :             if let Some(s) = t.by_type.get(*ty) {
     394          416 :                 ids.extend(s.iter().map(String::as_str));
     395          416 :             }
     396              :         }
     397          452 :         for a in changed_attrs {
     398          452 :             if let Some(s) = t.by_attr.get(*a) {
     399          106 :                 ids.extend(s.iter().map(String::as_str));
     400          346 :             }
     401              :         }
     402          430 :         ids.iter()
     403        12230 :             .filter_map(|id| t.docs.get(*id).cloned())
     404          430 :             .collect()
     405         2511 :     }
     406              : 
     407              :     #[cfg(any(test, feature = "test-kit"))]
     408            8 :     pub fn docs(&self, tenant: &str) -> Vec<std::sync::Arc<Value>> {
     409            8 :         self.map
     410            8 :             .read()
     411            8 :             .unwrap_or_else(std::sync::PoisonError::into_inner)
     412            8 :             .get(tenant)
     413            8 :             .map(|t| t.docs.values().cloned().collect())
     414            8 :             .unwrap_or_default()
     415            8 :     }
     416              : 
     417              :     /// The interval sweep's whole input: the tenant's periodic (5.2.12
     418              :     /// `timeInterval`) subscriptions. The walk is over every subscription the
     419              :     /// tenant holds, but only the periodic ones are carried out of it — the
     420              :     /// sweep clocks keep a tick with nothing due from reaching this at all.
     421          131 :     pub(crate) fn periodic_docs(&self, tenant: &str) -> Vec<std::sync::Arc<Value>> {
     422          131 :         self.map
     423          131 :             .read()
     424          131 :             .unwrap_or_else(std::sync::PoisonError::into_inner)
     425          131 :             .get(tenant)
     426          131 :             .map(|t| {
     427           40 :                 t.docs
     428           40 :                     .values()
     429           48 :                     .filter(|d| d.get("timeInterval").is_some())
     430           40 :                     .cloned()
     431           40 :                     .collect()
     432           40 :             })
     433          131 :             .unwrap_or_default()
     434          131 :     }
     435              : 
     436              :     #[cfg(any(test, feature = "test-kit"))]
     437            2 :     pub fn tenants(&self) -> Vec<String> {
     438            2 :         self.map
     439            2 :             .read()
     440            2 :             .unwrap_or_else(std::sync::PoisonError::into_inner)
     441            2 :             .keys()
     442            2 :             .cloned()
     443            2 :             .collect()
     444            2 :     }
     445              : }
     446              : 
     447              : #[cfg(test)]
     448              : impl SubMirror {
     449              :     /// Poison the index lock from a thread that panics while holding it:
     450              :     /// what a matcher that dies mid-write leaves behind, for the
     451              :     /// supervision tests.
     452            4 :     pub(crate) fn poison(self: &std::sync::Arc<Self>) {
     453            4 :         let m = std::sync::Arc::clone(self);
     454            4 :         let _ = std::thread::spawn(move || {
     455            4 :             let _held = m
     456            4 :                 .map
     457            4 :                 .write()
     458            4 :                 .unwrap_or_else(std::sync::PoisonError::into_inner);
     459            4 :             panic!("poison the mirror");
     460              :         })
     461            4 :         .join();
     462            4 :     }
     463              : }
     464              : 
     465              : impl Mirror for SubMirror {
     466            8 :     fn apply(&self, tenant: &str, id: &str, doc: Option<Value>) {
     467            8 :         SubMirror::apply(self, tenant, id, doc);
     468            8 :     }
     469              : 
     470            2 :     fn csub_written(&self) {
     471            2 :         SubMirror::csub_written(self);
     472            2 :     }
     473              : }
     474              : 
     475              : impl DocMirror {
     476              :     /// Apply one KV delta: `None` doc = deleted.
     477          488 :     pub fn apply(&self, tenant: &str, id: &str, doc: Option<Value>) {
     478          488 :         let mut map = self
     479          488 :             .map
     480          488 :             .write()
     481          488 :             .unwrap_or_else(std::sync::PoisonError::into_inner);
     482          488 :         match doc {
     483          482 :             Some(d) => {
     484          482 :                 map.entry(tenant.to_owned()).or_default().insert(id, d);
     485          482 :             }
     486              :             None => {
     487            6 :                 if let Some(t) = map.get_mut(tenant) {
     488            6 :                     t.remove(id);
     489            6 :                     if t.is_empty() {
     490            4 :                         map.remove(tenant);
     491            4 :                     }
     492            0 :                 }
     493              :             }
     494              :         }
     495          488 :     }
     496              : 
     497              :     /// The registrations of one tenant that can match these ids and types,
     498              :     /// shared rather than copied.
     499              :     ///
     500              :     /// Both dimensions are optional and each narrows on its own; an absent
     501              :     /// one is a dimension the caller asked nothing about, never an empty
     502              :     /// answer. With both given a registration has to survive both, which is
     503              :     /// the store's rule too.
     504          152 :     pub fn matching(
     505          152 :         &self,
     506          152 :         tenant: &str,
     507          152 :         ids: Option<&[String]>,
     508          152 :         types: Option<&[String]>,
     509          152 :     ) -> Vec<std::sync::Arc<Value>> {
     510          152 :         let map = self
     511          152 :             .map
     512          152 :             .read()
     513          152 :             .unwrap_or_else(std::sync::PoisonError::into_inner);
     514          152 :         let Some(t) = map.get(tenant) else {
     515           16 :             return Vec::new();
     516              :         };
     517          136 :         let by_type = bucketed(types, &t.by_type, &t.any_type);
     518          136 :         let by_id = bucketed(ids, &t.by_id, &t.any_id);
     519          136 :         let pick = |keys: std::collections::HashSet<&str>| -> Vec<std::sync::Arc<Value>> {
     520          124 :             keys.into_iter()
     521         7268 :                 .filter_map(|k| t.docs.get(k).map(std::sync::Arc::clone))
     522          124 :                 .collect()
     523          124 :         };
     524          136 :         match (by_type, by_id) {
     525           12 :             (None, None) => t.docs.values().map(std::sync::Arc::clone).collect(),
     526           36 :             (Some(k), None) | (None, Some(k)) => pick(k),
     527           88 :             (Some(a), Some(b)) => pick(a.intersection(&b).copied().collect()),
     528              :         }
     529          152 :     }
     530              : 
     531           18 :     pub fn docs(&self, tenant: &str) -> Vec<Value> {
     532           18 :         self.matching(tenant, None, None)
     533           18 :             .into_iter()
     534           18 :             .map(|d| (*d).clone())
     535           18 :             .collect()
     536           18 :     }
     537              : 
     538              :     #[cfg(any(test, feature = "test-kit"))]
     539            6 :     pub fn tenants(&self) -> Vec<String> {
     540            6 :         self.map
     541            6 :             .read()
     542            6 :             .unwrap_or_else(std::sync::PoisonError::into_inner)
     543            6 :             .keys()
     544            6 :             .cloned()
     545            6 :             .collect()
     546            6 :     }
     547              : }
        

Generated by: LCOV version 2.0-1