sim_lib_stream_host/
store_evict.rs1use std::collections::{BTreeMap, VecDeque};
4
5use sim_kernel::{Error, Expr, Result, Symbol};
6
7#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub struct StoreKey(Symbol);
10
11impl StoreKey {
12 pub fn new(symbol: Symbol) -> Self {
14 Self(symbol)
15 }
16
17 pub fn named(name: impl Into<String>) -> Self {
19 Self(Symbol::qualified("stream/content", name.into()))
20 }
21
22 pub fn as_symbol(&self) -> &Symbol {
24 &self.0
25 }
26}
27
28#[derive(Clone, Debug, PartialEq)]
30pub struct ContentFrame {
31 key: StoreKey,
32 session: Symbol,
33 receipt_seq: u64,
34 inserted_tick: u64,
35 size_bytes: usize,
36 value: Expr,
37}
38
39impl ContentFrame {
40 pub fn new(
42 key: StoreKey,
43 session: Symbol,
44 receipt_seq: u64,
45 inserted_tick: u64,
46 size_bytes: usize,
47 value: Expr,
48 ) -> Self {
49 Self {
50 key,
51 session,
52 receipt_seq,
53 inserted_tick,
54 size_bytes,
55 value,
56 }
57 }
58
59 pub fn key(&self) -> &StoreKey {
61 &self.key
62 }
63
64 pub fn session(&self) -> &Symbol {
66 &self.session
67 }
68
69 pub fn receipt_seq(&self) -> u64 {
71 self.receipt_seq
72 }
73
74 pub fn inserted_tick(&self) -> u64 {
76 self.inserted_tick
77 }
78
79 pub fn size_bytes(&self) -> usize {
81 self.size_bytes
82 }
83
84 pub fn value(&self) -> &Expr {
86 &self.value
87 }
88}
89
90#[derive(Clone, Debug, PartialEq, Eq)]
92pub struct RetentionWindow {
93 pub session: Symbol,
95 pub receipt_seq: u64,
97 pub retain_ms: u64,
99}
100
101impl RetentionWindow {
102 pub fn new(session: Symbol, receipt_seq: u64, retain_ms: u64) -> Self {
104 Self {
105 session,
106 receipt_seq,
107 retain_ms,
108 }
109 }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq)]
114pub struct StoreEvicted {
115 pub key: StoreKey,
117 pub reason: Symbol,
119}
120
121#[derive(Clone, Debug, PartialEq)]
123pub struct BoundedContentStore {
124 max_bytes: usize,
125 used_bytes: usize,
126 entries: BTreeMap<StoreKey, ContentFrame>,
127 order: VecDeque<StoreKey>,
128}
129
130impl BoundedContentStore {
131 pub fn new(max_bytes: usize) -> Result<Self> {
133 if max_bytes == 0 {
134 return Err(Error::Eval(
135 "content store bound must be greater than zero".to_owned(),
136 ));
137 }
138 Ok(Self {
139 max_bytes,
140 used_bytes: 0,
141 entries: BTreeMap::new(),
142 order: VecDeque::new(),
143 })
144 }
145
146 pub fn insert(&mut self, frame: ContentFrame) -> Result<Vec<StoreEvicted>> {
148 if frame.size_bytes > self.max_bytes {
149 return Err(Error::Eval(format!(
150 "content frame {} bytes exceeds store bound {}",
151 frame.size_bytes, self.max_bytes
152 )));
153 }
154 let mut evicted = Vec::new();
155 if self.remove_existing(&frame.key).is_some() {
156 evicted.push(StoreEvicted {
157 key: frame.key.clone(),
158 reason: replaced_reason(),
159 });
160 }
161 while self.used_bytes.saturating_add(frame.size_bytes) > self.max_bytes {
162 if let Some(item) = self.evict_oldest(size_bound_reason()) {
163 evicted.push(item);
164 } else {
165 break;
166 }
167 }
168 self.used_bytes = self.used_bytes.saturating_add(frame.size_bytes);
169 self.order.push_back(frame.key.clone());
170 self.entries.insert(frame.key.clone(), frame);
171 Ok(evicted)
172 }
173
174 pub fn sweep_retention(
176 &mut self,
177 now_tick: u64,
178 adapt_hz: u16,
179 windows: &[RetentionWindow],
180 ) -> Vec<StoreEvicted> {
181 let expired: Vec<StoreKey> = self
182 .entries
183 .values()
184 .filter(|frame| {
185 windows
186 .iter()
187 .find(|window| {
188 window.session == frame.session && window.receipt_seq == frame.receipt_seq
189 })
190 .is_none_or(|window| {
191 elapsed_ms(now_tick, frame.inserted_tick, adapt_hz) > window.retain_ms
192 })
193 })
194 .map(|frame| frame.key.clone())
195 .collect();
196 expired
197 .into_iter()
198 .filter_map(|key| {
199 self.remove_existing(&key).map(|_| StoreEvicted {
200 key,
201 reason: retention_reason(),
202 })
203 })
204 .collect()
205 }
206
207 pub fn contains(&self, key: &StoreKey) -> bool {
209 self.entries.contains_key(key)
210 }
211
212 pub fn used_bytes(&self) -> usize {
214 self.used_bytes
215 }
216
217 pub fn max_bytes(&self) -> usize {
219 self.max_bytes
220 }
221
222 pub fn len(&self) -> usize {
224 self.entries.len()
225 }
226
227 pub fn is_empty(&self) -> bool {
229 self.entries.is_empty()
230 }
231
232 fn evict_oldest(&mut self, reason: Symbol) -> Option<StoreEvicted> {
233 while let Some(key) = self.order.pop_front() {
234 if self.remove_existing(&key).is_some() {
235 return Some(StoreEvicted { key, reason });
236 }
237 }
238 None
239 }
240
241 fn remove_existing(&mut self, key: &StoreKey) -> Option<ContentFrame> {
242 let frame = self.entries.remove(key)?;
243 self.used_bytes = self.used_bytes.saturating_sub(frame.size_bytes);
244 self.order.retain(|existing| existing != key);
245 Some(frame)
246 }
247}
248
249pub fn size_bound_reason() -> Symbol {
251 Symbol::qualified("stream/content-evict", "size-bound")
252}
253
254pub fn retention_reason() -> Symbol {
256 Symbol::qualified("stream/content-evict", "retention")
257}
258
259fn replaced_reason() -> Symbol {
260 Symbol::qualified("stream/content-evict", "replaced")
261}
262
263fn elapsed_ms(now_tick: u64, then_tick: u64, adapt_hz: u16) -> u64 {
264 let elapsed_ticks = now_tick.saturating_sub(then_tick);
265 elapsed_ticks.saturating_mul(1000) / u64::from(adapt_hz.max(1))
266}