1use 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 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 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 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 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}