Skip to main content

nodedb_types/
calvin.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Primitive Calvin scheduling types shared between `nodedb-physical`
4//! (the physical-plan IR layer) and `nodedb-cluster` (the distributed
5//! Calvin sequencer / scheduler).
6//!
7//! Provides [`SortedVec`], [`EngineKeySet`], and [`PassiveReadKey`] —
8//! the building blocks of Calvin read/write sets. `DependentReadSpec`
9//! and other scheduler-internal aggregates stay in `nodedb-cluster`.
10
11use serde::{Deserialize, Serialize};
12
13use crate::{KeyRepr, Lsn};
14
15/// A newtype over `Vec<T>` that guarantees sorted, deduplicated contents.
16///
17/// Constructed via [`SortedVec::new`], which sorts and deduplicates at
18/// construction time. This property is load-bearing for byte-determinism:
19/// two `SortedVec`s built from the same logical set (in any insertion order)
20/// produce identical serialized bytes.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct SortedVec<T>(Vec<T>);
23
24impl<T: zerompk::ToMessagePack> zerompk::ToMessagePack for SortedVec<T> {
25    fn write<W: zerompk::Write>(&self, writer: &mut W) -> zerompk::Result<()> {
26        self.0.write(writer)
27    }
28}
29
30impl<'de, T> zerompk::FromMessagePack<'de> for SortedVec<T>
31where
32    T: zerompk::FromMessagePack<'de> + Ord + Clone,
33{
34    fn read<R: zerompk::Read<'de>>(reader: &mut R) -> zerompk::Result<Self> {
35        let v = Vec::<T>::read(reader)?;
36        Ok(Self::new(v))
37    }
38}
39
40impl<T: Ord + Clone> SortedVec<T> {
41    /// Build from any slice. Sorts and deduplicates in place.
42    pub fn new(mut items: Vec<T>) -> Self {
43        items.sort();
44        items.dedup();
45        Self(items)
46    }
47
48    pub fn as_slice(&self) -> &[T] {
49        &self.0
50    }
51
52    pub fn is_empty(&self) -> bool {
53        self.0.is_empty()
54    }
55
56    pub fn len(&self) -> usize {
57        self.0.len()
58    }
59
60    pub fn iter(&self) -> std::slice::Iter<'_, T> {
61        self.0.iter()
62    }
63}
64
65impl<T: Ord + Clone> From<Vec<T>> for SortedVec<T> {
66    fn from(v: Vec<T>) -> Self {
67        Self::new(v)
68    }
69}
70
71/// A typed key set for one engine within a read or write set.
72///
73/// Keys are normalized to surrogates (or byte keys for KV) at admission, so
74/// all engine-specific naming is resolved upstream of the sequencer.
75#[derive(
76    Debug,
77    Clone,
78    PartialEq,
79    Eq,
80    Serialize,
81    Deserialize,
82    zerompk::ToMessagePack,
83    zerompk::FromMessagePack,
84)]
85pub enum EngineKeySet {
86    /// Document engine (schemaless or strict): identified by surrogate.
87    Document {
88        collection: String,
89        surrogates: SortedVec<u32>,
90    },
91    /// Vector engine: identified by surrogate.
92    Vector {
93        collection: String,
94        surrogates: SortedVec<u32>,
95    },
96    /// Key-Value engine: identified by raw byte keys.
97    Kv {
98        collection: String,
99        keys: SortedVec<Vec<u8>>,
100    },
101    /// Graph edge engine: identified by (src_surrogate, dst_surrogate) pairs.
102    ///
103    /// `edges` carries the surrogate-pair IDENTITY used for Calvin locking and
104    /// conflict detection. `home_vshards` carries the from_key ROUTING homes —
105    /// the set of `VShardId::from_key(src_str)` / `from_key(dst_str)` u32 ids
106    /// for every edge in this set. Routing (which vShards participate) is driven
107    /// by `home_vshards`, NOT by the collection name, because a graph edge is
108    /// dual-homed across its two endpoint key-hashed vShards.
109    Edge {
110        collection: String,
111        edges: SortedVec<(u32, u32)>,
112        home_vshards: SortedVec<u32>,
113    },
114}
115
116impl EngineKeySet {
117    /// O(1) estimate of the serialized byte size of this key set.
118    ///
119    /// Used by the dependent-read cap check at sequencer admission to bound
120    /// the total bytes that would be Raft-replicated in a `CalvinReadResult`
121    /// entry.  This is an estimate, not an exact count; do NOT use it as a
122    /// correctness check — only as a pre-flight guard.
123    pub fn serialized_size_hint(&self) -> usize {
124        match self {
125            // u32 surrogates: 4 bytes each.
126            Self::Document { surrogates, .. } | Self::Vector { surrogates, .. } => {
127                surrogates.len() * 4
128            }
129            // KV keys: sum of key byte lengths.
130            Self::Kv { keys, .. } => keys.iter().map(|k| k.len()).sum(),
131            // Edge: two u32 per edge = 8 bytes each.
132            Self::Edge { edges, .. } => edges.len() * 8,
133        }
134    }
135
136    /// The collection this key set belongs to.
137    pub fn collection(&self) -> &str {
138        match self {
139            Self::Document { collection, .. }
140            | Self::Vector { collection, .. }
141            | Self::Kv { collection, .. }
142            | Self::Edge { collection, .. } => collection,
143        }
144    }
145
146    /// Returns `true` if this key set contains no keys.
147    pub fn is_empty(&self) -> bool {
148        match self {
149            Self::Document { surrogates, .. } => surrogates.is_empty(),
150            Self::Vector { surrogates, .. } => surrogates.is_empty(),
151            Self::Kv { keys, .. } => keys.is_empty(),
152            Self::Edge { edges, .. } => edges.is_empty(),
153        }
154    }
155}
156
157/// A single key that a passive participant must read and broadcast.
158///
159/// Wraps an [`EngineKeySet`]; per the dependent-read protocol each
160/// `PassiveReadKey` contains a single-element (or small) key set.  The
161/// sequencer does not enforce single-element sets; the scheduler enforces the
162/// total byte budget via `DependentReadSpec::total_bytes()` (which lives in
163/// `nodedb-cluster`).
164#[derive(
165    Debug,
166    Clone,
167    PartialEq,
168    Eq,
169    Serialize,
170    Deserialize,
171    zerompk::ToMessagePack,
172    zerompk::FromMessagePack,
173)]
174pub struct PassiveReadKey {
175    /// The engine key set to read on the passive vshard.
176    pub engine_key: EngineKeySet,
177}
178
179/// Which peer engine served a read. Mirrors the top-level physical-plan
180/// engine variants one-to-one so the classifier is total and a new engine
181/// forces a decision at compile time.
182///
183/// Encoded as a plain integer discriminant (`#[msgpack(c_enum)]`): stable,
184/// compact, and comparable across the wire. The discriminant assignment is
185/// load-bearing for on-wire stability — do NOT reorder variants.
186#[derive(
187    Debug,
188    Clone,
189    Copy,
190    PartialEq,
191    Eq,
192    Serialize,
193    Deserialize,
194    zerompk::ToMessagePack,
195    zerompk::FromMessagePack,
196)]
197#[msgpack(c_enum)]
198pub enum EngineTag {
199    Vector,
200    Graph,
201    Document,
202    Kv,
203    Text,
204    Columnar,
205    Timeseries,
206    Spatial,
207    Crdt,
208    Query,
209    Meta,
210    Array,
211    ClusterArray,
212}
213
214/// The identity a read observed within a collection, carried on the
215/// replicated Calvin `TxClass`.
216///
217/// `Point` carries the exact row identity ([`KeyRepr`]) for a keyed lookup
218/// (per-key optimistic-concurrency validation). `Predicate` is the coarse,
219/// collection-scoped observation for scans / searches / aggregates — safe
220/// against phantoms, never under-approximating. `IndexEq` / `IndexRange` carry
221/// the indexed dimension of a secondary-index equality / range read (canonical
222/// stringified index value, identical to the index-key segment) for narrower
223/// per-value validation.
224///
225/// New variants are APPENDED only — the on-wire encoding is positional, so
226/// reordering would break cross-version decode. Do NOT reorder variants.
227#[derive(
228    Debug,
229    Clone,
230    PartialEq,
231    Eq,
232    Serialize,
233    Deserialize,
234    zerompk::ToMessagePack,
235    zerompk::FromMessagePack,
236)]
237pub enum ReadKeyIdent {
238    /// A single-row keyed observation.
239    Point(KeyRepr),
240    /// A collection-scoped predicate observation.
241    Predicate,
242    /// A secondary-index equality observation on one indexed field.
243    IndexEq { field: String, value: String },
244    /// A secondary-index range observation on one indexed field. `lo`/`hi` are
245    /// optional so a one-sided native range is representable; both `None` is
246    /// never emitted.
247    IndexRange {
248        field: String,
249        lo: Option<String>,
250        hi: Option<String>,
251    },
252}
253
254/// One LSN-versioned, predicate-aware read observed by a transaction, carried
255/// on the replicated Calvin `TxClass` so participants can validate it at the
256/// commit serialization point.
257///
258/// `read_lsn` is the responding shard's write-LSN watermark at read time. The
259/// enclosing `TxClass` scopes the tenant; per-database scoping is carried by
260/// the transaction as a whole.
261#[derive(
262    Debug,
263    Clone,
264    PartialEq,
265    Eq,
266    Serialize,
267    Deserialize,
268    zerompk::ToMessagePack,
269    zerompk::FromMessagePack,
270)]
271pub struct VersionedReadEntry {
272    /// Which engine served the read.
273    pub engine: EngineTag,
274    /// The collection the read observed.
275    pub collection: String,
276    /// Point-key or collection-scoped-predicate identity of the observation.
277    pub key: ReadKeyIdent,
278    /// The responding shard's write-LSN watermark at read time.
279    pub read_lsn: Lsn,
280}
281
282/// The LSN-versioned read-set of a Calvin transaction.
283///
284/// Empty for pure-write transactions and for autocommit statements (which
285/// accumulate no session read-set). Populated at commit time from the neutral
286/// session read-set, and validated per-participant against the local
287/// write-version index at Calvin stage time (the commit vote on
288/// `read_set_valid`).
289#[derive(
290    Debug,
291    Clone,
292    Default,
293    PartialEq,
294    Eq,
295    Serialize,
296    Deserialize,
297    zerompk::ToMessagePack,
298    zerompk::FromMessagePack,
299)]
300pub struct VersionedReadSet(pub Vec<VersionedReadEntry>);
301
302impl VersionedReadSet {
303    /// Build from a vector of entries.
304    pub fn new(entries: Vec<VersionedReadEntry>) -> Self {
305        Self(entries)
306    }
307
308    /// Returns `true` if no reads were recorded.
309    pub fn is_empty(&self) -> bool {
310        self.0.is_empty()
311    }
312
313    /// Number of recorded read entries.
314    pub fn len(&self) -> usize {
315        self.0.len()
316    }
317
318    /// Iterate the recorded read entries.
319    pub fn iter(&self) -> std::slice::Iter<'_, VersionedReadEntry> {
320        self.0.iter()
321    }
322
323    /// Borrow the entries as a slice.
324    pub fn as_slice(&self) -> &[VersionedReadEntry] {
325        &self.0
326    }
327}