1pub mod config;
5pub mod rolling;
6pub mod rolling_incremental;
7pub mod rolling_top_k;
8pub mod tumbling;
9pub mod tumbling_carry;
10
11use std::{collections::HashMap, ops::Bound};
12
13use reifydb_codec::{
14 key::{encode_u64_asc, encoded::EncodedKey},
15 row::operator::state::{OperatorState, StateCodec, decode, encode},
16};
17use reifydb_core::{
18 key::{
19 operator::{
20 keyspace::window::{Emit, WindowMeta, WindowMetaSuffix},
21 state::{GroupId, GroupStateKey, IntoGroupStateKey, KeyspaceId, OperatorStateKey},
22 },
23 typed::direction::{Asc, Desc},
24 },
25 metrics::heap::HeapSize,
26 state::{
27 timer::StateStore,
28 typed::{TypedStateStore, typed_key},
29 },
30};
31use reifydb_macro::operator_state;
32use reifydb_value::{
33 Result,
34 util::hash::{Hash128, xxh3_128},
35 value::row_number::RowNumber,
36};
37use tracing::{debug, instrument};
38
39use crate::{
40 operator::{
41 state::seal::coord::Coord,
42 state_access::{get_classified, remove, set},
43 },
44 window::span::{Slot, WindowSpan},
45};
46
47pub enum AccumulatorEvent<Contribution> {
48 Add(Contribution),
49 Remove(Contribution),
50}
51
52fn note_when_expiry_capped(expired: usize, expire_batch: usize) {
53 if expired >= expire_batch {
54 debug!(expired, expire_batch, "window expiry hit per-tick batch cap, backlog deferred to next tick");
55 }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum EmitKind {
60 Insert,
61 Update,
62 Remove,
63}
64
65pub struct WindowResult<G, Coord, Output> {
66 pub row_number: RowNumber,
67 pub group: G,
68 pub span: WindowSpan<Coord>,
69 pub value: Output,
70 pub prior: Option<Output>,
71 pub kind: EmitKind,
72}
73
74#[operator_state]
75#[derive(Debug, Clone)]
76pub struct GroupMeta<S> {
77 pub high_water: Option<S>,
78}
79
80impl<S> Default for GroupMeta<S> {
81 fn default() -> Self {
82 Self {
83 high_water: None,
84 }
85 }
86}
87
88impl<S> HeapSize for GroupMeta<S> {
89 fn heap_size(&self) -> usize {
90 0
91 }
92}
93
94pub(crate) trait MetaHighWater: OperatorState {
95 fn high_water_order(&self) -> Option<u64>;
96}
97
98impl<S: Slot> MetaHighWater for GroupMeta<S> {
99 fn high_water_order(&self) -> Option<u64> {
100 self.high_water.map(|hw| hw.order_key().to_order())
101 }
102}
103
104pub(crate) struct BatchMeta<S> {
105 pub(crate) initial: Option<S>,
106 pub(crate) bumped: Option<S>,
107}
108
109impl<S> Default for BatchMeta<S> {
110 fn default() -> Self {
111 Self {
112 initial: None,
113 bumped: None,
114 }
115 }
116}
117
118impl<S: Slot> BatchMeta<S> {
119 pub(crate) fn observe(&mut self, slot: S) {
120 match self.high_water() {
121 Some(hw) if slot > hw => self.bumped = Some(slot),
122 None => self.bumped = Some(slot),
123 _ => {}
124 }
125 }
126
127 pub(crate) fn high_water(&self) -> Option<S> {
128 self.bumped.or(self.initial)
129 }
130}
131
132pub(crate) fn load_batch_meta<S>(store: &mut dyn StateStore, key: &MetaKey) -> Result<BatchMeta<S>>
133where
134 S: Slot,
135{
136 let initial = get_classified::<_, GroupMeta<S>>(store, key)?.and_then(|meta| meta.high_water);
137 Ok(BatchMeta {
138 initial,
139 bumped: None,
140 })
141}
142
143pub(crate) fn persist_batch_meta<G, S>(store: &mut dyn StateStore, loaded: HashMap<G, BatchMeta<S>>) -> Result<()>
144where
145 G: StateCodec,
146 S: Slot,
147{
148 for (group, batch) in loaded {
149 let Some(bumped) = batch.bumped else {
150 continue;
151 };
152 set(
153 store,
154 &meta_key_for(group_hash(&group)?),
155 &GroupMeta {
156 high_water: Some(bumped),
157 },
158 )?;
159 }
160 Ok(())
161}
162
163const META_SWEEP_PAGE: usize = 1024;
164
165#[derive(Default)]
166pub(crate) struct MetaSweep {
167 low_water: Option<u64>,
168 cursor: Option<WindowMetaSuffix>,
169 surviving: Option<u64>,
170}
171
172impl MetaSweep {
173 #[instrument(name = "flow::window::sweep_stale_meta", level = "debug", skip_all)]
174 pub(crate) fn sweep<M>(&mut self, store: &mut dyn StateStore, threshold: u64) -> Result<usize>
175 where
176 M: MetaHighWater + Clone + OperatorState + HeapSize,
177 {
178 if self.cursor.is_none() && self.low_water.is_some_and(|lw| lw >= threshold) {
179 return Ok(0);
180 }
181 let cursor = self.cursor.take();
182 let page = store.state_scan_in::<WindowMeta>(
183 GroupId::ROOT,
184 match &cursor {
185 Some(key) => Bound::Excluded(key),
186 None => Bound::Unbounded,
187 },
188 Some(META_SWEEP_PAGE),
189 )?;
190 let visited = page.len();
191 let mut stale: Vec<MetaKey> = Vec::new();
192 let mut surviving = self.surviving;
193 let mut furthest: Option<WindowMetaSuffix> = None;
194 for (suffix, bytes) in page {
195 if let Some(hw) = decode::<M>(&bytes)?.high_water_order() {
196 if hw < threshold {
197 stale.push(MetaKey(suffix));
198 } else {
199 surviving = Some(surviving.map_or(hw, |m| m.min(hw)));
200 }
201 }
202 furthest = Some(suffix);
203 }
204 if visited < META_SWEEP_PAGE {
205 self.low_water = surviving;
206 self.surviving = None;
207 } else {
208 self.low_water = None;
209 self.surviving = surviving;
210 self.cursor = furthest;
211 }
212 let count = stale.len();
213 for key in &stale {
214 remove(store, key)?;
215 }
216 Ok(count)
217 }
218}
219
220#[derive(Clone, Hash, PartialEq, Eq)]
221pub struct MetaKey(pub WindowMetaSuffix);
222
223impl HeapSize for MetaKey {
224 fn heap_size(&self) -> usize {
225 self.0.heap_size()
226 }
227}
228
229#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
230pub enum KeyspaceFamily {
231 Host,
232 Guest,
233}
234
235impl KeyspaceFamily {
236 fn keyspace(&self, host: KeyspaceId, guest: KeyspaceId) -> KeyspaceId {
237 match self {
238 Self::Host => host,
239 Self::Guest => guest,
240 }
241 }
242
243 fn suffix(&self, slot: &EncodedKey) -> Vec<u8> {
244 match self {
245 Self::Host => slot.as_bytes().to_vec(),
246 Self::Guest => xxh3_128(slot.as_bytes()).0.to_be_bytes().to_vec(),
247 }
248 }
249}
250
251#[derive(Clone, Hash, PartialEq, Eq)]
252pub struct RunningKey {
253 pub family: KeyspaceFamily,
254 pub group: GroupId,
255 pub slot: EncodedKey,
256}
257
258impl RunningKey {
259 pub fn new(family: KeyspaceFamily, group: GroupId, slot: EncodedKey) -> Self {
260 Self {
261 family,
262 group,
263 slot,
264 }
265 }
266
267 pub fn of_row(family: KeyspaceFamily, group: GroupId, row: RowNumber) -> Self {
268 Self::new(family, group, EncodedKey::new(encode_u64_asc(row.0)))
269 }
270}
271
272impl HeapSize for RunningKey {
273 fn heap_size(&self) -> usize {
274 self.slot.heap_size()
275 }
276}
277
278impl IntoGroupStateKey for &RunningKey {
279 fn into_group_state_key(self) -> GroupStateKey {
280 OperatorStateKey::inner_encoded(
281 self.group,
282 self.family.keyspace(KeyspaceId::RUNNING, KeyspaceId::GUEST_RUNNING),
283 self.family.suffix(&self.slot),
284 )
285 }
286}
287
288#[derive(Clone, Hash, PartialEq, Eq)]
289pub struct WindowStateKey {
290 pub family: KeyspaceFamily,
291 pub group: GroupId,
292 pub slot: EncodedKey,
293}
294
295impl WindowStateKey {
296 pub fn new(family: KeyspaceFamily, group: GroupId, slot: EncodedKey) -> Self {
297 Self {
298 family,
299 group,
300 slot,
301 }
302 }
303
304 pub fn of_row(family: KeyspaceFamily, group: GroupId, row: RowNumber) -> Self {
305 Self::new(family, group, EncodedKey::new(encode_u64_asc(row.0)))
306 }
307}
308
309impl HeapSize for WindowStateKey {
310 fn heap_size(&self) -> usize {
311 self.slot.heap_size()
312 }
313}
314
315impl IntoGroupStateKey for &WindowStateKey {
316 fn into_group_state_key(self) -> GroupStateKey {
317 OperatorStateKey::inner_encoded(
318 self.group,
319 self.family.keyspace(KeyspaceId::ACCUMULATOR, KeyspaceId::GUEST_ACCUMULATOR),
320 self.family.suffix(&self.slot),
321 )
322 }
323}
324
325#[derive(Clone, Hash, PartialEq, Eq)]
326pub struct BufferKey {
327 pub family: KeyspaceFamily,
328 pub group: GroupId,
329 pub slot: EncodedKey,
330}
331
332impl BufferKey {
333 pub fn new(family: KeyspaceFamily, group: GroupId, slot: EncodedKey) -> Self {
334 Self {
335 family,
336 group,
337 slot,
338 }
339 }
340
341 pub fn of_row(family: KeyspaceFamily, group: GroupId, row: RowNumber) -> Self {
342 Self::new(family, group, EncodedKey::new(encode_u64_asc(row.0)))
343 }
344}
345
346impl HeapSize for BufferKey {
347 fn heap_size(&self) -> usize {
348 self.slot.heap_size()
349 }
350}
351
352impl IntoGroupStateKey for &BufferKey {
353 fn into_group_state_key(self) -> GroupStateKey {
354 OperatorStateKey::inner_encoded(
355 self.group,
356 self.family.keyspace(KeyspaceId::BUFFER, KeyspaceId::GUEST_BUFFER),
357 self.family.suffix(&self.slot),
358 )
359 }
360}
361
362#[derive(Clone, Copy, Hash, PartialEq, Eq)]
363pub struct EmitKey {
364 pub group: GroupId,
365 pub row: RowNumber,
366}
367
368impl EmitKey {
369 pub fn new(group: GroupId, row: RowNumber) -> Self {
370 Self {
371 group,
372 row,
373 }
374 }
375}
376
377impl HeapSize for EmitKey {
378 fn heap_size(&self) -> usize {
379 0
380 }
381}
382
383impl IntoGroupStateKey for &EmitKey {
384 fn into_group_state_key(self) -> GroupStateKey {
385 typed_key::<Emit>(self.group, &Asc(self.row))
386 }
387}
388
389impl IntoGroupStateKey for &MetaKey {
390 fn into_group_state_key(self) -> GroupStateKey {
391 typed_key::<WindowMeta>(GroupId::ROOT, &self.0)
392 }
393}
394
395pub(crate) fn group_hash<G: StateCodec>(group: &G) -> Result<Hash128> {
396 Ok(xxh3_128(encode(group)?.body()))
397}
398
399pub fn meta_key_for(group: Hash128) -> MetaKey {
400 MetaKey(WindowMetaSuffix {
401 window: Desc(group),
402 })
403}
404
405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
406pub enum ExpiryAnchor {
407 Unindexed,
408 WindowStart,
409 LastEvent,
410}
411
412impl ExpiryAnchor {
413 pub fn of(&self, window_start: u64, last_event: Option<u64>) -> Option<u64> {
414 match self {
415 ExpiryAnchor::Unindexed => None,
416 ExpiryAnchor::WindowStart => Some(window_start),
417 ExpiryAnchor::LastEvent => last_event,
418 }
419 }
420}
421
422#[cfg(test)]
423mod archived_projection_tests {
424 use reifydb_value::value::datetime::DateTime;
425
426 use super::*;
427
428 fn via_storage<M: MetaHighWater>(meta: &M) -> Option<u64> {
430 let bytes = meta.encode_state().unwrap();
431 decode::<M>(&bytes).unwrap().high_water_order()
432 }
433
434 #[test]
435 fn stored_high_water_yields_the_slot_order_key() {
436 let instant = DateTime::from_epoch_millis(1_700_000_000_123).unwrap();
438 let datetime_meta = GroupMeta {
439 high_water: Some(instant),
440 };
441 assert_eq!(
442 via_storage(&datetime_meta),
443 Some(instant.to_order()),
444 "the order key is the stored layout, so the projection must round-trip exactly"
445 );
446
447 let next_instant = GroupMeta {
450 high_water: Some(DateTime::from_bits(instant.to_bits() + 1)),
451 };
452 assert!(via_storage(&next_instant) > via_storage(&datetime_meta));
453 }
454
455 #[test]
456 fn a_group_that_never_advanced_projects_to_none_through_storage() {
457 let empty: GroupMeta<DateTime> = GroupMeta {
459 high_water: None,
460 };
461 assert_eq!(via_storage(&empty), None);
462 }
463}
464
465#[cfg(test)]
466mod meta_sweep_tests {
467 use reifydb_value::{factory::time::at_millis, value::datetime::DateTime};
468
469 use super::*;
470 use crate::operator::state::mock::MockStore;
471
472 fn meta_key(index: u32) -> MetaKey {
473 meta_key_for(Hash128::from(index as u128))
476 }
477
478 fn seed(store: &mut MockStore, count: u32, high_water: impl Fn(u32) -> DateTime) {
479 for index in 0..count {
480 set(
481 store,
482 &meta_key(index),
483 &GroupMeta {
484 high_water: Some(high_water(index)),
485 },
486 )
487 .expect("seeding a group meta must succeed");
488 }
489 }
490
491 #[test]
492 fn a_meta_sweep_stops_at_one_page_and_resumes_past_its_cursor() {
493 let mut store = MockStore::default();
497 let total = META_SWEEP_PAGE as u32 + 3;
498 seed(&mut store, total, |_| at_millis(200));
499
500 let mut sweep = MetaSweep::default();
501 let threshold = at_millis(50).to_order();
502
503 assert_eq!(sweep.sweep::<GroupMeta<DateTime>>(&mut store, threshold).unwrap(), 0);
504 assert_eq!(store.rows_visited(), META_SWEEP_PAGE, "one call must visit at most one page");
505
506 assert_eq!(sweep.sweep::<GroupMeta<DateTime>>(&mut store, threshold).unwrap(), 0);
507 assert_eq!(
508 store.rows_visited(),
509 total as usize,
510 "the next call must resume past the cursor rather than rescan the first page"
511 );
512 }
513
514 #[test]
515 fn a_paged_meta_sweep_publishes_the_low_water_of_every_page_it_walked() {
516 let mut store = MockStore::default();
521 let total = META_SWEEP_PAGE as u32 + 3;
522 seed(&mut store, total, |index| {
523 if index == 0 {
524 at_millis(100)
525 } else {
526 at_millis(200)
527 }
528 });
529
530 let mut sweep = MetaSweep::default();
531 let early = at_millis(50).to_order();
532 assert_eq!(sweep.sweep::<GroupMeta<DateTime>>(&mut store, early).unwrap(), 0);
533 assert_eq!(sweep.sweep::<GroupMeta<DateTime>>(&mut store, early).unwrap(), 0);
534
535 let walked = store.rows_visited();
536 assert_eq!(sweep.sweep::<GroupMeta<DateTime>>(&mut store, at_millis(90).to_order()).unwrap(), 0);
537 assert_eq!(
538 store.rows_visited(),
539 walked,
540 "a completed revolution must publish a low water the guard can skip on"
541 );
542
543 let mut reclaimed = 0;
546 for _ in 0..2 {
547 reclaimed += sweep.sweep::<GroupMeta<DateTime>>(&mut store, at_millis(150).to_order()).unwrap();
548 }
549 assert_eq!(
550 reclaimed, 1,
551 "the group whose high water sits below the threshold is stale and must be reclaimed"
552 );
553 }
554}