Skip to main content

reifydb_flow/window/engine/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4//! Schema-agnostic windowing state-machine engines. An engine owns accumulator state, late
5//! rejection, eviction and diff routing; the caller (the "face") owns extraction and output
6//! construction, handing over pre-bucketed events and translating [`WindowResult`]s into diffs.
7
8pub mod config;
9pub mod rolling;
10pub mod rolling_incremental;
11pub mod rolling_top_k;
12pub mod tumbling;
13pub mod tumbling_carry;
14
15use std::collections::HashMap;
16
17use reifydb_codec::{
18	key::{
19		encode_u64_asc,
20		encoded::{EncodedKey, EncodedKeyRange, IntoEncodedKey},
21	},
22	row::operator::{OperatorState, decode},
23};
24use reifydb_core::{
25	key::operator_state::{
26		GroupId, GroupStateKey, IntoGroupStateKey, Keyspace, OperatorStateKey, keyspace_inner_range,
27	},
28	metrics::heap::HeapSize,
29	state::{cache::StateCache, store::StateStore},
30};
31use reifydb_macro::operator_state;
32use reifydb_value::{Result, value::row_number::RowNumber};
33use tracing::{debug, instrument};
34
35use crate::{
36	operator::state::seal::coord::Coord,
37	window::span::{Slot, WindowSpan},
38};
39
40/// One contribution routed to a window accumulator.
41pub enum AccumulatorEvent<C> {
42	Add(C),
43	Remove(C),
44}
45
46fn note_when_expiry_capped(expired: usize, expire_batch: usize) {
47	if expired >= expire_batch {
48		debug!(expired, expire_batch, "window expiry hit per-tick batch cap, backlog deferred to next tick");
49	}
50}
51
52/// How a finalized window value should be emitted downstream.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum EmitKind {
55	Insert,
56	Update,
57	Remove,
58}
59
60/// A finalized window the engine produced; the face turns it into a diff.
61pub struct WindowResult<G, Coord, Output> {
62	pub row_number: RowNumber,
63	pub group: G,
64	pub span: WindowSpan<Coord>,
65	pub value: Output,
66	/// The finalized value before this batch's events, for faces that emit a real pre on
67	/// Update/Remove. `None` for a brand-new window.
68	pub prior: Option<Output>,
69	pub kind: EmitKind,
70}
71
72/// The highest window start seen for a group, used to drop late events for already-closed windows.
73#[operator_state]
74#[derive(Debug, Clone)]
75pub struct GroupMeta<K> {
76	pub high_water: Option<K>,
77}
78
79impl<K> Default for GroupMeta<K> {
80	fn default() -> Self {
81		Self {
82			high_water: None,
83		}
84	}
85}
86
87impl<K> HeapSize for GroupMeta<K> {
88	fn heap_size(&self) -> usize {
89		0
90	}
91}
92
93/// The group's high-water anchor as a comparable order key, so the meta sweep is uniform across
94/// engines. Callers sweep at the seal horizon, so a group below it has seen nothing since its own
95/// windows sealed and the late-event rejection this meta drives has nothing left to reject.
96pub(crate) trait MetaHighWater: OperatorState {
97	fn high_water_order(&self) -> Option<u64>;
98}
99
100impl<C: Slot> MetaHighWater for GroupMeta<C> {
101	fn high_water_order(&self) -> Option<u64> {
102		self.high_water.map(|hw| hw.order_key().to_order())
103	}
104}
105
106/// `initial` is the persisted value snapshotted at load, served archived without materializing;
107/// `bumped` is the batch-local monotonic advance. Only groups with a bump persist.
108pub(crate) struct BatchMeta<C> {
109	pub(crate) initial: Option<C>,
110	pub(crate) bumped: Option<C>,
111}
112
113impl<C> Default for BatchMeta<C> {
114	fn default() -> Self {
115		Self {
116			initial: None,
117			bumped: None,
118		}
119	}
120}
121
122impl<C: Slot> BatchMeta<C> {
123	pub(crate) fn observe(&mut self, coord: C) {
124		match self.high_water() {
125			Some(hw) if coord > hw => self.bumped = Some(coord),
126			None => self.bumped = Some(coord),
127			_ => {}
128		}
129	}
130
131	pub(crate) fn high_water(&self) -> Option<C> {
132		self.bumped.or(self.initial)
133	}
134}
135
136pub(crate) fn load_batch_meta<C>(
137	store: &mut dyn StateStore,
138	meta: &mut StateCache<MetaKey, GroupMeta<C>>,
139	key: &MetaKey,
140) -> Result<BatchMeta<C>>
141where
142	C: Slot,
143{
144	let initial = meta.get(store, key)?.and_then(|meta| meta.high_water);
145	Ok(BatchMeta {
146		initial,
147		bumped: None,
148	})
149}
150
151pub(crate) fn persist_batch_meta<G, C>(
152	store: &mut dyn StateStore,
153	meta: &mut StateCache<MetaKey, GroupMeta<C>>,
154	loaded: HashMap<G, BatchMeta<C>>,
155) -> Result<()>
156where
157	for<'a> &'a G: IntoEncodedKey,
158	C: Slot,
159{
160	for (group, batch) in loaded {
161		let Some(bumped) = batch.bumped else {
162			continue;
163		};
164		meta.modify(store, &meta_key_for(&group), |native| {
165			if native.high_water.is_none_or(|hw| bumped > hw) {
166				native.high_water = Some(bumped);
167			}
168		})?;
169	}
170	Ok(())
171}
172
173/// The internal-key range covering every per-group meta. It lives in the root group because the meta
174/// is keyed by partition while a window group is (partition, window), so it fits inside neither group's range.
175pub(crate) fn meta_range() -> EncodedKeyRange {
176	keyspace_inner_range(GroupId::ROOT, Keyspace::WINDOW_META)
177}
178
179/// Reclaim every group meta whose high water is strictly below `threshold`.
180///
181/// `low_water` is a lower bound on the current minimum high water, so a bound already at or above
182/// the threshold skips the scan entirely and keeps the steady-state sweep O(1). Staleness is a
183/// value, not a key prefix, so the scan itself must cover the whole meta keyspace.
184#[instrument(name = "flow::window::sweep_stale_meta", level = "debug", skip_all)]
185pub(crate) fn sweep_stale_meta<M>(
186	store: &mut dyn StateStore,
187	meta: &mut StateCache<MetaKey, M>,
188	threshold: u64,
189	low_water: &mut Option<u64>,
190) -> Result<usize>
191where
192	M: MetaHighWater + Clone + OperatorState + HeapSize,
193{
194	if low_water.is_some_and(|lw| lw >= threshold) {
195		return Ok(0);
196	}
197	let mut stale: Vec<MetaKey> = Vec::new();
198	let mut min_surviving: Option<u64> = None;
199	store.state_range_visit(meta_range(), None, &mut |key, bytes| {
200		if let Some(hw) = decode::<M>(&bytes)?.high_water_order() {
201			if hw < threshold {
202				let Some(key) = decode_meta_key(key.as_encoded()) else {
203					return Ok(());
204				};
205				stale.push(key);
206			} else {
207				min_surviving = Some(min_surviving.map_or(hw, |m| m.min(hw)));
208			}
209		}
210		Ok(())
211	})?;
212	*low_water = min_surviving;
213	let count = stale.len();
214	for key in &stale {
215		meta.remove(store, key)?;
216	}
217	Ok(count)
218}
219
220/// State-cache key for a group's [`GroupMeta`], tagged into a keyspace distinct from the
221/// per-window accumulators.
222#[derive(Clone, Hash, PartialEq, Eq)]
223pub struct MetaKey(pub EncodedKey);
224
225impl HeapSize for MetaKey {
226	fn heap_size(&self) -> usize {
227		self.0.heap_size()
228	}
229}
230
231#[derive(Clone, Hash, PartialEq, Eq)]
232pub struct RunningKey {
233	pub group: GroupId,
234	pub slot: EncodedKey,
235}
236
237impl RunningKey {
238	pub fn new(group: GroupId, slot: EncodedKey) -> Self {
239		Self {
240			group,
241			slot,
242		}
243	}
244
245	pub fn of_row(group: GroupId, row: RowNumber) -> Self {
246		Self::new(group, EncodedKey::new(encode_u64_asc(row.0)))
247	}
248}
249
250impl HeapSize for RunningKey {
251	fn heap_size(&self) -> usize {
252		match &self.slot {
253			EncodedKey::Inline {
254				..
255			} => 0,
256			EncodedKey::Shared(bytes) => bytes.len(),
257		}
258	}
259}
260
261impl IntoGroupStateKey for &RunningKey {
262	fn into_group_state_key(self) -> GroupStateKey {
263		OperatorStateKey::inner_encoded(self.group, Keyspace::RUNNING, self.slot.as_bytes())
264	}
265}
266
267#[derive(Clone, Hash, PartialEq, Eq)]
268pub struct WindowStateKey {
269	pub group: GroupId,
270	pub slot: EncodedKey,
271}
272
273impl WindowStateKey {
274	pub fn new(group: GroupId, slot: EncodedKey) -> Self {
275		Self {
276			group,
277			slot,
278		}
279	}
280
281	pub fn root(slot: EncodedKey) -> Self {
282		Self::new(GroupId::ROOT, slot)
283	}
284
285	pub fn of_row(group: GroupId, row: RowNumber) -> Self {
286		Self::new(group, EncodedKey::new(encode_u64_asc(row.0)))
287	}
288}
289
290impl HeapSize for WindowStateKey {
291	fn heap_size(&self) -> usize {
292		match &self.slot {
293			EncodedKey::Inline {
294				..
295			} => 0,
296			EncodedKey::Shared(bytes) => bytes.len(),
297		}
298	}
299}
300
301impl IntoGroupStateKey for &WindowStateKey {
302	fn into_group_state_key(self) -> GroupStateKey {
303		OperatorStateKey::inner_encoded(self.group, Keyspace::ACCUMULATOR, self.slot.as_bytes())
304	}
305}
306
307#[derive(Clone, Hash, PartialEq, Eq)]
308pub struct BufferKey {
309	pub group: GroupId,
310	pub slot: EncodedKey,
311}
312
313impl BufferKey {
314	pub fn new(group: GroupId, slot: EncodedKey) -> Self {
315		Self {
316			group,
317			slot,
318		}
319	}
320
321	pub fn of_row(group: GroupId, row: RowNumber) -> Self {
322		Self::new(group, EncodedKey::new(encode_u64_asc(row.0)))
323	}
324}
325
326impl HeapSize for BufferKey {
327	fn heap_size(&self) -> usize {
328		match &self.slot {
329			EncodedKey::Inline {
330				..
331			} => 0,
332			EncodedKey::Shared(bytes) => bytes.len(),
333		}
334	}
335}
336
337impl IntoGroupStateKey for &BufferKey {
338	fn into_group_state_key(self) -> GroupStateKey {
339		OperatorStateKey::inner_encoded(self.group, Keyspace::BUFFER, self.slot.as_bytes())
340	}
341}
342
343#[derive(Clone, Copy, Hash, PartialEq, Eq)]
344pub struct EmitKey {
345	pub group: GroupId,
346	pub row: RowNumber,
347}
348
349impl EmitKey {
350	pub fn new(group: GroupId, row: RowNumber) -> Self {
351		Self {
352			group,
353			row,
354		}
355	}
356}
357
358impl HeapSize for EmitKey {
359	fn heap_size(&self) -> usize {
360		0
361	}
362}
363
364impl IntoGroupStateKey for &EmitKey {
365	fn into_group_state_key(self) -> GroupStateKey {
366		OperatorStateKey::inner_encoded(self.group, Keyspace::EMIT, encode_u64_asc(self.row.0))
367	}
368}
369
370impl IntoGroupStateKey for &MetaKey {
371	fn into_group_state_key(self) -> GroupStateKey {
372		OperatorStateKey::inner_encoded(GroupId::ROOT, Keyspace::WINDOW_META, &self.0)
373	}
374}
375
376pub fn meta_key_for<G>(group: &G) -> MetaKey
377where
378	for<'a> &'a G: IntoEncodedKey,
379{
380	MetaKey(group.into_encoded_key())
381}
382
383/// Which coordinate a window's expiry-index entry is ordered by, and so which coordinate its seal
384/// horizon is measured from.
385#[derive(Debug, Clone, Copy, PartialEq, Eq)]
386pub enum ExpiryAnchor {
387	/// Never indexed, never swept: count-based and plain aggregation windows have no time
388	/// coordinate to expire against.
389	Unindexed,
390	/// Fixed grid, so a late event can neither extend the horizon nor resurrect a swept window.
391	WindowStart,
392	/// A session, whose horizon rides the newest event because that is what keeps it open.
393	LastEvent,
394}
395
396impl ExpiryAnchor {
397	/// `last_event` is `None` only when no event time is known, never zero-as-absent: an event at
398	/// the epoch is a legitimate coordinate, and reading its zero as "no information" lets a
399	/// long-sealed window keep admitting rows.
400	pub fn of(&self, window_start: u64, last_event: Option<u64>) -> Option<u64> {
401		match self {
402			ExpiryAnchor::Unindexed => None,
403			ExpiryAnchor::WindowStart => Some(window_start),
404			ExpiryAnchor::LastEvent => last_event,
405		}
406	}
407}
408
409pub(crate) fn decode_window_state_key(key: &EncodedKey) -> Option<WindowStateKey> {
410	let (group, keyspace, suffix) = OperatorStateKey::decode_inner(key.as_bytes())?;
411	if keyspace != Keyspace::ACCUMULATOR {
412		return None;
413	}
414	Some(WindowStateKey::new(group, EncodedKey::new(suffix)))
415}
416
417pub(crate) fn decode_meta_key(key: &EncodedKey) -> Option<MetaKey> {
418	let (group, keyspace, suffix) = OperatorStateKey::decode_inner(key.as_bytes())?;
419	(group == GroupId::ROOT && keyspace == Keyspace::WINDOW_META).then(|| MetaKey(EncodedKey::new(suffix)))
420}
421
422#[cfg(test)]
423mod archived_projection_tests {
424	use reifydb_value::value::datetime::DateTime;
425
426	use super::*;
427
428	/// Projects the high water the way `sweep_stale_meta` does: encode, decode, then read it.
429	fn via_storage<M: MetaHighWater>(meta: &M) -> Option<u64> {
430		let bytes = meta.encode_state(DateTime::EPOCH).unwrap();
431		decode::<M>(&bytes).unwrap().high_water_order()
432	}
433
434	#[test]
435	fn stored_high_water_yields_the_slot_order_key() {
436		// A wrong order key silently drops a live group's meta or keeps dead meta forever.
437		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		// Adjacent representable instants must keep distinct order keys, or a live group is swept with a dead
448		// one.
449		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		// An accidental Some(0) compares below every threshold and reclaims meta for live groups.
458		let empty: GroupMeta<DateTime> = GroupMeta {
459			high_water: None,
460		};
461		assert_eq!(via_storage(&empty), None);
462	}
463}