Skip to main content

reifydb_flow/window/
meta.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::mem::size_of;
5
6use reifydb_core::{
7	key::operator_state::{GroupId, GroupStateKey, IntoGroupStateKey, Keyspace, OperatorStateKey},
8	metrics::heap::HeapSize,
9	state::{cache::StateCache, store::StateStore},
10};
11use reifydb_macro::operator_state;
12use reifydb_value::{
13	Result,
14	value::{Value, datetime::DateTime, row_number::RowNumber},
15};
16
17use crate::{
18	operator::state::seal::{
19		coord::Coord,
20		ledger::{SealLedgerState, seal_ledger_key},
21	},
22	window::kind::session::SessionTracker,
23};
24
25#[operator_state]
26#[derive(Clone, Default)]
27pub struct CountState {
28	pub value: u64,
29}
30
31impl HeapSize for CountState {
32	fn heap_size(&self) -> usize {
33		0
34	}
35}
36
37#[operator_state]
38#[derive(Clone, Default)]
39pub struct RowIndexState {
40	pub window_ids: Vec<u64>,
41}
42
43impl HeapSize for RowIndexState {
44	fn heap_size(&self) -> usize {
45		self.window_ids.len() * size_of::<u64>()
46	}
47}
48
49#[operator_state]
50#[derive(Clone, Default)]
51pub struct SessionState {
52	pub session_id: u64,
53	pub last_event_time: u64,
54	pub session_start: u64,
55}
56
57impl HeapSize for SessionState {
58	fn heap_size(&self) -> usize {
59		0
60	}
61}
62
63#[operator_state]
64#[derive(Clone, Default)]
65pub struct EngineMeta {
66	pub last_event_time: u64,
67}
68
69impl HeapSize for EngineMeta {
70	fn heap_size(&self) -> usize {
71		0
72	}
73}
74
75#[operator_state]
76#[derive(Clone, Default)]
77pub struct RollingMeta {
78	pub group_hash: u128,
79	pub row_number: u64,
80	pub group_values: Vec<Value>,
81	pub last_value: Vec<Value>,
82}
83
84impl HeapSize for RollingMeta {
85	fn heap_size(&self) -> usize {
86		(self.group_values.capacity() + self.last_value.capacity()) * size_of::<Value>()
87			+ self.group_values.iter().map(|v| v.heap_size()).sum::<usize>()
88			+ self.last_value.iter().map(|v| v.heap_size()).sum::<usize>()
89	}
90}
91
92#[derive(Clone, Copy, Hash, PartialEq, Eq)]
93pub struct SealLedgerKey;
94
95impl HeapSize for SealLedgerKey {
96	fn heap_size(&self) -> usize {
97		0
98	}
99}
100
101impl IntoGroupStateKey for &SealLedgerKey {
102	fn into_group_state_key(self) -> GroupStateKey {
103		seal_ledger_key()
104	}
105}
106
107#[derive(Clone, Copy, Hash, PartialEq, Eq)]
108pub struct CountKey(pub GroupId);
109
110impl HeapSize for CountKey {
111	fn heap_size(&self) -> usize {
112		0
113	}
114}
115
116impl IntoGroupStateKey for &CountKey {
117	fn into_group_state_key(self) -> GroupStateKey {
118		OperatorStateKey::inner_encoded(self.0, Keyspace::COUNT, vec![])
119	}
120}
121
122#[derive(Clone, Copy, Hash, PartialEq, Eq)]
123pub struct RowIndexKey(pub GroupId, pub RowNumber);
124
125impl HeapSize for RowIndexKey {
126	fn heap_size(&self) -> usize {
127		0
128	}
129}
130
131impl IntoGroupStateKey for &RowIndexKey {
132	fn into_group_state_key(self) -> GroupStateKey {
133		OperatorStateKey::inner_encoded(self.0, Keyspace::ROW_INDEX, self.1.0.to_be_bytes())
134	}
135}
136
137#[derive(Clone, Copy, Hash, PartialEq, Eq)]
138pub struct SessionKey(pub GroupId);
139
140impl HeapSize for SessionKey {
141	fn heap_size(&self) -> usize {
142		0
143	}
144}
145
146impl IntoGroupStateKey for &SessionKey {
147	fn into_group_state_key(self) -> GroupStateKey {
148		OperatorStateKey::inner_encoded(self.0, Keyspace::SESSION, vec![])
149	}
150}
151
152#[derive(Clone, Copy, Hash, PartialEq, Eq)]
153pub struct EngineMetaKey(pub GroupId);
154
155impl HeapSize for EngineMetaKey {
156	fn heap_size(&self) -> usize {
157		0
158	}
159}
160
161impl IntoGroupStateKey for &EngineMetaKey {
162	fn into_group_state_key(self) -> GroupStateKey {
163		OperatorStateKey::inner_encoded(self.0, Keyspace::ENGINE_META, vec![])
164	}
165}
166
167#[derive(Clone, Copy, Hash, PartialEq, Eq)]
168pub struct RollingMetaKey(pub GroupId);
169
170impl HeapSize for RollingMetaKey {
171	fn heap_size(&self) -> usize {
172		0
173	}
174}
175
176impl IntoGroupStateKey for &RollingMetaKey {
177	fn into_group_state_key(self) -> GroupStateKey {
178		OperatorStateKey::inner_encoded(self.0, Keyspace::ROLLING_META, vec![])
179	}
180}
181
182pub struct WindowMeta {
183	seal_ledger: StateCache<SealLedgerKey, SealLedgerState>,
184	count: StateCache<CountKey, CountState>,
185	row_index: StateCache<RowIndexKey, RowIndexState>,
186	session: StateCache<SessionKey, SessionState>,
187	rolling_meta: StateCache<RollingMetaKey, RollingMeta>,
188}
189
190impl Default for WindowMeta {
191	fn default() -> Self {
192		Self::new()
193	}
194}
195
196impl WindowMeta {
197	pub fn new() -> Self {
198		Self {
199			seal_ledger: StateCache::new(),
200			count: StateCache::new(),
201			row_index: StateCache::new(),
202			session: StateCache::new(),
203			rolling_meta: StateCache::new(),
204		}
205	}
206
207	pub fn seal_ledger(&mut self, store: &mut dyn StateStore) -> Result<u64> {
208		Ok(self.seal_ledger.get_or_default(store, &SealLedgerKey)?.sealed_through)
209	}
210
211	pub fn advance_seal_ledger(&mut self, store: &mut dyn StateStore, coord: u64) -> Result<()> {
212		if coord > self.seal_ledger(store)? {
213			self.seal_ledger.put(
214				store,
215				&SealLedgerKey,
216				SealLedgerState {
217					sealed_through: coord,
218				},
219			)?;
220		}
221		Ok(())
222	}
223
224	pub fn get_and_increment_count(&mut self, store: &mut dyn StateStore, group: GroupId) -> Result<u64> {
225		let key = CountKey(group);
226		let current = self.count.get_or_default(store, &key)?.value;
227		self.count.put(
228			store,
229			&key,
230			CountState {
231				value: current + 1,
232			},
233		)?;
234		Ok(current)
235	}
236
237	pub fn lookup_row_index(
238		&mut self,
239		store: &mut dyn StateStore,
240		group: GroupId,
241		row_number: RowNumber,
242	) -> Result<Vec<u64>> {
243		Ok(self.row_index.get_or_default(store, &RowIndexKey(group, row_number))?.window_ids)
244	}
245
246	pub fn store_row_index(
247		&mut self,
248		store: &mut dyn StateStore,
249		group: GroupId,
250		row_number: RowNumber,
251		window_id: u64,
252	) -> Result<()> {
253		let key = RowIndexKey(group, row_number);
254		let mut state = self.row_index.get_or_default(store, &key)?;
255		if !state.window_ids.contains(&window_id) {
256			state.window_ids.push(window_id);
257		}
258		self.row_index.put(store, &key, state)
259	}
260
261	pub fn drop_row_index(
262		&mut self,
263		store: &mut dyn StateStore,
264		group: GroupId,
265		row_number: RowNumber,
266	) -> Result<()> {
267		self.row_index.remove(store, &RowIndexKey(group, row_number))
268	}
269
270	pub fn load_session(&mut self, store: &mut dyn StateStore, group: GroupId) -> Result<SessionTracker> {
271		let Some(state) = self.session.get(store, &SessionKey(group))? else {
272			return Ok(SessionTracker::default());
273		};
274		Ok(SessionTracker::resumed(
275			state.session_id,
276			<DateTime as Coord>::from_order(state.last_event_time),
277			<DateTime as Coord>::from_order(state.session_start),
278		))
279	}
280
281	pub fn save_session(
282		&mut self,
283		store: &mut dyn StateStore,
284		group: GroupId,
285		tracker: &SessionTracker,
286	) -> Result<()> {
287		self.session.put(
288			store,
289			&SessionKey(group),
290			SessionState {
291				session_id: tracker.session_id,
292				last_event_time: tracker.last.to_order(),
293				session_start: tracker.start.to_order(),
294			},
295		)
296	}
297
298	pub fn rolling_meta(&mut self, store: &mut dyn StateStore, group: GroupId) -> Result<Option<RollingMeta>> {
299		self.rolling_meta.get(store, &RollingMetaKey(group))
300	}
301
302	pub fn put_rolling_meta(
303		&mut self,
304		store: &mut dyn StateStore,
305		group: GroupId,
306		meta: RollingMeta,
307	) -> Result<()> {
308		self.rolling_meta.put(store, &RollingMetaKey(group), meta)
309	}
310
311	pub fn drop_rolling_meta(&mut self, store: &mut dyn StateStore, group: GroupId) -> Result<()> {
312		self.rolling_meta.remove(store, &RollingMetaKey(group))
313	}
314}
315
316#[cfg(test)]
317mod tests {
318	use std::ops::Bound::{Excluded, Included, Unbounded};
319
320	use reifydb_codec::key::encoded::EncodedKeyRange;
321	use reifydb_core::key::operator_state::{GroupId, IntoGroupStateKey, OperatorStateKey, group_data_inner_range};
322	use reifydb_value::{factory::time::at_millis, value::row_number::RowNumber};
323
324	use super::{CountKey, RowIndexKey, SealLedgerKey, SessionKey, WindowMeta};
325	use crate::{operator::state::mock::MockStore, window::kind::session::SessionTracker};
326
327	const GROUP: GroupId = GroupId(42);
328
329	fn contains(range: &EncodedKeyRange, key: &[u8]) -> bool {
330		let above = match &range.start {
331			Included(bound) => key >= bound.as_slice(),
332			Excluded(bound) => key > bound.as_slice(),
333			Unbounded => true,
334		};
335		let below = match &range.end {
336			Included(bound) => key <= bound.as_slice(),
337			Excluded(bound) => key < bound.as_slice(),
338			Unbounded => true,
339		};
340		above && below
341	}
342
343	#[test]
344	fn partition_scoped_meta_lands_inside_the_group_the_substrate_reclaims() {
345		// landing this in the root group would leave no group range able to reach it, stranding one row per
346		// partition forever, so it lives in the partition group's data range instead
347		let range = group_data_inner_range(GROUP);
348		for key in [
349			(&CountKey(GROUP)).into_group_state_key(),
350			(&SessionKey(GROUP)).into_group_state_key(),
351			(&RowIndexKey(GROUP, RowNumber(7))).into_group_state_key(),
352		] {
353			let (group, keyspace, _) =
354				OperatorStateKey::decode_inner(key.as_bytes()).expect("meta keys are structured");
355			assert_eq!(group, GROUP, "partition-scoped meta escaped its group");
356			assert!(keyspace.is_data(), "{keyspace:?} must be a data keyspace to be reclaimed by phase 1");
357			assert!(contains(&range, key.as_bytes()), "{keyspace:?} landed outside the group data range");
358		}
359	}
360
361	#[test]
362	fn the_seal_ledger_stays_out_of_every_group_range() {
363		// The seal ledger is per operator, one entry for the whole operator. Under a real group id,
364		// reclaiming that group would reset it and every later event would look admissible again.
365		let key = (&SealLedgerKey).into_group_state_key();
366		let (group, _, _) = OperatorStateKey::decode_inner(key.as_bytes()).expect("meta keys are structured");
367		assert_eq!(group, GroupId::ROOT);
368		assert!(!contains(&group_data_inner_range(GROUP), key.as_bytes()));
369	}
370
371	#[test]
372	fn count_and_session_share_a_group_and_are_told_apart_only_by_the_keyspace() {
373		// Both are a bare partition group with an empty suffix, so the keyspace byte is all that
374		// separates them. Reading one as the other deserializes happily - two u64 payloads - and
375		// corrupts session assignment with an event ordinal.
376		let count = (&CountKey(GROUP)).into_group_state_key();
377		let session = (&SessionKey(GROUP)).into_group_state_key();
378		assert_ne!(count, session, "count and session must not share a key");
379
380		let (count_group, count_ks, count_suffix) = OperatorStateKey::decode_inner(count.as_bytes()).unwrap();
381		let (session_group, session_ks, session_suffix) =
382			OperatorStateKey::decode_inner(session.as_bytes()).unwrap();
383		assert_eq!(count_group, session_group, "both belong to the same partition");
384		assert_ne!(count_ks, session_ks, "only the keyspace may distinguish them");
385		assert!(count_suffix.is_empty() && session_suffix.is_empty());
386	}
387
388	#[test]
389	fn a_session_persisted_at_the_epoch_reloads_as_open_rather_than_as_a_fresh_tracker() {
390		// A SessionState row exists only once a group has opened a session, so row presence IS the
391		// openness bit and load_session must read it as an Option. Defaulting an absent row would
392		// make an all-zero session, which is one opened at the epoch, read as never-seen.
393		let mut meta = WindowMeta::new();
394		let mut store = MockStore::default();
395
396		assert_eq!(
397			meta.load_session(&mut store, GROUP).unwrap(),
398			SessionTracker::default(),
399			"a group with no persisted session must load as unopened"
400		);
401
402		meta.save_session(&mut store, GROUP, &SessionTracker::resumed(0, at_millis(0), at_millis(0))).unwrap();
403
404		assert_eq!(
405			meta.load_session(&mut store, GROUP).unwrap(),
406			SessionTracker::resumed(0, at_millis(0), at_millis(0))
407		);
408	}
409}