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