Skip to main content

krabka_client_streams/store/
versioned.rs

1//! Versioned key-value store (KIP-889).
2//!
3//! Each key maps to a chain of versions keyed by `validFrom`, in epoch millis. A
4//! version's value is `Some` when live and `None` for a tombstone. `get` returns
5//! the latest version. `get_as_of(t)` returns the version valid at `t`. The store
6//! expires history older than `history_retention`, measured from the max observed
7//! timestamp.
8//!
9//! Changelog format (KIP-889 / JVM-exact):
10//! - Changelog KEY  = raw key bytes.
11//! - Changelog VALUE = bare serialized value bytes (`Some`) or `None` (tombstone).
12//! - The version timestamp lives in the Kafka RECORD timestamp field.
13use std::{any::Any, collections::BTreeMap};
14
15use async_trait::async_trait;
16use bytes::{BufMut, Bytes};
17use krabka_units::prelude::*;
18
19use crate::{
20    processor::serde::Serde,
21    store::{
22        api::StateStore,
23        snapshot::{SnapshotReader, put_sized, store_payload},
24    },
25};
26
27/// A single resolved version.
28///
29/// The struct holds the value, the timestamp the version became valid, and the
30/// timestamp the next version superseded it. A `None` there means the version is
31/// still the latest, ∞.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct VersionedRecord<V> {
34    pub value: V,
35    pub valid_from: i64,
36    pub valid_to: Option<i64>,
37}
38
39/// Typed versioned store surface.
40#[async_trait]
41pub trait VersionedKeyValueStore<K: Send + Sync, V: Send>: StateStore {
42    /// Inserts a version at `validFrom = timestamp`.
43    ///
44    /// A `value == None` is a tombstone version. The store inserts out-of-order
45    /// timestamps mid-chain. The latest pointer only advances when
46    /// `timestamp >=` the current max. The store drops a timestamp older than the
47    /// retention horizon.
48    async fn put(&mut self, key: K, value: Option<V>, timestamp: i64);
49    /// Inserts a tombstone version at `timestamp`.
50    ///
51    /// Returns the record that was valid at `timestamp` just before the delete,
52    /// if there was a live value.
53    async fn delete(&mut self, key: &K, timestamp: i64) -> Option<VersionedRecord<V>>;
54    /// Returns the latest live version.
55    ///
56    /// Returns `None` if the key is absent, or if the latest version is a
57    /// tombstone.
58    async fn get(&self, key: &K) -> Option<VersionedRecord<V>>;
59    /// Returns the version valid at `as_of`.
60    ///
61    /// Returns `None` if the key is absent, if that version is a tombstone, or if
62    /// `as_of` predates the oldest retained version.
63    async fn get_as_of(&self, key: &K, as_of: i64) -> Option<VersionedRecord<V>>;
64}
65
66pub struct VersionedBytesStore<K, V> {
67    name: String,
68    changelog_topic: String,
69    history_retention: Time,
70    key_serde: Box<dyn Serde<K>>,
71    value_serde: Box<dyn Serde<V>>,
72    // key bytes -> (validFrom -> Some(value bytes) | None tombstone)
73    chains: BTreeMap<Bytes, BTreeMap<i64, Option<Bytes>>>,
74    observed_stream_time: i64,
75    // (key, bare value | None tombstone, version_ts)
76    changelog: Vec<(Bytes, Option<Bytes>, i64)>,
77    logging: bool,
78}
79
80impl<K: 'static, V: 'static> VersionedBytesStore<K, V> {
81    #[must_use]
82    pub(crate) fn new(
83        name: String,
84        history_retention: Time,
85        key_serde: Box<dyn Serde<K>>,
86        value_serde: Box<dyn Serde<V>>,
87        changelog_topic: String,
88    ) -> Self {
89        Self {
90            name,
91            changelog_topic,
92            history_retention,
93            key_serde,
94            value_serde,
95            chains: BTreeMap::new(),
96            observed_stream_time: i64::MIN,
97            changelog: Vec::new(),
98            logging: true,
99        }
100    }
101
102    #[must_use]
103    pub fn in_memory(
104        name: String,
105        history_retention: Time,
106        key_serde: Box<dyn Serde<K>>,
107        value_serde: Box<dyn Serde<V>>,
108        changelog_topic: String,
109    ) -> Self {
110        Self::new(
111            name,
112            history_retention,
113            key_serde,
114            value_serde,
115            changelog_topic,
116        )
117    }
118
119    /// Returns the retention horizon.
120    ///
121    /// Versions whose `valid_to` is strictly below the horizon are unreachable,
122    /// and the store may evict them. The store drops a put below the horizon.
123    fn horizon(&self) -> i64 {
124        // The horizon is an instant, so the retention extent crosses into epoch
125        // milliseconds here.
126        self.observed_stream_time
127            .saturating_sub(self.history_retention.millis_i64())
128    }
129
130    /// Inserts raw version bytes into a chain and applies retention.
131    ///
132    /// The bytes are already serialized. `put` and `delete` share this method
133    /// with logging on, and restore shares it with logging off.
134    fn insert_raw(&mut self, key: Bytes, valid_from: i64, value: Option<Bytes>) -> bool {
135        self.observed_stream_time = self.observed_stream_time.max(valid_from);
136        // Compute horizon after updating stream time (horizon depends on it).
137        let h = self.horizon();
138        // KIP-889 out-of-bounds drop: if an existing later version already
139        // supersedes this one entirely below the horizon, it is not retained.
140        // Checked before `entry()` so a dropped put for a brand-new key does not
141        // leave an orphan empty chain.
142        if let Some(chain) = self.chains.get(&key)
143            && let Some(next_above) = chain.range((valid_from + 1)..).next().map(|(t, _)| *t)
144            && next_above <= h
145        {
146            return false;
147        }
148        let chain = self.chains.entry(key).or_default();
149        chain.insert(valid_from, value);
150        // Evict versions whose successor is at/below the horizon (their validTo
151        // makes them unreachable). The latest version (no successor) is never a
152        // left edge here, so the chain always retains at least the new version.
153        let times: Vec<i64> = chain.keys().copied().collect();
154        for w in times.windows(2) {
155            let (from, next) = (w[0], w[1]);
156            if next <= h {
157                chain.remove(&from);
158            }
159        }
160        true
161    }
162}
163
164#[async_trait]
165impl<K: Send + 'static, V: Send + 'static> StateStore for VersionedBytesStore<K, V> {
166    fn name(&self) -> &str {
167        &self.name
168    }
169    async fn flush(&mut self) {}
170    fn close(&mut self) {}
171    fn as_any_mut(&mut self) -> &mut dyn Any {
172        self
173    }
174    fn changelog_topic(&self) -> &str {
175        &self.changelog_topic
176    }
177    fn take_changelog(&mut self) -> Vec<(Bytes, Option<Bytes>)> {
178        self.changelog.drain(..).map(|(k, v, _ts)| (k, v)).collect()
179    }
180    fn take_changelog_ts(&mut self) -> Vec<(Bytes, Option<Bytes>, Option<i64>)> {
181        self.changelog
182            .drain(..)
183            .map(|(k, v, ts)| (k, v, Some(ts)))
184            .collect()
185    }
186    async fn apply_changelog(&mut self, _key: Bytes, _value: Option<Bytes>) {
187        // Bare changelog value without a timestamp cannot be placed into the
188        // version chain; callers should use apply_changelog_ts instead.
189        // This no-op fallback keeps the trait object safe.
190    }
191    async fn apply_changelog_ts(&mut self, key: Bytes, value: Option<Bytes>, timestamp: i64) {
192        self.insert_raw(key, timestamp, value);
193    }
194    fn set_logging(&mut self, on: bool) {
195        self.logging = on;
196    }
197    fn as_iq(&self) -> Option<&dyn crate::store::iq::IqQueryable> {
198        Some(self)
199    }
200    async fn clear(&mut self) {
201        self.chains.clear();
202        self.changelog.clear();
203        self.observed_stream_time = i64::MIN;
204    }
205    async fn snapshot(&mut self) -> Bytes {
206        // A versioned store needs more than the byte backend, because it does
207        // not have one. Its state is a version chain per key plus the observed
208        // stream time, and the horizon that expires history comes from that
209        // time. A payload without it would restore a store that re-expires the
210        // wrong versions.
211        let mut buffer = store_payload();
212        buffer.put_i64(self.observed_stream_time);
213        buffer.put_u32(u32::try_from(self.chains.len()).expect("chain count fits u32"));
214        for (key, chain) in &self.chains {
215            put_sized(&mut buffer, key);
216            buffer.put_u32(u32::try_from(chain.len()).expect("version count fits u32"));
217            for (valid_from, value) in chain {
218                buffer.put_i64(*valid_from);
219                match value {
220                    Some(value) => {
221                        buffer.put_u8(1);
222                        put_sized(&mut buffer, value);
223                    }
224                    // A tombstone version holds no bytes of its own.
225                    None => buffer.put_u8(0),
226                }
227            }
228        }
229        buffer.freeze()
230    }
231    async fn restore_snapshot(
232        &mut self,
233        data: Bytes,
234    ) -> Result<(), crate::error::StreamsClientError> {
235        let mut reader = SnapshotReader::new(&data);
236        reader.store_version()?;
237        let observed_stream_time = reader.i64()?;
238        let chain_count = reader.u32()?;
239        let mut chains: BTreeMap<Bytes, BTreeMap<i64, Option<Bytes>>> = BTreeMap::new();
240        for _ in 0..chain_count {
241            let key = reader.sized()?;
242            let version_count = reader.u32()?;
243            let mut chain = BTreeMap::new();
244            for _ in 0..version_count {
245                let valid_from = reader.i64()?;
246                let value = match reader.u8()? {
247                    0 => None,
248                    1 => Some(reader.sized()?),
249                    tag => {
250                        return Err(crate::error::StreamsClientError::Snapshot(format!(
251                            "unknown versioned store value tag {tag}"
252                        )));
253                    }
254                };
255                chain.insert(valid_from, value);
256            }
257            chains.insert(key, chain);
258        }
259        reader.finish()?;
260        // Assign the chains as they were, and do not replay them through
261        // `insert_raw`. A replay would re-apply retention and could drop a
262        // version the snapshot kept.
263        self.chains = chains;
264        self.observed_stream_time = observed_stream_time;
265        self.changelog.clear();
266        Ok(())
267    }
268}
269
270#[async_trait]
271impl<K: Send + Sync + 'static, V: Send + 'static> VersionedKeyValueStore<K, V>
272    for VersionedBytesStore<K, V>
273{
274    async fn put(&mut self, key: K, value: Option<V>, timestamp: i64) {
275        let kb = self.key_serde.serialize(&self.changelog_topic, &key);
276        let vb = value
277            .as_ref()
278            .map(|v| self.value_serde.serialize(&self.changelog_topic, v));
279        let inserted = self.insert_raw(kb.clone(), timestamp, vb.clone());
280        if inserted && self.logging {
281            // Changelog VALUE = bare serialized value (KIP-889 / JVM-exact).
282            // The version timestamp lives in the Kafka record timestamp field.
283            self.changelog.push((kb, vb, timestamp));
284        }
285    }
286
287    async fn delete(&mut self, key: &K, timestamp: i64) -> Option<VersionedRecord<V>> {
288        let prev = self.get_as_of(key, timestamp).await;
289        let kb = self.key_serde.serialize(&self.changelog_topic, key);
290        let inserted = self.insert_raw(kb.clone(), timestamp, None);
291        if inserted && self.logging {
292            // Tombstone: None value; timestamp in record timestamp field.
293            self.changelog.push((kb, None, timestamp));
294        }
295        prev
296    }
297
298    async fn get(&self, key: &K) -> Option<VersionedRecord<V>> {
299        let kb = self.key_serde.serialize(&self.changelog_topic, key);
300        let chain = self.chains.get(&kb)?;
301        let (&valid_from, value) = chain.iter().next_back()?;
302        let raw = value.as_ref()?; // latest is a tombstone => None
303        Some(VersionedRecord {
304            value: self
305                .value_serde
306                .deserialize(&self.changelog_topic, raw)
307                .expect("versioned value deserialize"),
308            valid_from,
309            valid_to: None,
310        })
311    }
312
313    async fn get_as_of(&self, key: &K, as_of: i64) -> Option<VersionedRecord<V>> {
314        let kb = self.key_serde.serialize(&self.changelog_topic, key);
315        let chain = self.chains.get(&kb)?;
316        let (&valid_from, value) = chain.range(..=as_of).next_back()?;
317        let raw = value.as_ref()?; // that version is a tombstone => None
318        let valid_to = chain.range((as_of + 1)..).next().map(|(t, _)| *t);
319        Some(VersionedRecord {
320            value: self
321                .value_serde
322                .deserialize(&self.changelog_topic, raw)
323                .expect("versioned value deserialize"),
324            valid_from,
325            valid_to,
326        })
327    }
328}
329
330// Holds only `Box<dyn Serde<_>>` + byte buffers → `Send + Sync` for any K/V.
331#[async_trait]
332impl<K: Send + 'static, V: Send + 'static> crate::store::iq::IqQueryable
333    for VersionedBytesStore<K, V>
334{
335    fn kind(&self) -> crate::store::iq::StoreKind {
336        crate::store::iq::StoreKind::Versioned
337    }
338    async fn iq_versioned_get(&self, key: &[u8]) -> Option<(i64, Option<i64>, Bytes)> {
339        let chain = self.chains.get(key)?;
340        let (&vf, value) = chain.iter().next_back()?;
341        let raw = value.as_ref()?;
342        Some((vf, None, raw.clone()))
343    }
344    async fn iq_versioned_get_as_of(
345        &self,
346        key: &[u8],
347        as_of: i64,
348    ) -> Option<(i64, Option<i64>, Bytes)> {
349        let chain = self.chains.get(key)?;
350        let (&vf, value) = chain.range(..=as_of).next_back()?;
351        let raw = value.as_ref()?;
352        let vt = chain.range((as_of + 1)..).next().map(|(t, _)| *t);
353        Some((vf, vt, raw.clone()))
354    }
355
356    async fn iq2_execute(
357        &self,
358        query: &crate::store::iq::Iq2Query,
359    ) -> Result<Box<dyn Any + Send>, crate::store::iq::Iq2Failure> {
360        use crate::store::iq::{Iq2Failure, Iq2Query};
361        let ser = |b: &dyn Any| -> Result<Bytes, Iq2Failure> {
362            let k = b.downcast_ref::<K>().ok_or(Iq2Failure::KeyTypeMismatch)?;
363            Ok(self.key_serde.serialize(&self.changelog_topic, k))
364        };
365        let deser = |raw: &[u8]| -> V {
366            self.value_serde
367                .deserialize(&self.changelog_topic, raw)
368                .expect("iqv2 versioned value deserialize")
369        };
370        match query {
371            Iq2Query::VersionedKey { key, as_of } => {
372                let kb = ser(&**key)?;
373                let out: Option<VersionedRecord<V>> = self.chains.get(&kb).and_then(|chain| {
374                    let (valid_from, value, valid_to) = match as_of {
375                        // Latest live version: last chain entry, valid_to = ∞.
376                        None => {
377                            let (&vf, value) = chain.iter().next_back()?;
378                            (vf, value, None)
379                        }
380                        // Version valid at `t`: greatest validFrom <= t; valid_to
381                        // = the next validFrom after `t` (if any).
382                        Some(t) => {
383                            let (&vf, value) = chain.range(..=*t).next_back()?;
384                            let vt = chain.range((*t + 1)..).next().map(|(x, _)| *x);
385                            (vf, value, vt)
386                        }
387                    };
388                    let raw = value.as_ref()?; // tombstone => None
389                    Some(VersionedRecord {
390                        value: deser(raw),
391                        valid_from,
392                        valid_to,
393                    })
394                });
395                Ok(Box::new(out))
396            }
397            Iq2Query::MultiVersionedKey {
398                key,
399                from_ts,
400                to_ts,
401                descending,
402            } => {
403                let kb = ser(&**key)?;
404                let from = from_ts.unwrap_or(i64::MIN);
405                let to = to_ts.unwrap_or(i64::MAX);
406                let mut out: Vec<VersionedRecord<V>> = Vec::new();
407                if let Some(chain) = self.chains.get(&kb) {
408                    let entries: Vec<(i64, &Option<Bytes>)> =
409                        chain.iter().map(|(t, v)| (*t, v)).collect();
410                    for (i, (valid_from, value)) in entries.iter().enumerate() {
411                        let Some(raw) = value.as_ref() else { continue }; // skip tombstones
412                        let valid_to = entries.get(i + 1).map(|(t, _)| *t);
413                        // Overlap of [valid_from, valid_to) with inclusive [from, to].
414                        let overlaps = *valid_from <= to && valid_to.is_none_or(|vt| vt > from);
415                        if overlaps {
416                            out.push(VersionedRecord {
417                                value: deser(raw),
418                                valid_from: *valid_from,
419                                valid_to,
420                            });
421                        }
422                    }
423                }
424                if *descending {
425                    out.reverse();
426                }
427                Ok(Box::new(out))
428            }
429            _ => Err(Iq2Failure::UnknownQueryType),
430        }
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437    use crate::processor::serde::{I64Serde, StringSerde};
438
439    fn store(retention: Time) -> VersionedBytesStore<String, i64> {
440        VersionedBytesStore::in_memory(
441            "v".into(),
442            retention,
443            Box::new(StringSerde),
444            Box::new(I64Serde),
445            "app-v-changelog".into(),
446        )
447    }
448
449    #[tokio::test]
450    async fn latest_and_as_of() {
451        let mut s = store(secs(1_000));
452        s.put("k".into(), Some(10), 100).await;
453        s.put("k".into(), Some(20), 200).await;
454        assert_eq!(s.get(&"k".into()).await.map(|r| r.value), Some(20));
455        let r = s.get_as_of(&"k".into(), 150).await.unwrap();
456        assert_eq!((r.value, r.valid_from, r.valid_to), (10, 100, Some(200)));
457        assert_eq!(s.get_as_of(&"k".into(), 50).await, None);
458    }
459
460    #[tokio::test]
461    async fn out_of_order_does_not_clobber_latest() {
462        let mut s = store(secs(1_000));
463        s.put("k".into(), Some(20), 200).await;
464        s.put("k".into(), Some(10), 100).await;
465        assert_eq!(s.get(&"k".into()).await.map(|r| r.value), Some(20));
466        assert_eq!(
467            s.get_as_of(&"k".into(), 150).await.map(|r| r.value),
468            Some(10)
469        );
470    }
471
472    #[tokio::test]
473    async fn tombstone_hides_latest_but_keeps_history() {
474        let mut s = store(secs(1_000));
475        s.put("k".into(), Some(10), 100).await;
476        let prev = s.delete(&"k".into(), 200).await;
477        assert_eq!(prev.map(|r| r.value), Some(10));
478        assert_eq!(s.get(&"k".into()).await, None);
479        assert_eq!(
480            s.get_as_of(&"k".into(), 150).await.map(|r| r.value),
481            Some(10)
482        );
483    }
484
485    #[tokio::test]
486    async fn retention_drops_old_put_and_evicts_history() {
487        let mut s = store(millis(50));
488        s.put("k".into(), Some(10), 100).await;
489        s.put("k".into(), Some(20), 200).await;
490        s.put("k".into(), Some(5), 40).await;
491        let cl = s.take_changelog();
492        assert_eq!(cl.len(), 2);
493        assert_eq!(s.get_as_of(&"k".into(), 40).await, None);
494    }
495
496    #[tokio::test]
497    // one cohesive test of every iq2_execute branch
498    async fn iq2_versioned_key_and_multi() {
499        use crate::store::iq::{Iq2Failure, Iq2Query, IqQueryable, StoreKind};
500        let mut s = store(secs(1_000));
501        // Three in-order versions of "k": 10@100, 20@200, 30@300.
502        s.put("k".into(), Some(10), 100).await;
503        s.put("k".into(), Some(20), 200).await;
504        s.put("k".into(), Some(30), 300).await;
505        let q: &dyn IqQueryable = s.as_iq().unwrap();
506        assert_eq!(q.kind(), StoreKind::Versioned);
507
508        // VersionedKeyQuery latest.
509        let latest = q
510            .iq2_execute(&Iq2Query::VersionedKey {
511                key: Box::new("k".to_string()),
512                as_of: None,
513            })
514            .await
515            .unwrap();
516        assert_eq!(
517            *latest.downcast::<Option<VersionedRecord<i64>>>().unwrap(),
518            Some(VersionedRecord {
519                value: 30,
520                valid_from: 300,
521                valid_to: None
522            })
523        );
524
525        // VersionedKeyQuery as_of(250) → the 20@200 version, superseded at 300.
526        let asof = q
527            .iq2_execute(&Iq2Query::VersionedKey {
528                key: Box::new("k".to_string()),
529                as_of: Some(250),
530            })
531            .await
532            .unwrap();
533        assert_eq!(
534            *asof.downcast::<Option<VersionedRecord<i64>>>().unwrap(),
535            Some(VersionedRecord {
536                value: 20,
537                valid_from: 200,
538                valid_to: Some(300)
539            })
540        );
541
542        // as_of(50) predates the oldest version → None.
543        let miss = q
544            .iq2_execute(&Iq2Query::VersionedKey {
545                key: Box::new("k".to_string()),
546                as_of: Some(50),
547            })
548            .await
549            .unwrap();
550        assert_eq!(
551            *miss.downcast::<Option<VersionedRecord<i64>>>().unwrap(),
552            None
553        );
554
555        // MultiVersionedKeyQuery all (unbounded), ascending.
556        let all = q
557            .iq2_execute(&Iq2Query::MultiVersionedKey {
558                key: Box::new("k".to_string()),
559                from_ts: None,
560                to_ts: None,
561                descending: false,
562            })
563            .await
564            .unwrap();
565        assert_eq!(
566            *all.downcast::<Vec<VersionedRecord<i64>>>().unwrap(),
567            vec![
568                VersionedRecord {
569                    value: 10,
570                    valid_from: 100,
571                    valid_to: Some(200)
572                },
573                VersionedRecord {
574                    value: 20,
575                    valid_from: 200,
576                    valid_to: Some(300)
577                },
578                VersionedRecord {
579                    value: 30,
580                    valid_from: 300,
581                    valid_to: None
582                },
583            ]
584        );
585
586        // MultiVersionedKeyQuery [150,250] → versions overlapping that range
587        // (10@[100,200) and 20@[200,300); 30@[300,∞) excluded), descending.
588        let win = q
589            .iq2_execute(&Iq2Query::MultiVersionedKey {
590                key: Box::new("k".to_string()),
591                from_ts: Some(150),
592                to_ts: Some(250),
593                descending: true,
594            })
595            .await
596            .unwrap();
597        assert_eq!(
598            *win.downcast::<Vec<VersionedRecord<i64>>>().unwrap(),
599            vec![
600                VersionedRecord {
601                    value: 20,
602                    valid_from: 200,
603                    valid_to: Some(300)
604                },
605                VersionedRecord {
606                    value: 10,
607                    valid_from: 100,
608                    valid_to: Some(200)
609                },
610            ]
611        );
612
613        // Wrong key type → KeyTypeMismatch.
614        let bad = q
615            .iq2_execute(&Iq2Query::VersionedKey {
616                key: Box::new(7_i64),
617                as_of: None,
618            })
619            .await;
620        assert_eq!(bad.err(), Some(Iq2Failure::KeyTypeMismatch));
621    }
622
623    #[tokio::test]
624    async fn changelog_roundtrip_restores_chain() {
625        let mut s = store(secs(1_000));
626        s.put("k".into(), Some(10), 100).await;
627        s.delete(&"k".into(), 150).await;
628        s.put("k".into(), Some(30), 300).await;
629        let cl = s.take_changelog_ts();
630        let mut r = store(secs(1_000));
631        r.set_logging(false);
632        for (k, v, ts) in cl {
633            r.apply_changelog_ts(k, v, ts.unwrap()).await;
634        }
635        assert!(r.take_changelog().is_empty());
636        assert_eq!(r.get(&"k".into()).await.map(|x| x.value), Some(30));
637        assert_eq!(
638            r.get_as_of(&"k".into(), 120).await.map(|x| x.value),
639            Some(10)
640        );
641        assert_eq!(r.get_as_of(&"k".into(), 160).await, None);
642    }
643}