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