Skip to main content

reifydb_engine/queue/
wake.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	collections::{HashMap, VecDeque},
6	fmt,
7	sync::Arc,
8};
9
10use reifydb_core::interface::catalog::id::QueueId;
11use reifydb_runtime::sync::{mutex::Mutex, waiter::WaiterHandle};
12
13#[derive(Clone)]
14pub struct QueueWakeRegistry(Arc<QueueWakeRegistryInner>);
15
16struct QueueWakeRegistryInner {
17	queues: Mutex<HashMap<QueueId, VecDeque<Arc<WaiterHandle>>>>,
18}
19
20impl fmt::Debug for QueueWakeRegistry {
21	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22		f.debug_struct("QueueWakeRegistry").finish_non_exhaustive()
23	}
24}
25
26impl Default for QueueWakeRegistry {
27	fn default() -> Self {
28		Self::new()
29	}
30}
31
32impl QueueWakeRegistry {
33	pub fn new() -> Self {
34		Self(Arc::new(QueueWakeRegistryInner {
35			queues: Mutex::new(HashMap::new()),
36		}))
37	}
38
39	pub fn register(&self, queue: QueueId, waiter: Arc<WaiterHandle>) {
40		self.0.queues.lock().entry(queue).or_default().push_back(waiter);
41	}
42
43	pub fn deregister(&self, queue: QueueId, waiter: &Arc<WaiterHandle>) {
44		let mut queues = self.0.queues.lock();
45		let Some(waiters) = queues.get_mut(&queue) else {
46			return;
47		};
48		if let Some(index) = waiters.iter().position(|parked| Arc::ptr_eq(parked, waiter)) {
49			waiters.remove(index);
50		}
51		if waiters.is_empty() {
52			queues.remove(&queue);
53		}
54	}
55
56	pub fn nudge(&self, queue: QueueId, count: usize) {
57		if count == 0 {
58			return;
59		}
60
61		let woken = {
62			let mut queues = self.0.queues.lock();
63			let Some(waiters) = queues.get_mut(&queue) else {
64				return;
65			};
66			let taken: Vec<Arc<WaiterHandle>> = waiters.drain(..count.min(waiters.len())).collect();
67			if waiters.is_empty() {
68				queues.remove(&queue);
69			}
70			taken
71		};
72
73		for waiter in woken {
74			waiter.notify();
75		}
76	}
77
78	pub fn parked(&self, queue: QueueId) -> usize {
79		self.0.queues.lock().get(&queue).map_or(0, VecDeque::len)
80	}
81}
82
83#[cfg(test)]
84mod tests {
85	use reifydb_value::value::duration::Duration;
86
87	use super::*;
88
89	fn zero() -> Duration {
90		Duration::from_milliseconds(0).unwrap()
91	}
92
93	fn park(registry: &QueueWakeRegistry, queue: QueueId) -> Arc<WaiterHandle> {
94		let waiter = Arc::new(WaiterHandle::new());
95		registry.register(queue, waiter.clone());
96		waiter
97	}
98
99	#[test]
100	fn test_a_nudge_wakes_the_oldest_parked_worker_first() {
101		// Wake-N FIFO is the whole thundering-herd policy: one committed item must wake exactly
102		// one worker, and it must be the one that has been waiting longest. If this silently
103		// became LIFO the newest worker starves the oldest, and if it became wake-all every
104		// single-item INSERT would spend N-1 workers on an empty rescan.
105		let registry = QueueWakeRegistry::new();
106		let queue = QueueId(7);
107
108		let first = park(&registry, queue);
109		let second = park(&registry, queue);
110		let third = park(&registry, queue);
111
112		registry.nudge(queue, 1);
113
114		assert!(first.wait_timeout(zero()), "the oldest parked worker must be the one woken");
115		assert!(!second.wait_timeout(zero()), "a single item must not wake a second worker");
116		assert!(!third.wait_timeout(zero()));
117		assert_eq!(registry.parked(queue), 2, "the woken waiter must leave the park list");
118	}
119
120	#[test]
121	fn test_a_nudge_that_arrives_before_the_wait_is_not_lost() {
122		// The park loop registers BEFORE it scans, precisely so an INSERT committing between the
123		// empty scan and the wait cannot be missed. That ordering is only safe if a notify landing
124		// on a not-yet-waiting handle is remembered; otherwise the worker sleeps out its whole
125		// budget while its item sits ready.
126		let registry = QueueWakeRegistry::new();
127		let queue = QueueId(1);
128
129		let waiter = park(&registry, queue);
130		registry.nudge(queue, 1);
131
132		assert!(waiter.wait_timeout(zero()), "a nudge delivered before the wait must still release it");
133	}
134
135	#[test]
136	fn test_deregistering_one_waiter_leaves_the_others_parked() {
137		// A client that disconnects drops its guard mid-park. Removing the wrong entry would either
138		// wake a dead waiter (the nudge is swallowed and a live worker keeps sleeping) or leak the
139		// disconnected one forever.
140		let registry = QueueWakeRegistry::new();
141		let queue = QueueId(3);
142
143		let first = park(&registry, queue);
144		let second = park(&registry, queue);
145
146		registry.deregister(queue, &second);
147		assert_eq!(registry.parked(queue), 1);
148
149		registry.nudge(queue, 1);
150
151		assert!(first.wait_timeout(zero()), "the surviving waiter must still be reachable");
152		assert!(!second.wait_timeout(zero()), "a deregistered waiter must never be notified");
153		assert_eq!(registry.parked(queue), 0);
154	}
155
156	#[test]
157	fn test_nudging_more_items_than_parked_workers_is_harmless() {
158		// A batch INSERT of 5 items with 2 idle workers is ordinary. Draining past the end of the
159		// list must not panic, and it must leave no empty map entry behind that would grow without
160		// bound across queue ids.
161		let registry = QueueWakeRegistry::new();
162		let queue = QueueId(9);
163
164		let first = park(&registry, queue);
165		let second = park(&registry, queue);
166
167		registry.nudge(queue, 5);
168
169		assert!(first.wait_timeout(zero()));
170		assert!(second.wait_timeout(zero()));
171		assert_eq!(registry.parked(queue), 0);
172	}
173
174	#[test]
175	fn test_queues_do_not_share_a_park_list() {
176		// Every queue has its own workers. A nudge that leaked across queue ids would hand a wake
177		// meant for one queue to a worker polling another, which then burns a claim on an empty
178		// queue while its own item waits.
179		let registry = QueueWakeRegistry::new();
180
181		let mine = park(&registry, QueueId(1));
182		let theirs = park(&registry, QueueId(2));
183
184		registry.nudge(QueueId(1), 4);
185
186		assert!(mine.wait_timeout(zero()));
187		assert!(!theirs.wait_timeout(zero()), "a nudge must not cross queue boundaries");
188		assert_eq!(registry.parked(QueueId(2)), 1);
189	}
190}