bb_runtime/framework/hold_table.rs
1//! `HoldTable` - slot-named value buffer for `Hold.Stash` /
2//! `Hold.Flush` syscalls.
3//!
4//! bounded by max-distinct-slot-names with a drop
5//! counter, so a producer that mints arbitrary slot names cannot
6//! grow the table unbounded. When the table is at capacity and an
7//! attempt to stash a fresh slot name arrives, the new entry is
8//! dropped and the drop counter ticks. Existing slots' values
9//! can always be overwritten (stash on an existing key never
10//! evicts).
11
12use std::collections::HashMap;
13
14/// Named-slot value buffer with bounded distinct-slot count.
15pub struct HoldTable {
16 slots: HashMap<String, Vec<u8>>,
17 max_slots: usize,
18 drops: u64,
19}
20
21impl Default for HoldTable {
22 fn default() -> Self {
23 Self {
24 slots: HashMap::new(),
25 max_slots: Self::DEFAULT_MAX_SLOTS,
26 drops: 0,
27 }
28 }
29}
30
31impl HoldTable {
32 /// Default cap on distinct slot names. 1024 covers any
33 /// reasonable user workload while bounding the worst-case
34 /// memory footprint of an unknown-key flood.
35 pub const DEFAULT_MAX_SLOTS: usize = 1024;
36
37 /// Construct a fresh empty table with the default cap.
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 /// Construct with a custom max distinct-slot cap.
43 pub fn with_max_slots(max_slots: usize) -> Self {
44 Self {
45 slots: HashMap::new(),
46 max_slots,
47 drops: 0,
48 }
49 }
50
51 /// Stash bytes into the named slot. Overwrites any previous
52 /// value (existing slot names ALWAYS replace cleanly). New
53 /// slot names that would exceed the table's configured cap are dropped
54 /// and the drop counter ticks.
55 pub fn stash(&mut self, slot: &str, bytes: Vec<u8>) {
56 if self.slots.contains_key(slot) || self.slots.len() < self.max_slots {
57 self.slots.insert(slot.to_string(), bytes);
58 } else {
59 self.drops += 1;
60 }
61 }
62
63 /// Take the stashed value from the named slot, if present.
64 pub fn flush(&mut self, slot: &str) -> Option<Vec<u8>> {
65 self.slots.remove(slot)
66 }
67
68 /// Cumulative drop count for new-slot-on-full attempts.
69 pub fn drops(&self) -> u64 {
70 self.drops
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 #[test]
78 fn stash_flush_roundtrip() {
79 let mut t = HoldTable::new();
80 t.stash("a", vec![1, 2, 3]);
81 assert_eq!(t.flush("a"), Some(vec![1, 2, 3]));
82 assert_eq!(t.flush("a"), None);
83 }
84
85 #[test]
86 fn stash_overflow_drops_new_slot_and_ticks_counter() {
87 let mut t = HoldTable::with_max_slots(2);
88 t.stash("a", vec![1]);
89 t.stash("b", vec![2]);
90 assert_eq!(t.drops(), 0);
91 // Third distinct slot at cap: dropped.
92 t.stash("c", vec![3]);
93 assert_eq!(t.drops(), 1);
94 assert_eq!(t.flush("c"), None);
95 // Existing slot overwrite still works at cap.
96 t.stash("a", vec![9]);
97 assert_eq!(t.flush("a"), Some(vec![9]));
98 }
99}