1use std::{
5 collections::{BTreeMap, BTreeSet, HashMap},
6 fmt::{self, Debug, Formatter},
7 hash::Hash,
8 marker::PhantomData,
9};
10
11use reifydb_value::{Result, reifydb_assertions, value::row_number::RowNumber};
12use serde::{Deserialize, Serialize, de::DeserializeOwned};
13
14use crate::{
15 encoded::key::{EncodedKey, IntoEncodedKey},
16 window::{
17 accumulator::WindowAccumulator,
18 engine::{AccumulatorEvent, GroupMeta, LatePolicy, MetaKey, meta_key_for, rolling::RollingBuckets},
19 span::Slot,
20 state::StateCache,
21 store::WindowStore,
22 },
23};
24
25pub type MultiRollingBuffer<C, Accumulator> = BTreeMap<C, Accumulator>;
26
27pub type MultiRollingEmit<SK, Output> = BTreeMap<SK, Output>;
28
29pub enum MultiEmit<Output> {
30 Insert {
31 row_number: RowNumber,
32 value: Output,
33 },
34 Update {
35 row_number: RowNumber,
36 prior: Output,
37 value: Output,
38 },
39 Remove {
40 row_number: RowNumber,
41 value: Output,
42 },
43}
44
45#[derive(Serialize, Deserialize)]
46#[serde(bound(
47 serialize = "C: Serialize + Ord, Accumulator: Serialize, SK: Serialize + Ord, Output: Serialize",
48 deserialize = "C: serde::de::DeserializeOwned + Ord, Accumulator: serde::de::DeserializeOwned, \
49 SK: serde::de::DeserializeOwned + Ord, Output: serde::de::DeserializeOwned"
50))]
51struct GroupState<C, Accumulator, SK, Output> {
52 buffer: MultiRollingBuffer<C, Accumulator>,
53 last_emit: MultiRollingEmit<SK, Output>,
54}
55
56impl<C: Ord, Accumulator, SK: Ord, Output> Default for GroupState<C, Accumulator, SK, Output> {
57 fn default() -> Self {
58 Self {
59 buffer: BTreeMap::new(),
60 last_emit: BTreeMap::new(),
61 }
62 }
63}
64
65impl<C: Ord + Clone, Accumulator: Clone, SK: Ord + Clone, Output: Clone> Clone
66 for GroupState<C, Accumulator, SK, Output>
67{
68 fn clone(&self) -> Self {
69 Self {
70 buffer: self.buffer.clone(),
71 last_emit: self.last_emit.clone(),
72 }
73 }
74}
75
76impl<C, Accumulator, SK, Output> Debug for GroupState<C, Accumulator, SK, Output> {
77 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
78 f.debug_struct("GroupState")
79 .field("buffer_len", &self.buffer.len())
80 .field("last_emit_len", &self.last_emit.len())
81 .finish()
82 }
83}
84
85type MetaLoaded<G, C> = HashMap<G, GroupMeta<C>>;
86type StateRows<G> = HashMap<G, RowNumber>;
87
88struct GroupSlot<C, Accumulator, SK, Output> {
89 state_row_number: RowNumber,
90 buffer: MultiRollingBuffer<C, Accumulator>,
91 prior_emit: MultiRollingEmit<SK, Output>,
92 buffer_changed: bool,
93}
94
95pub struct MultiRollingEngine<G, C, Accumulator, SK, Output> {
96 groups: StateCache<RowNumber, GroupState<C, Accumulator, SK, Output>>,
97 meta: StateCache<MetaKey, GroupMeta<C>>,
98 late_policy: LatePolicy,
99 _pd: PhantomData<G>,
100}
101
102impl<G, C, Accumulator, SK, Output> Default for MultiRollingEngine<G, C, Accumulator, SK, Output>
103where
104 G: Clone + Eq + Ord + Hash + Debug + Serialize + DeserializeOwned,
105 C: Slot + Hash + Serialize + DeserializeOwned,
106 Accumulator: WindowAccumulator,
107 SK: Clone + Eq + Ord + Hash + Debug + Serialize + DeserializeOwned,
108 Output: Clone + Debug + PartialEq + Serialize + DeserializeOwned,
109 for<'a> &'a G: IntoEncodedKey,
110{
111 fn default() -> Self {
112 Self::new()
113 }
114}
115
116impl<G, C, Accumulator, SK, Output> MultiRollingEngine<G, C, Accumulator, SK, Output>
117where
118 G: Clone + Eq + Ord + Hash + Debug + Serialize + DeserializeOwned,
119 C: Slot + Hash + Serialize + DeserializeOwned,
120 Accumulator: WindowAccumulator,
121 SK: Clone + Eq + Ord + Hash + Debug + Serialize + DeserializeOwned,
122 Output: Clone + Debug + PartialEq + Serialize + DeserializeOwned,
123 for<'a> &'a G: IntoEncodedKey,
124{
125 pub fn new() -> Self {
126 Self::with_late_policy(LatePolicy::Drop)
127 }
128
129 pub fn with_late_policy(late_policy: LatePolicy) -> Self {
130 Self {
131 groups: StateCache::<RowNumber, GroupState<C, Accumulator, SK, Output>>::new(8),
132 meta: StateCache::<MetaKey, GroupMeta<C>>::new_internal(64),
133 late_policy,
134 _pd: PhantomData,
135 }
136 }
137
138 pub fn apply<S, SKF, RKF, CB>(
139 &mut self,
140 store: &mut S,
141 buckets: RollingBuckets<G, C, Accumulator::Contribution>,
142 capacity: usize,
143 state_key: SKF,
144 row_key: RKF,
145 combine: CB,
146 ) -> Result<Vec<MultiEmit<Output>>>
147 where
148 S: WindowStore,
149 SKF: Fn(&G) -> EncodedKey,
150 RKF: Fn(&G, &SK) -> EncodedKey,
151 CB: Fn(&G, &MultiRollingBuffer<C, Accumulator>) -> MultiRollingEmit<SK, Output>,
152 {
153 if buckets.is_empty() {
154 return Ok(Vec::new());
155 }
156 let mut meta_loaded = self.warm_and_load_meta(store, &buckets)?;
157 let state_rows = self.resolve_state_rows(store, &buckets, &meta_loaded, &state_key)?;
158 let group_slots = self.apply_events_into_buffers(
159 store,
160 buckets,
161 &mut meta_loaded,
162 &state_rows,
163 &state_key,
164 capacity,
165 )?;
166 let emits = self.diff_emits(store, group_slots, &row_key, &combine)?;
167 self.persist_meta(store, meta_loaded)?;
168 Ok(emits)
169 }
170
171 pub fn flush<S: WindowStore>(&mut self, store: &mut S) -> Result<()> {
172 self.groups.flush(store)?;
173 self.meta.flush(store)?;
174 Ok(())
175 }
176
177 fn warm_and_load_meta<S: WindowStore>(
178 &mut self,
179 store: &mut S,
180 buckets: &RollingBuckets<G, C, Accumulator::Contribution>,
181 ) -> Result<MetaLoaded<G, C>> {
182 let meta_keys: Vec<MetaKey> = buckets
183 .keys()
184 .map(|(group, _)| group)
185 .collect::<BTreeSet<_>>()
186 .into_iter()
187 .map(meta_key_for)
188 .collect();
189 self.meta.warm(store, &meta_keys)?;
190
191 let mut meta_loaded: MetaLoaded<G, C> = HashMap::new();
192 for (group, _) in buckets.keys() {
193 if !meta_loaded.contains_key(group) {
194 let m = self.meta.get(store, &meta_key_for(group))?.unwrap_or_default();
195 meta_loaded.insert(group.clone(), m);
196 }
197 }
198 Ok(meta_loaded)
199 }
200
201 fn resolve_state_rows<S, SKF>(
202 &mut self,
203 store: &mut S,
204 buckets: &RollingBuckets<G, C, Accumulator::Contribution>,
205 meta_loaded: &MetaLoaded<G, C>,
206 state_key: &SKF,
207 ) -> Result<StateRows<G>>
208 where
209 S: WindowStore,
210 SKF: Fn(&G) -> EncodedKey,
211 {
212 let mut state_rows: StateRows<G> = HashMap::new();
213 let mut resolve_order: Vec<G> = Vec::new();
214 let mut state_lookup_keys: Vec<EncodedKey> = Vec::new();
215 let mut seen: BTreeSet<G> = BTreeSet::new();
216 for (group, coord) in buckets.keys() {
217 let initial_high_water = meta_loaded.get(group).and_then(|m| m.high_water);
218 if initial_high_water.is_none_or(|hw| *coord >= hw) && seen.insert(group.clone()) {
219 resolve_order.push(group.clone());
220 state_lookup_keys.push(state_key(group));
221 }
222 }
223 let resolved_rows = store.get_or_create_row_numbers(&state_lookup_keys)?;
224 reifydb_assertions! {
225 let resolved = resolved_rows.len();
226 let requested = state_lookup_keys.len();
227 assert!(
228 resolved == requested,
229 "get_or_create_row_numbers returned {resolved} rows for {requested} group keys; \
230 the zip below pairs resolve_order with resolved_rows by position, so a length \
231 mismatch would silently leave some groups without a state_rows entry and route \
232 them through the per-bucket get_or_create_row_number fallback, diverging behaviour"
233 );
234 }
235 let state_keys: Vec<RowNumber> = resolved_rows.iter().map(|(rn, _)| *rn).collect();
236 for (group, (state_row_number, _)) in resolve_order.into_iter().zip(resolved_rows) {
237 state_rows.insert(group, state_row_number);
238 }
239 self.groups.warm(store, &state_keys)?;
240 Ok(state_rows)
241 }
242
243 fn apply_events_into_buffers<S, SKF>(
244 &mut self,
245 store: &mut S,
246 buckets: RollingBuckets<G, C, Accumulator::Contribution>,
247 meta_loaded: &mut MetaLoaded<G, C>,
248 state_rows: &StateRows<G>,
249 state_key: &SKF,
250 capacity: usize,
251 ) -> Result<BTreeMap<G, GroupSlot<C, Accumulator, SK, Output>>>
252 where
253 S: WindowStore,
254 SKF: Fn(&G) -> EncodedKey,
255 {
256 let mut group_slots: BTreeMap<G, GroupSlot<C, Accumulator, SK, Output>> = BTreeMap::new();
257
258 for ((group, coord), events) in buckets {
259 let meta = meta_loaded.entry(group.clone()).or_default();
260
261 let slot = match group_slots.get_mut(&group) {
262 Some(s) => s,
263 None => {
264 let state_row_number = match state_rows.get(&group) {
265 Some(&rn) => rn,
266 None => {
267 let key = state_key(&group);
268 let (rn, _is_new) = store.get_or_create_row_number(&key)?;
269 rn
270 }
271 };
272 let GroupState {
273 buffer,
274 last_emit: prior_emit,
275 } = self.groups.get(store, &state_row_number)?.unwrap_or_default();
276 group_slots.insert(
277 group.clone(),
278 GroupSlot {
279 state_row_number,
280 buffer,
281 prior_emit,
282 buffer_changed: false,
283 },
284 );
285 group_slots.get_mut(&group).expect("just inserted")
286 }
287 };
288
289 let late = matches!(meta.high_water, Some(hw) if coord < hw)
290 && matches!(self.late_policy, LatePolicy::Drop)
291 && !slot.buffer.contains_key(&coord);
292
293 let mut accumulator = slot.buffer.remove(&coord).unwrap_or_default();
294 let mut touched = false;
295 for event in events {
296 match event {
297 AccumulatorEvent::Add(c) => {
298 if late {
299 continue;
300 }
301 accumulator.add(&c);
302 touched = true;
303 }
304 AccumulatorEvent::Remove(c) => {
305 if accumulator.is_empty() {
306 continue;
307 }
308 accumulator.remove(&c);
309 touched = true;
310 }
311 }
312 }
313 if !accumulator.is_empty() {
314 slot.buffer.insert(coord, accumulator);
315 }
316 if !touched {
317 continue;
318 }
319 while slot.buffer.len() > capacity {
320 slot.buffer.pop_first();
321 }
322 slot.buffer_changed = true;
323
324 let next_high_water = match meta.high_water {
325 Some(hw) if hw > coord => hw,
326 _ => coord,
327 };
328 reifydb_assertions! {
329 assert!(
330 next_high_water >= coord,
331 "high_water regressed below the window coord it just admitted, so the next batch would \
332 treat an already-processed window as late and silently drop its events (coord={coord:?}, \
333 prev_high_water={prev:?}, next_high_water={next_high_water:?})",
334 prev = meta.high_water
335 );
336 if let Some(prev) = meta.high_water {
337 assert!(
338 next_high_water >= prev,
339 "high_water moved backwards across an admit, breaking the monotonic late-event \
340 cutoff that buried-window dropping relies on (coord={coord:?}, prev_high_water={prev:?}, \
341 next_high_water={next_high_water:?})"
342 );
343 }
344 }
345 meta.high_water = Some(next_high_water);
346 }
347
348 Ok(group_slots)
349 }
350
351 fn diff_emits<S, RKF, CB>(
352 &mut self,
353 store: &mut S,
354 group_slots: BTreeMap<G, GroupSlot<C, Accumulator, SK, Output>>,
355 row_key: &RKF,
356 combine: &CB,
357 ) -> Result<Vec<MultiEmit<Output>>>
358 where
359 S: WindowStore,
360 RKF: Fn(&G, &SK) -> EncodedKey,
361 CB: Fn(&G, &MultiRollingBuffer<C, Accumulator>) -> MultiRollingEmit<SK, Output>,
362 {
363 let mut emits: Vec<MultiEmit<Output>> = Vec::new();
364
365 for (group, slot) in group_slots {
366 if !slot.buffer_changed {
367 continue;
368 }
369 let new_emit = combine(&group, &slot.buffer);
370
371 for (sk, new_out) in &new_emit {
372 let key = row_key(&group, sk);
373 let (rn, _is_new_alloc) = store.get_or_create_row_number(&key)?;
374 match slot.prior_emit.get(sk) {
375 Some(prior_out) => {
376 if prior_out != new_out {
377 emits.push(MultiEmit::Update {
378 row_number: rn,
379 prior: prior_out.clone(),
380 value: new_out.clone(),
381 });
382 }
383 }
384 None => {
385 emits.push(MultiEmit::Insert {
386 row_number: rn,
387 value: new_out.clone(),
388 });
389 }
390 }
391 }
392 for (sk, prior_out) in &slot.prior_emit {
393 if !new_emit.contains_key(sk) {
394 let key = row_key(&group, sk);
395 let (rn, _is_new_alloc) = store.get_or_create_row_number(&key)?;
396 emits.push(MultiEmit::Remove {
397 row_number: rn,
398 value: prior_out.clone(),
399 });
400 }
401 }
402
403 let combined = GroupState {
404 buffer: slot.buffer,
405 last_emit: new_emit,
406 };
407 self.groups.put(store, &slot.state_row_number, combined)?;
408 }
409
410 Ok(emits)
411 }
412
413 fn persist_meta<S: WindowStore>(&mut self, store: &mut S, meta_loaded: MetaLoaded<G, C>) -> Result<()> {
414 for (group, meta) in meta_loaded {
415 self.meta.set(store, &meta_key_for(&group), &meta)?;
416 }
417 Ok(())
418 }
419}