LCOV - code coverage report
Current view: top level - antares-sql/src/store/mem - redb.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 95.6 % 45 43
Test Date: 2026-09-21 10:31:06 Functions: 44.4 % 36 16

            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_META: TableDefinition<&str, &str> = TableDefinition::new("meta");
      38              : /// On-disk format version: bump on any key/value shape change; an older
      39              : /// or newer file refuses to load rather than being misread as valid data.
      40              : pub(super) const FORMAT_VERSION: &str = "1";
      41              : 
      42        56559 : pub(super) fn table_for(kind: Kind) -> TableDefinition<'static, &'static [u8], &'static [u8]> {
      43        56559 :     match kind {
      44        37990 :         Kind::Entity => T_ENTITIES,
      45         1480 :         Kind::Subscription => T_SUBSCRIPTIONS,
      46         1877 :         Kind::Registration => T_CSOURCE_REGISTRATIONS,
      47          108 :         Kind::CSourceSubscription => T_CSOURCE_SUBSCRIPTIONS,
      48        10812 :         Kind::Temporal => T_TEMPORAL_ENTITIES,
      49          624 :         Kind::Snapshot => T_SNAPSHOTS,
      50         2510 :         Kind::EntityMap => T_ENTITY_MAP_DOCS,
      51         1008 :         Kind::DistSub => T_DIST_SUBS,
      52          150 :         Kind::DeadLetter => T_DEAD_LETTERS,
      53              :     }
      54        56559 : }
      55              : 
      56              : /// Key = `tenant \0 id`. Unambiguous: TenantId is `[A-Za-z0-9_-]{1,64}`
      57              : /// by construction, so it can never contain the separator. Takes the tenant
      58              : /// as the plain string the maps are keyed by, so a persisted removal can
      59              : /// never be skipped for want of a re-parse.
      60        55869 : pub(super) fn key_bytes(tenant: &str, id: &str) -> Vec<u8> {
      61        55869 :     let mut k = Vec::with_capacity(tenant.len() + 1 + id.len());
      62        55869 :     k.extend_from_slice(tenant.as_bytes());
      63        55869 :     k.push(0);
      64        55869 :     k.extend_from_slice(id.as_bytes());
      65        55869 :     k
      66        55869 : }
      67              : 
      68           80 : pub(super) fn split_key(key: &[u8]) -> Option<(String, String)> {
      69          840 :     let pos = key.iter().position(|&b| b == 0)?;
      70              :     Some((
      71           80 :         String::from_utf8(key[..pos].to_vec()).ok()?,
      72           80 :         String::from_utf8(key[pos + 1..].to_vec()).ok()?,
      73              :     ))
      74           80 : }
      75              : 
      76              : pub(super) struct Shadow {
      77              :     pub(super) db: Database,
      78              : }
      79              : 
      80              : impl Shadow {
      81              :     /// One txn per mutation, fsynced before return. A failed commit
      82              :     /// aborts the process: the alternative is acking writes the file does not
      83              :     /// hold, which is the one lie a durable store must never tell.
      84              :     /// (Deliberately abort-on-commit-failure; per-request error plumbing only
      85              :     /// if a recoverable commit failure mode ever shows up in practice.)
      86         1241 :     pub(super) fn write(
      87         1241 :         &self,
      88         1241 :         table: TableDefinition<&[u8], &[u8]>,
      89         1241 :         key: &[u8],
      90         1241 :         value: Option<&[u8]>,
      91         1241 :     ) {
      92         1241 :         let result = (|| -> Result<(), String> {
      93         1241 :             let mut tx = self.db.begin_write().map_err(|e| e.to_string())?;
      94         1241 :             tx.set_durability(Durability::Immediate)
      95         1241 :                 .map_err(|e| e.to_string())?;
      96              :             {
      97         1241 :                 let mut t = tx.open_table(table).map_err(|e| e.to_string())?;
      98         1241 :                 match value {
      99          772 :                     Some(v) => {
     100          772 :                         t.insert(key, v).map_err(|e| e.to_string())?;
     101              :                     }
     102              :                     None => {
     103          469 :                         t.remove(key).map_err(|e| e.to_string())?;
     104              :                     }
     105              :                 }
     106              :             }
     107         1241 :             tx.commit().map_err(|e| e.to_string())
     108              :         })();
     109         1241 :         if let Err(e) = result {
     110            0 :             tracing::error!("redb commit failed: {e} — aborting: an acked write must be durable");
     111            0 :             std::process::abort();
     112         1241 :         }
     113         1241 :     }
     114              : }
        

Generated by: LCOV version 2.0-1