Skip to main content

kaspa_database/
item.rs

1use crate::{
2    db::DB,
3    errors::StoreError,
4    prelude::{DbSetAccess, ReadLock},
5};
6
7use super::prelude::{DbKey, DbWriter};
8use parking_lot::RwLock;
9use serde::{Serialize, de::DeserializeOwned};
10use std::{
11    collections::{HashSet, hash_map::RandomState},
12    hash::BuildHasher,
13    marker::PhantomData,
14    sync::Arc,
15};
16
17/// A cached DB item with concurrency support.
18///
19/// # Versioned items
20///
21/// `CachedDbItem` supports an opt-in "version-suffix" mode used by items
22/// whose on-disk value layout changed across a hardfork (currently
23/// `DbVirtualStateStore` across Toccata). A version-aware item is
24/// constructed with [`CachedDbItem::new_with_version_suffix`].
25///
26/// The store keeps two precomputed physical keys:
27/// - `live_key` — where the current-format row is read and written.
28/// - `legacy_key` — where a legacy-format row, if any, lives. `None` for
29///   non-versioned items.
30///
31/// On reads, the live key is probed first; on miss, the legacy key is
32/// probed and decoded through a caller-provided `TLegacy: Into<T>` shadow
33/// type. A successful legacy fallback eagerly migrates: the converted
34/// bytes are written under `live_key` and `legacy_key` is deleted, so the
35/// next read short-circuits on the live path and no dangling pre-fork
36/// bytes linger on disk. `remove` clears both layouts.
37///
38/// Dispatch is on physical-key presence, not on bincode decode outcome,
39/// so the version-aware path is robust to silent decode "successes"
40/// against mismatched bytes.
41///
42/// The `TLegacy` generic defaults to `T`, so existing callers that spell
43/// only the single type parameter continue to compile unchanged: `T:
44/// Into<T>` is supplied by the blanket `impl<U> From<U> for U`.
45pub struct CachedDbItem<T, TLegacy = T> {
46    db: Arc<DB>,
47    /// The physical DB key used by every read and write of the current
48    /// row. For a non-versioned item this is just the caller's prefix
49    /// bytes. For a versioned item it is `[caller_prefix || current_suffix]`.
50    live_key: Vec<u8>,
51    /// The physical DB key probed on a read miss. `Some` iff this item is
52    /// version-aware. The unversioned form `[caller_prefix]` and the
53    /// previously-versioned form `[caller_prefix || legacy_suffix]` are
54    /// both expressible here — see [`Self::new_with_version_suffix`].
55    legacy_key: Option<Vec<u8>>,
56    cached_item: Arc<RwLock<Option<T>>>,
57    _legacy: PhantomData<fn() -> TLegacy>,
58}
59
60// Manual `Clone` so the impl does NOT require `TLegacy: Clone` (the legacy
61// type is only used at decode time and never stored). `#[derive(Clone)]`
62// would conservatively bound every generic.
63impl<T, TLegacy> Clone for CachedDbItem<T, TLegacy> {
64    fn clone(&self) -> Self {
65        Self {
66            db: self.db.clone(),
67            live_key: self.live_key.clone(),
68            legacy_key: self.legacy_key.clone(),
69            cached_item: self.cached_item.clone(),
70            _legacy: PhantomData,
71        }
72    }
73}
74
75impl<T, TLegacy> CachedDbItem<T, TLegacy> {
76    pub fn new(db: Arc<DB>, key: Vec<u8>) -> Self {
77        Self { db, live_key: key, legacy_key: None, cached_item: Arc::new(RwLock::new(None)), _legacy: PhantomData }
78    }
79
80    /// Constructs a version-aware item.
81    ///
82    /// Writes go to `[key_prefix || current_suffix]`. Reads probe that
83    /// key first and, on miss, fall back to the legacy row decoded through
84    /// `TLegacy` and converted via `TLegacy: Into<T>`. The legacy row's
85    /// physical key is:
86    /// - `[key_prefix]` when `legacy_suffix = None` (pre-versioning,
87    ///   unversioned on-disk layout — the Toccata case);
88    /// - `[key_prefix || s]` when `legacy_suffix = Some(s)` (a previously
89    ///   versioned layout — placeholder for future migrations).
90    ///
91    /// A successful legacy fallback triggers an eager migration: the
92    /// converted `T` is written under the live key and the legacy row is
93    /// deleted. `remove` clears both layouts.
94    pub fn new_with_version_suffix(db: Arc<DB>, key_prefix: Vec<u8>, current_suffix: u8, legacy_suffix: Option<u8>) -> Self {
95        let mut live_key = key_prefix.clone();
96        live_key.push(current_suffix);
97        let legacy_key = match legacy_suffix {
98            None => Some(key_prefix),
99            Some(s) => {
100                let mut k = key_prefix;
101                k.push(s);
102                Some(k)
103            }
104        };
105        Self { db, live_key, legacy_key, cached_item: Arc::new(RwLock::new(None)), _legacy: PhantomData }
106    }
107
108    pub fn read(&self) -> Result<T, StoreError>
109    where
110        T: Clone + Serialize + DeserializeOwned,
111        TLegacy: DeserializeOwned + Into<T>,
112    {
113        if let Some(item) = self.cached_item.read().clone() {
114            return Ok(item);
115        }
116        if let Some(slice) = self.db.get_pinned(&self.live_key)? {
117            let item: T = bincode::deserialize(&slice)?;
118            *self.cached_item.write() = Some(item.clone());
119            return Ok(item);
120        }
121        // No live row. Fall through to the legacy probe if version-aware,
122        // else this is just KeyNotFound on the single physical key.
123        if let Some(legacy_key) = self.legacy_key.as_deref()
124            && let Some(slice) = self.db.get_pinned(legacy_key)?
125        {
126            let legacy_value: TLegacy = bincode::deserialize(&slice)?;
127            let item: T = legacy_value.into();
128            // Migrate: write under the live key and clear the legacy row.
129            let bin_data = bincode::serialize(&item)?;
130            self.db.put(&self.live_key, bin_data)?;
131            self.db.delete(legacy_key)?;
132            *self.cached_item.write() = Some(item.clone());
133            return Ok(item);
134        }
135        Err(StoreError::KeyNotFound(DbKey::prefix_only(&self.live_key)))
136    }
137
138    pub fn write(&mut self, mut writer: impl DbWriter, item: &T) -> Result<(), StoreError>
139    where
140        T: Clone + Serialize,
141    {
142        *self.cached_item.write() = Some(item.clone());
143        let bin_data = bincode::serialize(item)?;
144        writer.put(&self.live_key, bin_data)?;
145        Ok(())
146    }
147
148    pub fn remove(&mut self, mut writer: impl DbWriter) -> Result<(), StoreError>
149where {
150        *self.cached_item.write() = None;
151        writer.delete(&self.live_key)?;
152        if let Some(legacy_key) = self.legacy_key.as_deref() {
153            writer.delete(legacy_key)?;
154        }
155        Ok(())
156    }
157
158    pub fn update<F>(&mut self, mut writer: impl DbWriter, op: F) -> Result<T, StoreError>
159    where
160        T: Clone + Serialize + DeserializeOwned,
161        TLegacy: DeserializeOwned + Into<T>,
162        F: Fn(T) -> T,
163    {
164        let mut guard = self.cached_item.write();
165        let mut item = if let Some(item) = guard.take() {
166            item
167        } else if let Some(slice) = self.db.get_pinned(&self.live_key)? {
168            bincode::deserialize(&slice)?
169        } else if let Some(legacy_slice) = self.legacy_key.as_deref().and_then(|k| self.db.get_pinned(k).transpose()).transpose()? {
170            let legacy_value: TLegacy = bincode::deserialize(&legacy_slice)?;
171            legacy_value.into()
172        } else {
173            return Err(StoreError::KeyNotFound(DbKey::prefix_only(&self.live_key)));
174        };
175
176        item = op(item); // Apply the update op
177        *guard = Some(item.clone());
178        let bin_data = bincode::serialize(&item)?;
179        writer.put(&self.live_key, bin_data)?;
180        Ok(item)
181    }
182}
183
184#[derive(Clone, Copy, Hash, PartialEq, Eq)]
185struct EmptyKey;
186
187impl AsRef<[u8]> for EmptyKey {
188    fn as_ref(&self) -> &[u8] {
189        &[]
190    }
191}
192
193type LockedSet<T, S> = Arc<RwLock<HashSet<T, S>>>;
194
195#[derive(Clone)]
196pub struct CachedDbSetItem<T: Clone + Send + Sync, S = RandomState> {
197    access: DbSetAccess<EmptyKey, T>,
198    cached_set: Arc<RwLock<Option<LockedSet<T, S>>>>,
199}
200
201impl<T, S> CachedDbSetItem<T, S>
202where
203    T: Clone + std::hash::Hash + Eq + Send + Sync + DeserializeOwned + Serialize,
204    S: BuildHasher + Default,
205{
206    pub fn new(db: Arc<DB>, key: Vec<u8>) -> Self {
207        Self { access: DbSetAccess::new(db, key), cached_set: Arc::new(RwLock::new(None)) }
208    }
209
210    fn read_locked_set(&self) -> Result<LockedSet<T, S>, StoreError>
211    where
212        T: Clone + DeserializeOwned,
213    {
214        if let Some(item) = self.cached_set.read().clone() {
215            return Ok(item);
216        }
217        let set = self.access.bucket_iterator(EmptyKey).collect::<Result<HashSet<_, _>, _>>()?;
218        let set = Arc::new(RwLock::new(set));
219        self.cached_set.write().replace(set.clone());
220        Ok(set)
221    }
222
223    pub fn read(&self) -> Result<ReadLock<HashSet<T, S>>, StoreError>
224    where
225        T: Clone + DeserializeOwned,
226    {
227        Ok(ReadLock::new(self.read_locked_set()?))
228    }
229
230    pub fn update(
231        &mut self,
232        mut writer: impl DbWriter,
233        added_items: &[T],
234        removed_items: &[T],
235    ) -> Result<ReadLock<HashSet<T, S>>, StoreError>
236    where
237        T: Clone + Serialize,
238    {
239        let set = self.read_locked_set()?;
240        {
241            let mut set_write = set.write();
242            for item in removed_items.iter() {
243                self.access.delete(&mut writer, EmptyKey, item.clone())?;
244                set_write.remove(item);
245            }
246            for item in added_items.iter().cloned() {
247                self.access.write(&mut writer, EmptyKey, item.clone())?;
248                set_write.insert(item);
249            }
250        }
251        Ok(ReadLock::new(set))
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use crate::{
259        create_temp_db,
260        errors::StoreErrorPredicates,
261        prelude::{ConnBuilder, DirectDbWriter},
262    };
263    use serde::Deserialize;
264
265    /// Tests for the version-aware code path on `CachedDbItem`, mirroring
266    /// the `CachedDbAccess` versioned tests added in PR #956 for
267    /// `DbUtxoDiffsStore`. Each test uses a dedicated `Current` / `Legacy`
268    /// pair so the legacy decode path is exercised through a real
269    /// `TLegacy: Into<T>` conversion rather than the trivial `T -> T`
270    /// identity.
271    mod versioned {
272        use super::*;
273
274        /// Post-fork value type.
275        #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276        struct Current {
277            value: u64,
278            extra: Option<u32>,
279        }
280
281        /// Pre-fork value type — one field shorter than `Current`. Its
282        /// derived `Deserialize` consumes only the pre-fork byte layout.
283        #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
284        struct Legacy {
285            value: u64,
286        }
287
288        impl From<Legacy> for Current {
289            fn from(l: Legacy) -> Self {
290                Current { value: l.value, extra: None }
291            }
292        }
293
294        const ITEM_KEY: &[u8] = &[42];
295        const VERSION_SUFFIX: u8 = 1;
296
297        fn versioned_item(db: Arc<DB>) -> CachedDbItem<Current, Legacy> {
298            // `legacy_suffix = None` ⇒ the legacy row is the unversioned
299            // `[key]` layout (the Toccata case). The `Some(_)` form is
300            // exercised by `versioned_to_versioned_migration` below.
301            CachedDbItem::new_with_version_suffix(db, ITEM_KEY.to_vec(), VERSION_SUFFIX, None)
302        }
303
304        fn versioned_raw_key() -> Vec<u8> {
305            let mut k = ITEM_KEY.to_vec();
306            k.push(VERSION_SUFFIX);
307            k
308        }
309
310        #[test]
311        fn versioned_round_trip() {
312            let (_lifetime, db) = create_temp_db!(ConnBuilder::default().with_files_limit(10));
313            let mut item = versioned_item(db.clone());
314
315            let value = Current { value: 1_000_000, extra: Some(42) };
316            item.write(DirectDbWriter::new(&db), &value).unwrap();
317
318            // On-disk bytes must live under the versioned key, NOT the legacy key.
319            assert!(db.get_pinned(versioned_raw_key()).unwrap().is_some());
320            assert!(db.get_pinned(ITEM_KEY).unwrap().is_none());
321
322            // Round trip — fresh item (no cache) to force a DB hit.
323            let fresh = versioned_item(db.clone());
324            assert_eq!(fresh.read().unwrap(), value);
325        }
326
327        #[test]
328        fn versioned_read_migrates_legacy_row() {
329            let (_lifetime, db) = create_temp_db!(ConnBuilder::default().with_files_limit(10));
330            let item = versioned_item(db.clone());
331
332            let legacy = Legacy { value: 99 };
333            db.put(ITEM_KEY, bincode::serialize(&legacy).unwrap()).unwrap();
334
335            // Reading through the version-aware item finds the legacy row at
336            // `[key]`, decodes via `Legacy`, converts into `Current`, and
337            // migrates the on-disk layout to the versioned key.
338            let current = item.read().unwrap();
339            assert_eq!(current, Current { value: 99, extra: None });
340
341            // Post-read: the legacy row is gone and the versioned row holds
342            // the converted bytes.
343            assert!(db.get_pinned(ITEM_KEY).unwrap().is_none(), "legacy row must be deleted after migration");
344            let migrated: Current = {
345                let migrated_bytes = db.get_pinned(versioned_raw_key()).unwrap().expect("versioned row must exist after migration");
346                bincode::deserialize(&migrated_bytes).unwrap()
347            };
348            assert_eq!(migrated, Current { value: 99, extra: None });
349
350            // A fresh item (no cache) sees the migrated row directly.
351            let fresh = versioned_item(db);
352            assert_eq!(fresh.read().unwrap(), Current { value: 99, extra: None });
353        }
354
355        #[test]
356        fn versioned_prefers_current_over_legacy_when_both_present() {
357            // Cannot arise through normal usage (write never touches the legacy
358            // key), but the read path must still deterministically return the
359            // current row when an upgrade-time legacy row coexists.
360            let (_lifetime, db) = create_temp_db!(ConnBuilder::default().with_files_limit(10));
361            let mut item = versioned_item(db.clone());
362
363            db.put(ITEM_KEY, bincode::serialize(&Legacy { value: 7 }).unwrap()).unwrap();
364            item.write(DirectDbWriter::new(&db), &Current { value: 7, extra: Some(1) }).unwrap();
365
366            let fresh = versioned_item(db.clone());
367            assert_eq!(fresh.read().unwrap(), Current { value: 7, extra: Some(1) });
368            // The current row is preferred, so the legacy row is NOT touched
369            // — it lingers until the next `remove`. (Crash-recovery scenario.)
370            assert!(db.get_pinned(ITEM_KEY).unwrap().is_some());
371        }
372
373        #[test]
374        fn versioned_remove_clears_both_layouts() {
375            let (_lifetime, db) = create_temp_db!(ConnBuilder::default().with_files_limit(10));
376            let mut item = versioned_item(db.clone());
377
378            // Plant both a legacy row AND a current row for the same logical key.
379            db.put(ITEM_KEY, bincode::serialize(&Legacy { value: 7 }).unwrap()).unwrap();
380            item.write(DirectDbWriter::new(&db), &Current { value: 7, extra: Some(1) }).unwrap();
381
382            item.remove(DirectDbWriter::new(&db)).unwrap();
383
384            assert!(db.get_pinned(ITEM_KEY).unwrap().is_none());
385            assert!(db.get_pinned(versioned_raw_key()).unwrap().is_none());
386            assert!(item.read().unwrap_err().is_key_not_found());
387        }
388
389        #[test]
390        fn versioned_read_from_missing_key_is_not_found() {
391            let (_lifetime, db) = create_temp_db!(ConnBuilder::default().with_files_limit(10));
392            let item = versioned_item(db);
393            assert!(item.read().unwrap_err().is_key_not_found());
394        }
395
396        /// Exercises the `legacy_suffix = Some(_)` branch — the future
397        /// "vN → vN+1" rotation. Plants a row under `[key || PREV]`,
398        /// reads through an item configured for `current = NEXT, legacy =
399        /// Some(PREV)`, and asserts the migration rewrites the row under
400        /// `[key || NEXT]` and clears `[key || PREV]`.
401        #[test]
402        fn versioned_to_versioned_migration() {
403            const PREV: u8 = 1;
404            const NEXT: u8 = 2;
405            let (_lifetime, db) = create_temp_db!(ConnBuilder::default().with_files_limit(10));
406
407            let item: CachedDbItem<Current, Legacy> =
408                CachedDbItem::new_with_version_suffix(db.clone(), ITEM_KEY.to_vec(), NEXT, Some(PREV));
409
410            let mut prev_key = ITEM_KEY.to_vec();
411            prev_key.push(PREV);
412            let mut next_key = ITEM_KEY.to_vec();
413            next_key.push(NEXT);
414
415            db.put(&prev_key, bincode::serialize(&Legacy { value: 314 }).unwrap()).unwrap();
416
417            let current = item.read().unwrap();
418            assert_eq!(current, Current { value: 314, extra: None });
419
420            assert!(db.get_pinned(&prev_key).unwrap().is_none(), "previous-version row must be deleted");
421            assert!(db.get_pinned(&next_key).unwrap().is_some(), "next-version row must exist");
422            // The unversioned `[key]` slot was never touched — only the
423            // configured legacy key is migrated, not all older variants.
424            assert!(db.get_pinned(ITEM_KEY).unwrap().is_none());
425        }
426
427        #[test]
428        fn non_versioned_default_unchanged() {
429            // Sanity: a default `CachedDbItem::new` keeps the unversioned key
430            // layout — the version-aware extension is opt-in only.
431            let (_lifetime, db) = create_temp_db!(ConnBuilder::default().with_files_limit(10));
432            let mut item: CachedDbItem<Current> = CachedDbItem::new(db.clone(), ITEM_KEY.to_vec());
433            let value = Current { value: 5, extra: None };
434            item.write(DirectDbWriter::new(&db), &value).unwrap();
435            assert!(db.get_pinned(ITEM_KEY).unwrap().is_some());
436            assert!(db.get_pinned(versioned_raw_key()).unwrap().is_none());
437        }
438    }
439}