1use std::{
5 collections::{BTreeMap, BTreeSet, HashMap},
6 fmt::Debug,
7 hash::Hash,
8 marker::PhantomData,
9};
10
11use reifydb_codec::key::encoded::{EncodedKey, IntoEncodedKey};
12use reifydb_value::{Result, reifydb_assertions, value::row_number::RowNumber};
13use serde::{Serialize, de::DeserializeOwned};
14
15use crate::{
16 key::flow_node_internal_state::FlowNodeInternalStateKey,
17 window::{
18 accumulator::WindowAccumulator,
19 engine::{
20 AccumulatorEvent, EmitKind, GroupMeta, LatePolicy, MetaKey,
21 config::WindowEngineConfig,
22 meta_key_for,
23 rolling::{RollingBuckets, RollingBuffer, RollingResult},
24 },
25 span::Slot,
26 state::StateCache,
27 store::WindowStore,
28 },
29};
30
31#[derive(Clone, Copy, Hash, PartialEq, Eq)]
32struct RunningKey(RowNumber);
33
34impl IntoEncodedKey for &RunningKey {
35 fn into_encoded_key(self) -> EncodedKey {
36 let inner = (&self.0).into_encoded_key();
37 let inner = inner.as_ref();
38 let mut bytes = Vec::with_capacity(1 + inner.len());
39 bytes.push(FlowNodeInternalStateKey::WINDOW_RUNNING_TAG);
40 bytes.extend_from_slice(inner);
41 EncodedKey::new(bytes)
42 }
43}
44
45type MetaLoaded<G, C> = HashMap<G, GroupMeta<C>>;
46type BufferRows<G> = HashMap<G, (RowNumber, bool)>;
47
48struct GroupSlot<C, Accumulator, Running, Output> {
49 row_number: RowNumber,
50 is_new: bool,
51 buffer: RollingBuffer<C, Accumulator>,
52 running: Running,
53 was_empty_before: bool,
54 buffer_changed: bool,
55 prior_output: Option<Output>,
56}
57
58pub struct RollingIncrementalEngine<G, C, Accumulator, Running> {
59 buffers: StateCache<RowNumber, RollingBuffer<C, Accumulator>>,
60 running: StateCache<RunningKey, Running>,
61 meta: StateCache<MetaKey, GroupMeta<C>>,
62 late_policy: LatePolicy,
63 _pd: PhantomData<G>,
64}
65
66impl<G, C, Accumulator, Running> RollingIncrementalEngine<G, C, Accumulator, Running>
67where
68 G: Clone + Eq + Ord + Hash + Debug + Serialize + DeserializeOwned,
69 C: Slot + Hash + Serialize + DeserializeOwned,
70 Accumulator: WindowAccumulator,
71 Running: WindowAccumulator,
72 for<'a> &'a G: IntoEncodedKey,
73{
74 pub fn new(config: WindowEngineConfig) -> Self {
75 Self {
76 buffers: StateCache::<RowNumber, RollingBuffer<C, Accumulator>>::new(
77 config.state_cache_capacity(),
78 ),
79 running: StateCache::<RunningKey, Running>::new(config.state_cache_capacity()),
80 meta: StateCache::<MetaKey, GroupMeta<C>>::new_internal(config.internal_state_cache_capacity()),
81 late_policy: config.late_policy(),
82 _pd: PhantomData,
83 }
84 }
85
86 pub fn apply<S, K, WC, CR, Output>(
87 &mut self,
88 store: &mut S,
89 buckets: RollingBuckets<G, C, Accumulator::Contribution>,
90 capacity: usize,
91 row_key: K,
92 window_contribution: WC,
93 combine_running: CR,
94 ) -> Result<Vec<RollingResult<G, Output>>>
95 where
96 S: WindowStore,
97 K: Fn(&G) -> EncodedKey,
98 WC: Fn(&Accumulator::Output) -> Running::Contribution,
99 CR: Fn(&G, &Running, &Accumulator::Output, C) -> Option<Output>,
100 {
101 if buckets.is_empty() {
102 return Ok(Vec::new());
103 }
104 let mut meta_loaded = self.warm_and_load_meta(store, &buckets)?;
105 let buffer_rows = self.resolve_buffer_rows(store, &buckets, &meta_loaded, &row_key)?;
106
107 let late_policy = self.late_policy;
108 let mut group_slots: BTreeMap<G, GroupSlot<C, Accumulator, Running, Output>> = BTreeMap::new();
109
110 for ((group, coord), events) in buckets {
111 let meta = meta_loaded.entry(group.clone()).or_default();
112
113 let slot = match group_slots.get_mut(&group) {
114 Some(s) => s,
115 None => {
116 let (row_number, is_new) = match buffer_rows.get(&group) {
117 Some(&resolved) => resolved,
118 None => {
119 let key = row_key(&group);
120 store.get_or_create_row_number(&key)?
121 }
122 };
123 let buffer: RollingBuffer<C, Accumulator> =
124 self.buffers.get(store, &row_number)?.unwrap_or_default();
125 let running: Running =
126 self.running.get(store, &RunningKey(row_number))?.unwrap_or_default();
127 let was_empty_before = buffer.is_empty();
128 let prior_output = match buffer.iter().next_back() {
129 Some((coord, accumulator)) => {
130 accumulator.finalize().and_then(|newest| {
131 combine_running(&group, &running, &newest, *coord)
132 })
133 }
134 None => None,
135 };
136 group_slots.insert(
137 group.clone(),
138 GroupSlot {
139 row_number,
140 is_new,
141 buffer,
142 running,
143 was_empty_before,
144 buffer_changed: false,
145 prior_output,
146 },
147 );
148 group_slots.get_mut(&group).expect("just inserted")
149 }
150 };
151
152 let late = matches!(meta.high_water, Some(hw) if coord < hw)
153 && matches!(late_policy, LatePolicy::Drop)
154 && !slot.buffer.contains_key(&coord);
155
156 let mut accumulator = slot.buffer.remove(&coord).unwrap_or_default();
157 let old_value = accumulator.finalize();
158 let mut touched = false;
159 for event in events {
160 match event {
161 AccumulatorEvent::Add(c) => {
162 if late {
163 continue;
164 }
165 accumulator.add(&c);
166 touched = true;
167 }
168 AccumulatorEvent::Remove(c) => {
169 if accumulator.is_empty() {
170 continue;
171 }
172 accumulator.remove(&c);
173 touched = true;
174 }
175 }
176 }
177 if !touched {
178 continue;
179 }
180 let new_value = accumulator.finalize();
181
182 if let Some(old) = &old_value {
183 slot.running.remove(&window_contribution(old));
184 }
185 if let Some(new) = &new_value {
186 slot.running.add(&window_contribution(new));
187 }
188
189 if !accumulator.is_empty() {
190 slot.buffer.insert(coord, accumulator);
191 }
192 while slot.buffer.len() > capacity {
193 if let Some((_, evicted)) = slot.buffer.pop_first()
194 && let Some(value) = evicted.finalize()
195 {
196 slot.running.remove(&window_contribution(&value));
197 }
198 }
199 slot.buffer_changed = true;
200
201 meta.high_water = Some(match meta.high_water {
202 Some(hw) if hw > coord => hw,
203 _ => coord,
204 });
205 }
206
207 let mut results: Vec<RollingResult<G, Output>> = Vec::new();
208 for (group, slot) in group_slots {
209 if !slot.buffer_changed {
210 continue;
211 }
212 let output = match slot.buffer.iter().next_back() {
213 Some((coord, accumulator)) => accumulator
214 .finalize()
215 .and_then(|newest| combine_running(&group, &slot.running, &newest, *coord)),
216 None => None,
217 };
218 self.buffers.put(store, &slot.row_number, slot.buffer)?;
219 self.running.put(store, &RunningKey(slot.row_number), slot.running)?;
220
221 if let Some(out) = output {
222 let kind = if slot.is_new || slot.was_empty_before {
223 EmitKind::Insert
224 } else {
225 EmitKind::Update
226 };
227 results.push(RollingResult {
228 row_number: slot.row_number,
229 group,
230 value: out,
231 prior: None,
232 kind,
233 });
234 } else if let Some(prior) = slot.prior_output {
235 results.push(RollingResult {
236 row_number: slot.row_number,
237 group,
238 value: prior,
239 prior: None,
240 kind: EmitKind::Remove,
241 });
242 }
243 }
244 self.persist_meta(store, meta_loaded)?;
245 Ok(results)
246 }
247
248 pub fn flush<S: WindowStore>(&mut self, store: &mut S) -> Result<()> {
249 self.buffers.flush(store)?;
250 self.running.flush(store)?;
251 self.meta.flush(store)?;
252 Ok(())
253 }
254
255 fn warm_and_load_meta<S: WindowStore>(
256 &mut self,
257 store: &mut S,
258 buckets: &RollingBuckets<G, C, Accumulator::Contribution>,
259 ) -> Result<MetaLoaded<G, C>> {
260 let meta_keys: Vec<MetaKey> = buckets
261 .keys()
262 .map(|(group, _)| group)
263 .collect::<BTreeSet<_>>()
264 .into_iter()
265 .map(meta_key_for)
266 .collect();
267 self.meta.warm(store, &meta_keys)?;
268
269 let mut meta_loaded: MetaLoaded<G, C> = HashMap::new();
270 for (group, _) in buckets.keys() {
271 if !meta_loaded.contains_key(group) {
272 let m = self.meta.get(store, &meta_key_for(group))?.unwrap_or_default();
273 meta_loaded.insert(group.clone(), m);
274 }
275 }
276 Ok(meta_loaded)
277 }
278
279 fn resolve_buffer_rows<S, K>(
280 &mut self,
281 store: &mut S,
282 buckets: &RollingBuckets<G, C, Accumulator::Contribution>,
283 meta_loaded: &MetaLoaded<G, C>,
284 row_key: &K,
285 ) -> Result<BufferRows<G>>
286 where
287 S: WindowStore,
288 K: Fn(&G) -> EncodedKey,
289 {
290 let mut buffer_rows: BufferRows<G> = HashMap::new();
291 let mut resolve_order: Vec<G> = Vec::new();
292 let mut group_keys: Vec<EncodedKey> = Vec::new();
293 let mut seen: BTreeSet<G> = BTreeSet::new();
294 for (group, coord) in buckets.keys() {
295 let initial_high_water = meta_loaded.get(group).and_then(|m| m.high_water);
296 if initial_high_water.is_none_or(|hw| *coord >= hw) && seen.insert(group.clone()) {
297 resolve_order.push(group.clone());
298 group_keys.push(row_key(group));
299 }
300 }
301 let resolved_rows = store.get_or_create_row_numbers(&group_keys)?;
302 reifydb_assertions! {
303 let resolved = resolved_rows.len();
304 let requested = group_keys.len();
305 assert!(
306 resolved == requested,
307 "get_or_create_row_numbers returned {resolved} rows for {requested} group keys; \
308 the zip below pairs resolve_order with resolved_rows by position, so a length \
309 mismatch would silently leave some groups without a buffer_rows entry and route \
310 them through the per-bucket get_or_create_row_number fallback, diverging behaviour"
311 );
312 }
313 let state_keys: Vec<RowNumber> = resolved_rows.iter().map(|(rn, _)| *rn).collect();
314 let running_keys: Vec<RunningKey> = state_keys.iter().map(|rn| RunningKey(*rn)).collect();
315 for (group, resolved) in resolve_order.into_iter().zip(resolved_rows) {
316 buffer_rows.insert(group, resolved);
317 }
318 self.buffers.warm(store, &state_keys)?;
319 self.running.warm(store, &running_keys)?;
320 Ok(buffer_rows)
321 }
322
323 fn persist_meta<S: WindowStore>(&mut self, store: &mut S, meta_loaded: MetaLoaded<G, C>) -> Result<()> {
324 for (group, meta) in meta_loaded {
325 self.meta.set(store, &meta_key_for(&group), &meta)?;
326 }
327 Ok(())
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use std::collections::BTreeMap;
334
335 use reifydb_codec::key::encoded::EncodedKey;
336
337 use crate::window::{
338 accumulator::WindowAccumulator,
339 engine::{
340 AccumulatorEvent, EmitKind,
341 config::WindowEngineConfig,
342 rolling::{RollingBuckets, RollingResult},
343 rolling_incremental::RollingIncrementalEngine,
344 test_support::{MockStore, SumAccumulator},
345 },
346 };
347
348 fn test_config() -> WindowEngineConfig {
349 WindowEngineConfig::builder().state_cache_capacity(8).internal_state_cache_capacity(64).build()
350 }
351
352 fn row_key(group: &u32) -> EncodedKey {
353 EncodedKey::builder().u32(*group).build()
354 }
355
356 fn running_sum(_group: &u32, running: &SumAccumulator, _newest: &i64, _coord: u64) -> Option<i64> {
357 running.finalize()
358 }
359
360 #[test]
361 fn buffer_survives_restart_without_running_collision() {
362 let mut store = MockStore::default();
373
374 let mut engine =
375 RollingIncrementalEngine::<u32, u64, SumAccumulator, SumAccumulator>::new(test_config());
376 let mut buckets: RollingBuckets<u32, u64, i64> = BTreeMap::new();
377 buckets.insert((1u32, 10u64), vec![AccumulatorEvent::Add(5)]);
378 let published: Vec<RollingResult<u32, i64>> =
379 engine.apply(&mut store, buckets, 4, row_key, |v: &i64| *v, running_sum).unwrap();
380 engine.flush(&mut store).unwrap();
381 assert_eq!(published.len(), 1);
382 assert!(matches!(published[0].kind, EmitKind::Insert));
383 assert_eq!(published[0].value, 5);
384
385 let mut engine =
388 RollingIncrementalEngine::<u32, u64, SumAccumulator, SumAccumulator>::new(test_config());
389 let mut buckets: RollingBuckets<u32, u64, i64> = BTreeMap::new();
390 buckets.insert((1u32, 10u64), vec![AccumulatorEvent::Remove(5)]);
391 let withdrawn: Vec<RollingResult<u32, i64>> =
392 engine.apply(&mut store, buckets, 4, row_key, |v: &i64| *v, running_sum).unwrap();
393 engine.flush(&mut store).unwrap();
394
395 assert_eq!(withdrawn.len(), 1, "emptying the group emits exactly one terminal diff");
396 assert!(
397 matches!(withdrawn[0].kind, EmitKind::Remove),
398 "the group emptied under retraction, so the last published row must be withdrawn"
399 );
400 assert_eq!(
401 withdrawn[0].value, 5,
402 "the withdrawn value is reconstructed from the persisted buffer plus running accumulator"
403 );
404 assert_eq!(
405 withdrawn[0].row_number, published[0].row_number,
406 "the withdrawal targets the same row that was published"
407 );
408 }
409
410 #[test]
411 fn buffer_survives_lru_eviction_without_running_collision() {
412 let mut store = MockStore::default();
419 let mut engine =
420 RollingIncrementalEngine::<u32, u64, SumAccumulator, SumAccumulator>::new(test_config());
421
422 let mut published_group_1: Vec<RollingResult<u32, i64>> = Vec::new();
423 for group in 1u32..=11u32 {
424 let mut buckets: RollingBuckets<u32, u64, i64> = BTreeMap::new();
425 buckets.insert((group, 10u64), vec![AccumulatorEvent::Add(i64::from(group))]);
426 let out: Vec<RollingResult<u32, i64>> =
427 engine.apply(&mut store, buckets, 4, row_key, |v: &i64| *v, running_sum).unwrap();
428 if group == 1 {
429 published_group_1 = out;
430 }
431 }
432 engine.flush(&mut store).unwrap();
433 assert_eq!(published_group_1.len(), 1);
434 assert!(matches!(published_group_1[0].kind, EmitKind::Insert));
435 assert_eq!(published_group_1[0].value, 1);
436
437 let mut buckets: RollingBuckets<u32, u64, i64> = BTreeMap::new();
440 buckets.insert((1u32, 10u64), vec![AccumulatorEvent::Remove(1)]);
441 let withdrawn: Vec<RollingResult<u32, i64>> =
442 engine.apply(&mut store, buckets, 4, row_key, |v: &i64| *v, running_sum).unwrap();
443 engine.flush(&mut store).unwrap();
444
445 assert_eq!(withdrawn.len(), 1, "emptying the evicted group emits exactly one terminal diff");
446 assert!(
447 matches!(withdrawn[0].kind, EmitKind::Remove),
448 "the evicted group emptied under retraction, so the last published row must be withdrawn"
449 );
450 assert_eq!(
451 withdrawn[0].value, 1,
452 "the withdrawn value is reconstructed from the evicted group's persisted buffer and running"
453 );
454 assert_eq!(
455 withdrawn[0].row_number, published_group_1[0].row_number,
456 "the withdrawal targets the same row that was published for group 1"
457 );
458 }
459}