1use std::{
16 any::Any,
17 collections::{BTreeMap, HashMap},
18};
19
20use async_trait::async_trait;
21use bytes::{BufMut, Bytes};
22use krabka_units::prelude::*;
23
24use crate::{
25 dsl::processors::change::Change,
26 processor::serde::Serde,
27 store::{
28 api::StateStore,
29 snapshot::{SnapshotReader, put_sized, store_payload},
30 suppress_bufval::{
31 SuppressRecordCtx, deserialize_buffer_change, serialize_buffer_change,
32 try_deserialize_buffer_change,
33 },
34 },
35};
36
37#[async_trait]
46pub(crate) trait SuppressStore<K: Send + Sync, V: Send>: StateStore {
47 async fn put(&mut self, key: K, buffer_time: i64, change: Change<V>, ctx: SuppressRecordCtx);
52 async fn evict_while(&mut self, threshold: i64) -> Vec<(K, Change<V>, i64)>;
58 async fn evict_oldest(&mut self) -> Option<(K, Change<V>, i64)>;
63 fn len(&self) -> usize;
64 #[allow(dead_code)]
69 fn is_empty(&self) -> bool;
70 fn byte_size(&self) -> ByteSize;
74}
75
76struct Entry {
83 key_bytes: Bytes,
84 new_bytes: Option<Bytes>,
85 old_bytes: Option<Bytes>,
86 record_ts: i64,
87}
88
89fn entry_size(key_bytes: &Bytes, new_bytes: Option<&Bytes>) -> usize {
91 key_bytes.len() + new_bytes.map_or(0, Bytes::len)
92}
93
94pub struct SuppressBytesStore<K, V> {
99 name: String,
100 changelog_topic: String,
101 logging: bool,
102 key_serde: Box<dyn Serde<K>>,
103 value_serde: Box<dyn Serde<V>>,
104 entries: BTreeMap<(i64, u64), Entry>,
106 index: HashMap<Bytes, (i64, u64)>,
108 seq: u64,
109 byte_size: usize,
110 changelog: Vec<(Bytes, Option<Bytes>)>,
111}
112
113impl<K: 'static, V: 'static> SuppressBytesStore<K, V> {
114 #[must_use]
115 pub(crate) fn new(
116 name: String,
117 key_serde: Box<dyn Serde<K>>,
118 value_serde: Box<dyn Serde<V>>,
119 changelog_topic: String,
120 ) -> Self {
121 Self {
122 name,
123 changelog_topic,
124 logging: true,
125 key_serde,
126 value_serde,
127 entries: BTreeMap::new(),
128 index: HashMap::new(),
129 seq: 0,
130 byte_size: 0,
131 changelog: Vec::new(),
132 }
133 }
134
135 #[must_use]
140 pub fn in_memory(
141 name: String,
142 key_serde: Box<dyn Serde<K>>,
143 value_serde: Box<dyn Serde<V>>,
144 changelog_topic: String,
145 ) -> Self {
146 Self::new(name, key_serde, value_serde, changelog_topic)
147 }
148
149 fn remove_existing(&mut self, key_bytes: &Bytes) -> Option<Bytes> {
155 let slot = self.index.remove(key_bytes)?;
156 let entry = self.entries.remove(&slot).expect("indexed slot present");
157 self.byte_size -= entry_size(&entry.key_bytes, entry.new_bytes.as_ref());
158 entry.new_bytes
159 }
160
161 fn insert_slot(&mut self, buffer_time: i64, entry: Entry) {
163 let slot = (buffer_time, self.seq);
164 self.seq += 1;
165 self.index.insert(entry.key_bytes.clone(), slot);
166 self.byte_size += entry_size(&entry.key_bytes, entry.new_bytes.as_ref());
167 self.entries.insert(slot, entry);
168 }
169
170 fn pop_slot(&mut self, slot: (i64, u64)) -> (K, Change<V>, i64) {
176 let entry = self.entries.remove(&slot).expect("slot present");
177 self.index.remove(&entry.key_bytes);
178 self.byte_size -= entry_size(&entry.key_bytes, entry.new_bytes.as_ref());
179 if self.logging {
180 self.changelog.push((entry.key_bytes.clone(), None));
181 }
182 let key = self
183 .key_serde
184 .deserialize(&self.changelog_topic, &entry.key_bytes)
185 .expect("suppress key deserialize");
186 let change = Change {
187 old: entry.old_bytes.map(|b| {
188 self.value_serde
189 .deserialize(&self.changelog_topic, &b)
190 .expect("old value deserialize")
191 }),
192 new: entry.new_bytes.map(|b| {
193 self.value_serde
194 .deserialize(&self.changelog_topic, &b)
195 .expect("new value deserialize")
196 }),
197 };
198 (key, change, entry.record_ts)
199 }
200}
201
202#[async_trait]
203impl<K: 'static, V: 'static> StateStore for SuppressBytesStore<K, V> {
204 fn name(&self) -> &str {
205 &self.name
206 }
207 async fn flush(&mut self) {}
208 fn close(&mut self) {}
209 fn as_any_mut(&mut self) -> &mut dyn Any {
210 self
211 }
212 fn changelog_topic(&self) -> &str {
213 &self.changelog_topic
214 }
215 fn take_changelog(&mut self) -> Vec<(Bytes, Option<Bytes>)> {
216 std::mem::take(&mut self.changelog)
217 }
218 async fn apply_changelog(&mut self, key: Bytes, value: Option<Bytes>) {
219 match value {
221 Some(v) => {
222 let d = deserialize_buffer_change(&v);
223 self.remove_existing(&key);
226 let entry = Entry {
227 key_bytes: key,
228 new_bytes: d.new.map(Bytes::from),
229 old_bytes: d.old.map(Bytes::from),
230 record_ts: d.ctx.timestamp,
231 };
232 self.insert_slot(d.buffer_time, entry);
233 }
234 None => {
235 self.remove_existing(&key);
236 }
237 }
238 }
239 fn set_logging(&mut self, on: bool) {
240 self.logging = on;
241 }
242 async fn clear(&mut self) {
243 self.entries.clear();
246 self.index.clear();
247 self.seq = 0;
248 self.byte_size = 0;
249 self.changelog.clear();
250 }
251 async fn snapshot(&mut self) -> Bytes {
252 let mut buffer = store_payload();
258 buffer.put_u64(self.seq);
259 buffer.put_u64(self.byte_size as u64);
260 buffer.put_u32(u32::try_from(self.entries.len()).expect("entry count fits u32"));
261 for ((buffer_time, seq), entry) in &self.entries {
262 buffer.put_i64(*buffer_time);
263 buffer.put_u64(*seq);
264 put_sized(&mut buffer, &entry.key_bytes);
265 let ctx = SuppressRecordCtx {
266 topic: self.changelog_topic.clone(),
267 partition: 0,
268 offset: 0,
269 timestamp: entry.record_ts,
270 };
271 let value = serialize_buffer_change(
272 &ctx,
273 None,
274 entry.old_bytes.as_deref(),
275 entry.new_bytes.as_deref(),
276 *buffer_time,
277 );
278 put_sized(&mut buffer, &value);
279 }
280 buffer.freeze()
281 }
282 async fn restore_snapshot(
283 &mut self,
284 data: Bytes,
285 ) -> Result<(), crate::error::StreamsClientError> {
286 let mut reader = SnapshotReader::new(&data);
287 reader.store_version()?;
288 let seq = reader.u64()?;
289 let byte_size = usize::try_from(reader.u64()?).map_err(|_error| {
290 crate::error::StreamsClientError::Snapshot(
291 "suppress buffer byte size does not fit this platform".to_owned(),
292 )
293 })?;
294 let count = reader.u32()?;
295 let mut entries = BTreeMap::new();
296 let mut index = HashMap::new();
297 for _ in 0..count {
298 let buffer_time = reader.i64()?;
299 let slot = (buffer_time, reader.u64()?);
300 let key_bytes = reader.sized()?;
301 let value = reader.sized()?;
302 let decoded = try_deserialize_buffer_change(&value).ok_or_else(|| {
303 crate::error::StreamsClientError::Snapshot(
304 "malformed suppress buffer value in snapshot".to_owned(),
305 )
306 })?;
307 index.insert(key_bytes.clone(), slot);
308 entries.insert(
309 slot,
310 Entry {
311 key_bytes,
312 new_bytes: decoded.new.map(Bytes::from),
313 old_bytes: decoded.old.map(Bytes::from),
314 record_ts: decoded.ctx.timestamp,
315 },
316 );
317 }
318 reader.finish()?;
319 self.entries = entries;
320 self.index = index;
321 self.seq = seq;
322 self.byte_size = byte_size;
323 self.changelog.clear();
324 Ok(())
325 }
326}
327
328#[async_trait]
329impl<K: Send + Sync + 'static, V: Send + 'static> SuppressStore<K, V> for SuppressBytesStore<K, V> {
330 async fn put(&mut self, key: K, buffer_time: i64, change: Change<V>, ctx: SuppressRecordCtx) {
331 let kb = self.key_serde.serialize(&self.changelog_topic, &key);
332 let new_bytes = change
333 .new
334 .as_ref()
335 .map(|v| self.value_serde.serialize(&self.changelog_topic, v));
336 let old_bytes = change
337 .old
338 .as_ref()
339 .map(|v| self.value_serde.serialize(&self.changelog_topic, v));
340 let existing_new = self.remove_existing(&kb);
345 let prior_bytes = existing_new.or_else(|| old_bytes.clone());
346
347 let entry = Entry {
348 key_bytes: kb.clone(),
349 new_bytes: new_bytes.clone(),
350 old_bytes: old_bytes.clone(),
351 record_ts: ctx.timestamp,
352 };
353 self.insert_slot(buffer_time, entry);
354
355 if self.logging {
356 let value = serialize_buffer_change(
357 &ctx,
358 prior_bytes.as_deref(),
359 old_bytes.as_deref(),
360 new_bytes.as_deref(),
361 buffer_time,
362 );
363 self.changelog.push((kb, Some(value)));
364 }
365 }
366
367 async fn evict_while(&mut self, threshold: i64) -> Vec<(K, Change<V>, i64)> {
368 let mut out = Vec::new();
369 while let Some((&slot, _)) = self.entries.iter().next() {
370 if slot.0 > threshold {
371 break;
372 }
373 out.push(self.pop_slot(slot));
374 }
375 out
376 }
377
378 async fn evict_oldest(&mut self) -> Option<(K, Change<V>, i64)> {
379 let (&slot, _) = self.entries.iter().next()?;
380 Some(self.pop_slot(slot))
381 }
382
383 fn len(&self) -> usize {
384 self.entries.len()
385 }
386
387 fn is_empty(&self) -> bool {
388 self.entries.is_empty()
389 }
390
391 fn byte_size(&self) -> ByteSize {
392 ByteSize::from_bytes(self.byte_size.try_into().unwrap_or(u64::MAX))
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use assert2::check;
401
402 use super::*;
403 use crate::processor::serde::{I64Serde, StringSerde};
404
405 fn store() -> SuppressBytesStore<String, i64> {
406 SuppressBytesStore::<String, i64>::in_memory(
407 "sup".into(),
408 Box::new(StringSerde),
409 Box::new(I64Serde),
410 "app-sup-changelog".into(),
411 )
412 }
413
414 fn ctx(timestamp: i64) -> SuppressRecordCtx {
415 SuppressRecordCtx {
416 topic: "in".into(),
417 partition: 0,
418 offset: 0,
419 timestamp,
420 }
421 }
422
423 #[tokio::test]
429 async fn uses_statestore_defaults_for_cache_and_iq() {
430 use std::sync::{Arc, Mutex};
431
432 use crate::store::cache::named::NamedCache;
433 let mut s = store();
434
435 assert!(s.as_iq().is_none());
437 assert!(!s.is_cached_erased());
438 let cache = Arc::new(Mutex::new(NamedCache::new("sup".into())));
439 assert!(
440 !s.enable_cache_erased(cache),
441 "suppress store is not cache-aware"
442 );
443 assert!(
444 !s.is_cached_erased(),
445 "still not cached after the no-op enable"
446 );
447
448 s.put("a".into(), 30, Change::update(None, 1), ctx(30))
450 .await;
451 let mut buffer = std::collections::VecDeque::new();
452 s.flush_cache_into(&mut buffer, &[0]).await;
453 assert!(buffer.is_empty(), "no record cache → no forwarded change");
454
455 let cl_ts = s.take_changelog_ts();
457 assert_eq!(cl_ts.len(), 1);
458 assert!(cl_ts[0].2.is_none(), "default timestamp is None");
459 assert!(s.take_changelog().is_empty());
461
462 s.set_record_context(crate::processor::record::RecordContext {
464 topic: "t".into(),
465 partition: 0,
466 offset: 0,
467 timestamp: 0,
468 });
469 }
470
471 #[tokio::test]
472 async fn put_then_evict_while_in_buffer_time_order() {
473 let mut s = store();
474 s.put("a".into(), 30, Change::update(None, 1), ctx(30))
475 .await;
476 s.put("b".into(), 10, Change::update(None, 2), ctx(10))
477 .await;
478 check!(s.len() == 2);
479 check!(!s.is_empty());
480 check!(s.byte_size() > ByteSize::ZERO);
481 let out = s.evict_while(30).await;
483 check!(out.len() == 2);
484 check!(out[0].0 == "b" && out[0].1.new == Some(2) && out[0].2 == 10);
485 check!(out[1].0 == "a" && out[1].1.new == Some(1) && out[1].2 == 30);
486 check!(s.len() == 0);
487 check!(s.is_empty());
488 check!(s.byte_size() == ByteSize::ZERO);
489 }
490
491 #[tokio::test]
492 async fn replace_by_key_keeps_one_entry_with_latest_change() {
493 let mut s = store();
494 s.put("k".into(), 10, Change::update(None, 1), ctx(10))
495 .await;
496 s.put("k".into(), 10, Change::update(Some(1), 2), ctx(12))
497 .await;
498 check!(s.len() == 1);
499 let out = s.evict_while(10).await;
500 check!(out.len() == 1);
501 check!(out[0].1.new == Some(2));
502 check!(out[0].1.old == Some(1));
503 }
504
505 #[tokio::test]
506 async fn evict_oldest_pops_lowest_buffer_time_then_none() {
507 let mut s = store();
508 s.put("a".into(), 30, Change::update(None, 1), ctx(30))
509 .await;
510 s.put("b".into(), 10, Change::update(None, 2), ctx(10))
511 .await;
512 let first = s.evict_oldest().await.unwrap();
513 check!(first.0 == "b" && first.1.new == Some(2));
514 let second = s.evict_oldest().await.unwrap();
515 check!(second.0 == "a" && second.1.new == Some(1));
516 check!(s.evict_oldest().await.is_none());
517 }
518
519 #[tokio::test]
520 async fn changelog_records_puts_then_tombstones_on_evict() {
521 let mut s = store();
522 s.put("a".into(), 10, Change::update(None, 1), ctx(10))
523 .await;
524 s.put("b".into(), 20, Change::update(None, 2), ctx(20))
525 .await;
526 let cl = s.take_changelog();
527 check!(cl.len() == 2);
528 check!(cl.iter().all(|(_, v)| v.is_some()));
529 let _ = s.evict_while(20).await;
531 let cl = s.take_changelog();
532 check!(cl.len() == 2);
533 check!(cl.iter().all(|(_, v)| v.is_none()));
534 check!(cl[0].0.as_ref() == b"a");
535 check!(cl[1].0.as_ref() == b"b");
536 }
537
538 #[tokio::test]
539 async fn apply_changelog_round_trips_then_tombstone_removes() {
540 let mut src = store();
542 src.put("k".into(), 42, Change::update(Some(7), 9), ctx(42))
543 .await;
544 let cl = src.take_changelog();
545 check!(cl.len() == 1);
546 let (key, value) = cl.into_iter().next().unwrap();
547
548 let mut dst = store();
550 dst.apply_changelog(key.clone(), value).await;
551 check!(dst.len() == 1);
552 check!(dst.take_changelog().is_empty()); let out = dst.evict_while(42).await;
554 check!(out.len() == 1);
555 check!(out[0].0 == "k");
556 check!(out[0].1.new == Some(9));
557 check!(out[0].1.old == Some(7));
558 check!(out[0].2 == 42);
559
560 dst.apply_changelog(
562 key.clone(),
563 Some(serialize_buffer_change(
564 &ctx(42),
565 None,
566 Some(&7i64.to_be_bytes()),
567 Some(&9i64.to_be_bytes()),
568 42,
569 )),
570 )
571 .await;
572 check!(dst.len() == 1);
573 dst.apply_changelog(key, None).await;
574 check!(dst.is_empty());
575 }
576
577 #[tokio::test]
578 async fn logging_off_suppresses_changelog() {
579 let mut s = store();
580 s.set_logging(false);
581 s.put("k".into(), 10, Change::update(None, 1), ctx(10))
582 .await;
583 check!(s.take_changelog().is_empty());
584 let _ = s.evict_while(10).await;
585 check!(s.take_changelog().is_empty());
586 }
587
588 #[tokio::test]
596 async fn windowed_buffer_restores_and_emits_on_close() {
597 use crate::dsl::windows::{TimeWindowedSerde, Window, Windowed};
598
599 fn win_store() -> SuppressBytesStore<Windowed<String>, i64> {
600 SuppressBytesStore::<Windowed<String>, i64>::in_memory(
601 "sup".into(),
602 Box::new(TimeWindowedSerde::new(StringSerde, millis(10))),
603 Box::new(I64Serde),
604 "app-sup-changelog".into(),
605 )
606 }
607 fn wk(key: &str, start: i64) -> Windowed<String> {
608 Windowed {
609 key: key.into(),
610 window: Window {
611 start,
612 end: start + 10,
613 },
614 }
615 }
616
617 let mut src = win_store();
619 src.put(wk("a", 0), 10, Change::update(None, 2), ctx(5))
620 .await;
621 src.put(wk("b", 20), 30, Change::update(None, 7), ctx(25))
622 .await;
623 let changelog = src.take_changelog();
624 check!(changelog.len() == 2);
625
626 let mut restored = win_store();
628 for (key, value) in changelog {
629 restored.apply_changelog(key, value).await;
630 }
631 check!(restored.len() == 2);
632 check!(restored.take_changelog().is_empty());
633
634 let closed = restored.evict_while(10).await;
637 check!(closed.len() == 1);
638 check!(closed[0].0 == wk("a", 0));
639 check!(closed[0].1.new == Some(2));
640 check!(restored.len() == 1);
641 let rest = restored.evict_while(30).await;
643 check!(rest.len() == 1);
644 check!(rest[0].0 == wk("b", 20));
645 check!(rest[0].1.new == Some(7));
646 }
647}