Skip to main content

rushdb_storage/
tenant_dir.rs

1//! Tenant directory: scope digest ↔ 4-byte tenant ordinal.
2//!
3//! The rows live in the system namespace and are written by an ordinary
4//! commit (`Mutation::RegisterTenant`, expanded by LMPG), so registration is
5//! logged, replicated and replayed like any other write. Ordinals are never
6//! reused: a purged tenant keeps its rows with state `Purged`, and a later
7//! registration of the same scope gets a fresh ordinal, so leftovers of the
8//! old ordinal can never become visible. This module owns the encoding and a
9//! bounded resolution cache.
10
11use std::collections::HashMap;
12
13use rushdb_types::ids::{TenantId, TenantScope};
14use rushdb_types::keys;
15
16use crate::{StorageError, Store};
17
18/// Maximum cached scope → ordinal entries; the cache is cleared when full
19/// (entries are re-read from disk on the next miss).
20const CACHE_ENTRIES: usize = 65_536;
21
22/// Lifecycle state of a directory entry.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum TenantState {
25    /// Serving reads and writes.
26    Active,
27    /// Purged; the ordinal is retired forever.
28    Purged,
29}
30
31impl TenantState {
32    fn to_byte(self) -> u8 {
33        match self {
34            Self::Active => 0,
35            Self::Purged => 1,
36        }
37    }
38
39    fn from_byte(byte: u8) -> Result<Self, StorageError> {
40        match byte {
41            0 => Ok(Self::Active),
42            1 => Ok(Self::Purged),
43            _ => Err(StorageError::CorruptLog),
44        }
45    }
46}
47
48/// Encode a scope → ordinal row value.
49#[must_use]
50pub fn encode_by_scope(tenant: TenantId, state: TenantState) -> Vec<u8> {
51    let mut out = tenant.to_be_bytes().to_vec();
52    out.push(state.to_byte());
53    out
54}
55
56/// Decode a scope → ordinal row value.
57///
58/// # Errors
59/// Returns [`StorageError::CorruptLog`] for a malformed value.
60pub fn decode_by_scope(raw: &[u8]) -> Result<(TenantId, TenantState), StorageError> {
61    let (ordinal, state) = raw.split_at_checked(4).ok_or(StorageError::CorruptLog)?;
62    let ordinal: [u8; 4] = ordinal.try_into().map_err(|_| StorageError::CorruptLog)?;
63    let [state] = state else {
64        return Err(StorageError::CorruptLog);
65    };
66    Ok((
67        TenantId(u32::from_be_bytes(ordinal)),
68        TenantState::from_byte(*state)?,
69    ))
70}
71
72/// Encode an ordinal → scope row value.
73#[must_use]
74pub fn encode_by_ord(scope: &TenantScope, state: TenantState) -> Vec<u8> {
75    let mut out = scope.0.to_vec();
76    out.push(state.to_byte());
77    out
78}
79
80/// Decode an ordinal → scope row value.
81///
82/// # Errors
83/// Returns [`StorageError::CorruptLog`] for a malformed value.
84pub fn decode_by_ord(raw: &[u8]) -> Result<(TenantScope, TenantState), StorageError> {
85    let (digest, state) = raw.split_at_checked(32).ok_or(StorageError::CorruptLog)?;
86    let digest: [u8; 32] = digest.try_into().map_err(|_| StorageError::CorruptLog)?;
87    let [state] = state else {
88        return Err(StorageError::CorruptLog);
89    };
90    Ok((TenantScope(digest), TenantState::from_byte(*state)?))
91}
92
93/// Decode the next-ordinal row (absent → [`TenantId::FIRST_ASSIGNED`]).
94///
95/// # Errors
96/// Returns [`StorageError::CorruptLog`] for a malformed value.
97pub fn decode_seq(raw: Option<&[u8]>) -> Result<u32, StorageError> {
98    raw.map_or(Ok(TenantId::FIRST_ASSIGNED), |raw| {
99        raw.try_into()
100            .map(u32::from_be_bytes)
101            .map_err(|_| StorageError::CorruptLog)
102    })
103}
104
105/// Bounded scope → ordinal cache in front of the directory rows.
106#[derive(Default)]
107pub(crate) struct TenantCache {
108    entries: HashMap<TenantScope, TenantId>,
109}
110
111impl Store {
112    /// The active ordinal registered for `scope`, if any. Reads through a
113    /// bounded cache; a miss reads the committed directory row.
114    ///
115    /// # Errors
116    /// Returns [`StorageError`] if the directory row cannot be read or is
117    /// malformed.
118    pub fn resolve_tenant(&self, scope: &TenantScope) -> Result<Option<TenantId>, StorageError> {
119        if let Some(tenant) = self
120            .health
121            .guard(self.tenant_cache.lock())?
122            .entries
123            .get(scope)
124        {
125            return Ok(Some(*tenant));
126        }
127        let Some(raw) = self.try_get(&keys::tenant_by_scope_key(scope))? else {
128            return Ok(None);
129        };
130        let (tenant, state) = decode_by_scope(&raw)?;
131        if state != TenantState::Active {
132            return Ok(None);
133        }
134        let mut cache = self.health.guard(self.tenant_cache.lock())?;
135        if cache.entries.len() >= CACHE_ENTRIES {
136            cache.entries.clear();
137        }
138        cache.entries.insert(*scope, tenant);
139        Ok(Some(tenant))
140    }
141
142    /// The scope digest registered for an ordinal, with its state.
143    ///
144    /// # Errors
145    /// Returns [`StorageError`] if the directory row cannot be read or is
146    /// malformed.
147    pub fn tenant_scope(
148        &self,
149        tenant: TenantId,
150    ) -> Result<Option<(TenantScope, TenantState)>, StorageError> {
151        self.try_get(&keys::tenant_by_ord_key(tenant))?
152            .map(|raw| decode_by_ord(&raw))
153            .transpose()
154    }
155
156    /// Every active registered tenant, in ordinal order (O(tenants)).
157    ///
158    /// # Errors
159    /// Returns [`StorageError`] if the directory cannot be read or a row is
160    /// malformed.
161    pub fn active_tenants(&self) -> Result<Vec<(TenantId, TenantScope)>, StorageError> {
162        let prefix = keys::tenant_by_ord_prefix();
163        let mut out = Vec::new();
164        for (key, raw) in self.try_range_prefix(&prefix)? {
165            let ordinal: [u8; 4] = key
166                .get(prefix.len()..)
167                .and_then(|bytes| bytes.try_into().ok())
168                .ok_or(StorageError::CorruptLog)?;
169            let (scope, state) = decode_by_ord(&raw)?;
170            if state == TenantState::Active {
171                out.push((TenantId(u32::from_be_bytes(ordinal)), scope));
172            }
173        }
174        Ok(out)
175    }
176
177    /// Drop a scope from the resolution cache (after it is retired).
178    ///
179    /// Lock poisoning fences the store until reopen.
180    pub fn forget_tenant(&self, scope: &TenantScope) {
181        self.health
182            .cleanup(self.tenant_cache.lock())
183            .entries
184            .remove(scope);
185    }
186}