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 nudge_all(&self, queue: QueueId) {
79		let woken = self.0.queues.lock().remove(&queue).unwrap_or_default();
80		for waiter in woken {
81			waiter.notify();
82		}
83	}
84
85	pub fn parked(&self, queue: QueueId) -> usize {
86		self.0.queues.lock().get(&queue).map_or(0, VecDeque::len)
87	}
88}
89
90#[cfg(test)]
91mod tests {
92	use reifydb_value::value::duration::Duration;
93
94	use super::*;
95
96	fn zero() -> Duration {
97		Duration::from_milliseconds(0).unwrap()
98	}
99
100	fn park(registry: &QueueWakeRegistry, queue: QueueId) -> Arc<WaiterHandle> {
101		let waiter = Arc::new(WaiterHandle::new());
102		registry.register(queue, waiter.clone());
103		waiter
104	}
105
106	#[test]
107	fn test_a_nudge_wakes_the_oldest_parked_worker_first() {
108		// Wake-N FIFO is the whole thundering-herd policy: one committed item must wake exactly
109		// one worker, and it must be the one that has been waiting longest. If this silently
110		// became LIFO the newest worker starves the oldest, and if it became wake-all every
111		// single-item INSERT would spend N-1 workers on an empty rescan.
112		let registry = QueueWakeRegistry::new();
113		let queue = QueueId(7);
114
115		let first = park(&registry, queue);
116		let second = park(&registry, queue);
117		let third = park(&registry, queue);
118
119		registry.nudge(queue, 1);
120
121		assert!(first.wait_timeout(zero()), "the oldest parked worker must be the one woken");
122		assert!(!second.wait_timeout(zero()), "a single item must not wake a second worker");
123		assert!(!third.wait_timeout(zero()));
124		assert_eq!(registry.parked(queue), 2, "the woken waiter must leave the park list");
125	}
126
127	#[test]
128	fn test_a_nudge_that_arrives_before_the_wait_is_not_lost() {
129		// The park loop registers BEFORE it scans, precisely so an INSERT committing between the
130		// empty scan and the wait cannot be missed. That ordering is only safe if a notify landing
131		// on a not-yet-waiting handle is remembered; otherwise the worker sleeps out its whole
132		// budget while its item sits ready.
133		let registry = QueueWakeRegistry::new();
134		let queue = QueueId(1);
135
136		let waiter = park(&registry, queue);
137		registry.nudge(queue, 1);
138
139		assert!(waiter.wait_timeout(zero()), "a nudge delivered before the wait must still release it");
140	}
141
142	#[test]
143	fn test_deregistering_one_waiter_leaves_the_others_parked() {
144		// A client that disconnects drops its guard mid-park. Removing the wrong entry would either
145		// wake a dead waiter (the nudge is swallowed and a live worker keeps sleeping) or leak the
146		// disconnected one forever.
147		let registry = QueueWakeRegistry::new();
148		let queue = QueueId(3);
149
150		let first = park(&registry, queue);
151		let second = park(&registry, queue);
152
153		registry.deregister(queue, &second);
154		assert_eq!(registry.parked(queue), 1);
155
156		registry.nudge(queue, 1);
157
158		assert!(first.wait_timeout(zero()), "the surviving waiter must still be reachable");
159		assert!(!second.wait_timeout(zero()), "a deregistered waiter must never be notified");
160		assert_eq!(registry.parked(queue), 0);
161	}
162
163	#[test]
164	fn test_nudging_more_items_than_parked_workers_is_harmless() {
165		// A batch INSERT of 5 items with 2 idle workers is ordinary. Draining past the end of the
166		// list must not panic, and it must leave no empty map entry behind that would grow without
167		// bound across queue ids.
168		let registry = QueueWakeRegistry::new();
169		let queue = QueueId(9);
170
171		let first = park(&registry, queue);
172		let second = park(&registry, queue);
173
174		registry.nudge(queue, 5);
175
176		assert!(first.wait_timeout(zero()));
177		assert!(second.wait_timeout(zero()));
178		assert_eq!(registry.parked(queue), 0);
179	}
180
181	#[test]
182	fn test_queues_do_not_share_a_park_list() {
183		// Every queue has its own workers. A nudge that leaked across queue ids would hand a wake
184		// meant for one queue to a worker polling another, which then burns a claim on an empty
185		// queue while its own item waits.
186		let registry = QueueWakeRegistry::new();
187
188		let mine = park(&registry, QueueId(1));
189		let theirs = park(&registry, QueueId(2));
190
191		registry.nudge(QueueId(1), 4);
192
193		assert!(mine.wait_timeout(zero()));
194		assert!(!theirs.wait_timeout(zero()), "a nudge must not cross queue boundaries");
195		assert_eq!(registry.parked(QueueId(2)), 1);
196	}
197
198	#[test]
199	fn test_nudging_an_unknown_queue_does_nothing() {
200		let registry = QueueWakeRegistry::new();
201
202		registry.nudge(QueueId(42), 3);
203		registry.nudge_all(QueueId(42));
204
205		assert_eq!(registry.parked(QueueId(42)), 0);
206	}
207}