LCOV - code coverage report
Current view: top level - antares-sql/src/store/mem - redb.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 96.2 % 53 51
Test Date: 2026-11-02 13:21:27 Functions: 39.7 % 63 25

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! `file` mode durability: the redb write-through shadow behind the
       3              : //! in-memory maps, its tables and key layout.
       4              : 
       5              : use ::redb::{Database, Durability, TableDefinition};
       6              : use antares_store::Kind;
       7              : 
       8              : // ---- `file` mode: redb write-through shadow --------------------------------
       9              : //
      10              : // redb is durability only — queries and the matcher keep running on the
      11              : // in-memory maps. Every mutation commits to redb (Durability::Immediate,
      12              : // fsync) INSIDE the store's write-critical section, so redb apply order is
      13              : // exactly memory apply order, and the commit happens before the store call
      14              : // returns — i.e. before the HTTP ack (commit-before-ack). Boot rebuilds
      15              : // the maps from the file and refuses to start on a format mismatch.
      16              : //
      17              : // Table per resource family, named after the spec resource, snake_cased.
      18              : // The v0 memory store keeps one temporal doc per entity, so `attr_instances`
      19              : // has no separate table; entityMaps are TTL-ephemeral and not durable state.
      20              : pub(super) const T_ENTITIES: TableDefinition<&[u8], &[u8]> = TableDefinition::new("entities");
      21              : pub(super) const T_SUBSCRIPTIONS: TableDefinition<&[u8], &[u8]> =
      22              :     TableDefinition::new("subscriptions");
      23              : pub(super) const T_CSOURCE_REGISTRATIONS: TableDefinition<&[u8], &[u8]> =
      24              :     TableDefinition::new("csource_registrations");
      25              : pub(super) const T_CSOURCE_SUBSCRIPTIONS: TableDefinition<&[u8], &[u8]> =
      26              :     TableDefinition::new("csource_subscriptions");
      27              : pub(super) const T_TEMPORAL_ENTITIES: TableDefinition<&[u8], &[u8]> =
      28              :     TableDefinition::new("temporal_entities");
      29              : pub(super) const T_JSONLD_CONTEXTS: TableDefinition<&[u8], &[u8]> =
      30              :     TableDefinition::new("jsonld_contexts");
      31              : pub(super) const T_SNAPSHOTS: TableDefinition<&[u8], &[u8]> = TableDefinition::new("snapshots");
      32              : pub(super) const T_ENTITY_MAP_DOCS: TableDefinition<&[u8], &[u8]> =
      33              :     TableDefinition::new("entity_map_docs");
      34              : pub(super) const T_DIST_SUBS: TableDefinition<&[u8], &[u8]> = TableDefinition::new("dist_subs");
      35              : pub(super) const T_DEAD_LETTERS: TableDefinition<&[u8], &[u8]> =
      36              :     TableDefinition::new("dead_letters");
      37              : pub(super) const T_VIEWS: TableDefinition<&[u8], &[u8]> = TableDefinition::new("views");
      38              : pub(super) const T_META: TableDefinition<&str, &str> = TableDefinition::new("meta");
      39              : /// On-disk format version: bump on any key/value shape change; an older
      40              : /// or newer file refuses to load rather than being misread as valid data.
      41              : pub(super) const FORMAT_VERSION: &str = "1";
      42              : 
      43              : /// A table of the shadow: document bytes under a `tenant \0 id` key.
      44              : pub(super) type Table = TableDefinition<'static, &'static [u8], &'static [u8]>;
      45              : 
      46       440915 : pub(super) fn table_for(kind: Kind) -> Table {
      47       440915 :     match kind {
      48       178488 :         Kind::Entity => T_ENTITIES,
      49         6637 :         Kind::Subscription => T_SUBSCRIPTIONS,
      50         3796 :         Kind::Registration => T_CSOURCE_REGISTRATIONS,
      51          781 :         Kind::CSourceSubscription => T_CSOURCE_SUBSCRIPTIONS,
      52       245032 :         Kind::Temporal => T_TEMPORAL_ENTITIES,
      53          807 :         Kind::Snapshot => T_SNAPSHOTS,
      54         2598 :         Kind::EntityMap => T_ENTITY_MAP_DOCS,
      55         2486 :         Kind::DistSub => T_DIST_SUBS,
      56          176 :         Kind::DeadLetter => T_DEAD_LETTERS,
      57          114 :         Kind::View => T_VIEWS,
      58              :     }
      59       440915 : }
      60              : 
      61              : /// Key = `tenant \0 id`. Unambiguous: TenantId is `[A-Za-z0-9_-]{1,64}`
      62              : /// by construction, so it can never contain the separator. Takes the tenant
      63              : /// as the plain string the maps are keyed by, so a persisted removal can
      64              : /// never be skipped for want of a re-parse.
      65       443231 : pub(super) fn key_bytes(tenant: &str, id: &str) -> Vec<u8> {
      66       443231 :     let mut k = Vec::with_capacity(tenant.len().saturating_add(1).saturating_add(id.len()));
      67       443231 :     k.extend_from_slice(tenant.as_bytes());
      68       443231 :     k.push(0);
      69       443231 :     k.extend_from_slice(id.as_bytes());
      70       443231 :     k
      71       443231 : }
      72              : 
      73         1710 : pub(super) fn split_key(key: &[u8]) -> Option<(String, String)> {
      74         4238 :     let pos = key.iter().position(|&b| b == 0)?;
      75         1710 :     let (tenant, rest) = key.split_at_checked(pos)?;
      76              :     Some((
      77         1710 :         String::from_utf8(tenant.to_vec()).ok()?,
      78         1710 :         String::from_utf8(rest.get(1..)?.to_vec()).ok()?,
      79              :     ))
      80         1710 : }
      81              : 
      82              : pub(super) struct Shadow {
      83              :     pub(super) db: Database,
      84              : }
      85              : 
      86              : impl Shadow {
      87              :     /// One txn per mutation, fsynced before return. A failed commit
      88              :     /// aborts the process: the alternative is acking writes the file does not
      89              :     /// hold, which is the one lie a durable store must never tell.
      90              :     /// (Deliberately abort-on-commit-failure; per-request error plumbing only
      91              :     /// if a recoverable commit failure mode ever shows up in practice.)
      92        12381 :     pub(super) fn write(&self, table: Table, key: &[u8], value: Option<&[u8]>) {
      93        12381 :         self.write_all([(table, key, value)]);
      94        12381 :     }
      95              : 
      96              :     /// Several writes (`None` = remove the key) in ONE txn, one fsync: a
      97              :     /// multi-document operation (a purge, the sweep, a retention batch) is
      98              :     /// on disk whole or not at all. Same abort rule as [`Self::write`].
      99        13013 :     pub(super) fn write_all<'a>(
     100        13013 :         &self,
     101        13013 :         writes: impl IntoIterator<Item = (Table, &'a [u8], Option<&'a [u8]>)>,
     102        13013 :     ) {
     103        13013 :         let mut writes = writes.into_iter().peekable();
     104        13013 :         if writes.peek().is_none() {
     105          475 :             return;
     106        12538 :         }
     107        12538 :         let result = (|| -> Result<(), String> {
     108        12538 :             let mut tx = self.db.begin_write().map_err(|e| e.to_string())?;
     109        12538 :             tx.set_durability(Durability::Immediate)
     110        12538 :                 .map_err(|e| e.to_string())?;
     111        14197 :             for (table, key, value) in writes {
     112        14197 :                 let mut t = tx.open_table(table).map_err(|e| e.to_string())?;
     113        14197 :                 match value {
     114         9792 :                     Some(v) => {
     115         9792 :                         t.insert(key, v).map_err(|e| e.to_string())?;
     116              :                     }
     117              :                     None => {
     118         4405 :                         t.remove(key).map_err(|e| e.to_string())?;
     119              :                     }
     120              :                 }
     121              :             }
     122        12538 :             tx.commit().map_err(|e| e.to_string())
     123              :         })();
     124        12538 :         if let Err(e) = result {
     125            0 :             tracing::error!("redb commit failed: {e} — aborting: an acked write must be durable");
     126            0 :             std::process::abort();
     127        12538 :         }
     128        13013 :     }
     129              : }
        

Generated by: LCOV version 2.0-1