Skip to main content

reifydb_core/key/operator/keyspace/
timer.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use reifydb_value::{util::hash::xxh3_128, value::datetime::DateTime};
5
6use crate::{
7	key::{
8		operator::{
9			state::{GroupId, KeyspaceId},
10			traits::Keyspace,
11		},
12		typed::{
13			BoundedKey, DenseKey, KeyLayout,
14			direction::{Asc, Direction, KeyField},
15			layout::{KeyColumn, KeyColumnType, KeyLayout, KeyValue, KeyValues},
16		},
17	},
18	metrics::heap::HeapSize,
19	state::timer::TimerKind,
20};
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, KeyLayout, HeapSize)]
23pub struct TimerWheelKey {
24	pub due: Asc<DateTime>,
25	pub kind: Asc<TimerKind>,
26	pub id: Asc<[u8; 16]>,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, KeyLayout, HeapSize)]
30pub struct TimerIndexKey {
31	pub kind: Asc<TimerKind>,
32	pub id: Asc<[u8; 16]>,
33}
34
35pub fn timer_id(bytes: &[u8]) -> [u8; 16] {
36	xxh3_128(bytes).0.to_be_bytes()
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub struct TimerWheel;
41
42impl Keyspace for TimerWheel {
43	const ID: KeyspaceId = KeyspaceId::TIMER_WHEEL;
44	const NAME: &'static str = "TIMER_WHEEL";
45	const RANGE_CACHED: bool = true;
46
47	type GroupedKey = TimerWheelKey;
48	type Suffix = TimerWheelKey;
49
50	fn split(key: &Self::GroupedKey) -> (GroupId, Self::Suffix) {
51		(GroupId::ROOT, *key)
52	}
53
54	fn join(_group: GroupId, suffix: Self::Suffix) -> Self::GroupedKey {
55		suffix
56	}
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub struct TimerIndex;
61
62impl Keyspace for TimerIndex {
63	const ID: KeyspaceId = KeyspaceId::TIMER_INDEX;
64	const NAME: &'static str = "TIMER_INDEX";
65	const RANGE_CACHED: bool = true;
66
67	type GroupedKey = TimerIndexKey;
68	type Suffix = TimerIndexKey;
69
70	fn split(key: &Self::GroupedKey) -> (GroupId, Self::Suffix) {
71		(GroupId::ROOT, *key)
72	}
73
74	fn join(_group: GroupId, suffix: Self::Suffix) -> Self::GroupedKey {
75		suffix
76	}
77}
78
79#[cfg(test)]
80mod tests {
81	use super::{TimerIndexKey, TimerWheelKey, timer_id};
82	use crate::key::typed::{
83		direction::{Asc, Direction},
84		layout::{KeyColumn, KeyColumnType, KeyLayout},
85	};
86
87	#[test]
88	fn a_timer_id_of_any_length_narrows_to_the_key_width() {
89		// two built-in producers exceed sixteen bytes and one of them serialises user supplied values,
90		// so a wider id must be folded here rather than truncated at the key boundary
91		for len in [0usize, 1, 16, 25, 4096] {
92			let bytes = vec![0xABu8; len];
93			assert_eq!(timer_id(&bytes).len(), 16, "an id of {len} bytes must still key on sixteen");
94		}
95	}
96
97	#[test]
98	fn distinct_timer_ids_stay_distinct_after_narrowing() {
99		// the wheel orders by time first and the id is identity, not sort order, so a collision here
100		// silently makes two timers the same row
101		let long = vec![0x11u8; 25];
102		let mut other = long.clone();
103		other[24] = 0x12;
104		assert_ne!(timer_id(&long), timer_id(&other));
105		assert_ne!(timer_id(b""), timer_id(b"\0"));
106	}
107
108	#[test]
109	fn a_timer_id_is_stable_across_calls() {
110		// the id is written once and looked up later; a per process seed would lose every armed timer
111		assert_eq!(timer_id(b"seal:window:7"), timer_id(b"seal:window:7"));
112	}
113
114	#[test]
115	fn timer_ids_order_as_the_unsigned_integers_they_hash_to() {
116		// R14: the big endian array is what sqlite stores and what derived Ord compares, so the two must
117		// agree or the in memory wheel and the table would walk the index in different orders
118		let low = 7u128.to_be_bytes();
119		let high = 8u128.to_be_bytes();
120		assert!(low < high);
121		assert!(Asc(low) < Asc(high));
122	}
123
124	#[test]
125	fn the_wheel_leads_on_due_time_and_the_index_leads_on_kind() {
126		// the wheel exists to answer "what is due next" in one forward scan, so due time must be the
127		// leading column; the index answers "where is this timer" and leads on kind instead
128		assert_eq!(TimerWheelKey::COLUMNS[0].name, "due");
129		assert_eq!(TimerWheelKey::COLUMNS[0].direction, Direction::Asc);
130		assert_eq!(TimerWheelKey::COLUMNS[2].ty, KeyColumnType::Blob16);
131		assert_eq!(TimerIndexKey::COLUMNS[0].name, "kind");
132		assert_eq!(
133			TimerIndexKey::COLUMNS[1],
134			KeyColumn {
135				name: "id",
136				ty: KeyColumnType::Blob16,
137				direction: Direction::Asc,
138			}
139		);
140	}
141}