reifydb_sub_server/subscription/
connection_batcher.rs1use std::sync::Arc;
5
6use indexmap::IndexMap;
7use reifydb_core::interface::catalog::id::SubscriptionId;
8use reifydb_runtime::sync::mutex::Mutex;
9use reifydb_value::value::duration::Duration;
10use tokio::{
11 pin, select,
12 sync::Notify,
13 time::{Instant, sleep},
14};
15
16pub type MergeFn<W> = Arc<dyn Fn(&mut W, W) + Send + Sync>;
17pub type EmitFn<W> = Arc<dyn Fn(Vec<W>) + Send + Sync>;
18
19struct Pending<W> {
20 item: W,
21 linger: Duration,
22 first_pending_at: Instant,
23}
24
25struct Inner<W> {
26 pending: Mutex<IndexMap<SubscriptionId, Pending<W>>>,
27 merge: MergeFn<W>,
28 emit: EmitFn<W>,
29 wake: Notify,
30}
31
32pub struct ConnectionBatcher<W> {
33 inner: Arc<Inner<W>>,
34}
35
36impl<W> Clone for ConnectionBatcher<W> {
37 fn clone(&self) -> Self {
38 Self {
39 inner: self.inner.clone(),
40 }
41 }
42}
43
44impl<W: Send + 'static> ConnectionBatcher<W> {
45 pub fn new(merge: MergeFn<W>, emit: EmitFn<W>) -> Self {
46 Self {
47 inner: Arc::new(Inner {
48 pending: Mutex::new(IndexMap::new()),
49 merge,
50 emit,
51 wake: Notify::new(),
52 }),
53 }
54 }
55
56 pub fn append(&self, subscription_id: SubscriptionId, item: W, linger: impl Into<Duration>) {
57 let linger = linger.into();
58 {
59 let mut pending = self.inner.pending.lock();
60 match pending.get_mut(&subscription_id) {
61 Some(existing) => (self.inner.merge)(&mut existing.item, item),
62 None => {
63 pending.insert(
64 subscription_id,
65 Pending {
66 item,
67 linger,
68 first_pending_at: Instant::now(),
69 },
70 );
71 }
72 }
73 }
74 self.inner.wake.notify_one();
75 }
76
77 pub fn flush_due(&self) {
78 let now = Instant::now();
79 self.emit_where(|p| Duration::from_std(now.duration_since(p.first_pending_at)) >= p.linger);
80 }
81
82 pub fn flush_all(&self) {
83 self.emit_where(|_| true);
84 }
85
86 fn emit_where(&self, due: impl Fn(&Pending<W>) -> bool) {
87 let items: Vec<W> = {
88 let mut pending = self.inner.pending.lock();
89 if pending.is_empty() {
90 return;
91 }
92 let keys: Vec<SubscriptionId> =
93 pending.iter().filter(|(_, p)| due(p)).map(|(k, _)| *k).collect();
94 if keys.is_empty() {
95 return;
96 }
97 keys.into_iter().filter_map(|k| pending.shift_remove(&k).map(|p| p.item)).collect()
98 };
99 if !items.is_empty() {
100 (self.inner.emit)(items);
101 }
102 }
103
104 fn next_deadline(&self) -> Option<Duration> {
105 let now = Instant::now();
106 let pending = self.inner.pending.lock();
107 pending.values()
108 .map(|p| p.linger.saturating_sub(Duration::from_std(now.duration_since(p.first_pending_at))))
109 .min()
110 }
111
112 pub async fn run(self) {
113 loop {
114 match self.next_deadline() {
115 None => self.inner.wake.notified().await,
116 Some(remaining) => {
117 let notified = self.inner.wake.notified();
118 pin!(notified);
119 select! {
120 _ = sleep(remaining.to_std()) => {}
121 _ = &mut notified => {}
122 }
123 }
124 }
125 self.flush_due();
126 }
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use std::sync::{
133 Arc,
134 atomic::{AtomicUsize, Ordering},
135 };
136
137 use reifydb_core::interface::catalog::id::SubscriptionId;
138 use reifydb_runtime::sync::mutex::Mutex;
139 use reifydb_value::value::duration::Duration;
140 use tokio::{spawn, time::sleep};
141
142 use super::{ConnectionBatcher, EmitFn, MergeFn};
143
144 fn collecting_emit(out: Arc<Mutex<Vec<Vec<u64>>>>) -> EmitFn<u64> {
145 Arc::new(move |items: Vec<u64>| out.lock().push(items))
146 }
147
148 fn sum_merge() -> MergeFn<u64> {
149 Arc::new(|existing: &mut u64, next: u64| *existing += next)
150 }
151
152 fn ms(n: u64) -> Duration {
153 Duration::from_milliseconds(n as i64).unwrap()
154 }
155
156 #[test]
157 fn flush_all_emits_one_batch_in_insertion_order() {
158 let out = Arc::new(Mutex::new(Vec::new()));
159 let batcher = ConnectionBatcher::new(sum_merge(), collecting_emit(out.clone()));
160
161 batcher.append(SubscriptionId(7), 1, ms(5));
162 batcher.append(SubscriptionId(3), 2, ms(5));
163 batcher.flush_all();
164
165 let batches = out.lock().clone();
166 assert_eq!(batches.len(), 1, "one flush emits exactly one batch");
167 assert_eq!(batches[0], vec![1, 2], "distinct subscriptions stay distinct items, in first-seen order");
168 }
169
170 #[test]
171 fn append_for_same_subscription_merges_into_one_item() {
172 let out = Arc::new(Mutex::new(Vec::new()));
173 let batcher = ConnectionBatcher::new(sum_merge(), collecting_emit(out.clone()));
174
175 batcher.append(SubscriptionId(7), 10, ms(5));
176 batcher.append(SubscriptionId(7), 5, ms(5));
177 batcher.append(SubscriptionId(7), 1, ms(5));
178 batcher.flush_all();
179
180 let batches = out.lock().clone();
181 assert_eq!(
182 batches[0],
183 vec![16],
184 "the same subscription collapses to one merged item, never a second item"
185 );
186 }
187
188 #[test]
189 fn empty_flush_does_not_emit() {
190 let out = Arc::new(Mutex::new(Vec::new()));
191 let batcher = ConnectionBatcher::new(sum_merge(), collecting_emit(out.clone()));
192
193 batcher.flush_all();
194
195 assert!(out.lock().is_empty(), "a flush with nothing pending must not emit an envelope");
196 }
197
198 #[test]
199 fn flush_due_holds_a_subscription_until_its_linger_elapses() {
200 let out = Arc::new(Mutex::new(Vec::new()));
201 let batcher = ConnectionBatcher::new(sum_merge(), collecting_emit(out.clone()));
202
203 batcher.append(SubscriptionId(7), 1, ms(3600_000));
204 batcher.flush_due();
205 assert!(out.lock().is_empty(), "a subscription is not due until its own linger has elapsed");
206
207 batcher.flush_all();
208 assert_eq!(out.lock()[0], vec![1], "flush_all ignores linger (teardown/shape path)");
209 }
210
211 #[tokio::test]
212 async fn run_loop_respects_per_subscription_linger_independently() {
213 let out = Arc::new(Mutex::new(Vec::new()));
214 let batcher = ConnectionBatcher::new(sum_merge(), collecting_emit(out.clone()));
215
216 let task = spawn(batcher.clone().run());
217 batcher.append(SubscriptionId(1), 1, ms(15));
218 batcher.append(SubscriptionId(2), 2, ms(150));
219
220 sleep(ms(60).to_std()).await;
221 assert_eq!(
222 out.lock().clone(),
223 vec![vec![1]],
224 "the short-linger subscription is delivered first, in its own envelope, while the long one waits"
225 );
226
227 sleep(ms(150).to_std()).await;
228 assert_eq!(
229 out.lock().clone(),
230 vec![vec![1], vec![2]],
231 "the long-linger subscription is delivered only after its own linger, in a separate envelope"
232 );
233
234 task.abort();
235 }
236
237 #[tokio::test]
238 async fn run_loop_coalesces_same_linger_subscriptions_into_one_envelope() {
239 let out = Arc::new(Mutex::new(Vec::new()));
240 let batcher = ConnectionBatcher::new(sum_merge(), collecting_emit(out.clone()));
241
242 let task = spawn(batcher.clone().run());
243 batcher.append(SubscriptionId(1), 1, ms(15));
244 batcher.append(SubscriptionId(2), 2, ms(15));
245
246 sleep(ms(60).to_std()).await;
247 assert_eq!(
248 out.lock().clone(),
249 vec![vec![1, 2]],
250 "subscriptions sharing a linger ride one envelope - the N-into-one case is just equal lingers"
251 );
252
253 task.abort();
254 }
255
256 #[tokio::test]
257 async fn run_loop_flushes_zero_linger_promptly() {
258 let out = Arc::new(Mutex::new(Vec::new()));
259 let batcher = ConnectionBatcher::new(sum_merge(), collecting_emit(out.clone()));
260
261 let task = spawn(batcher.clone().run());
262 batcher.append(SubscriptionId(1), 1, ms(0));
263
264 sleep(ms(30).to_std()).await;
265 assert_eq!(
266 out.lock().clone(),
267 vec![vec![1]],
268 "a zero-linger subscription is delivered with no added latency"
269 );
270
271 task.abort();
272 }
273
274 #[tokio::test]
275 async fn run_loop_lets_a_nearer_deadline_preempt_a_pending_sleep() {
276 let out = Arc::new(Mutex::new(Vec::new()));
277 let batcher = ConnectionBatcher::new(sum_merge(), collecting_emit(out.clone()));
278
279 let task = spawn(batcher.clone().run());
280 batcher.append(SubscriptionId(1), 1, ms(150));
281 sleep(ms(10).to_std()).await;
282 batcher.append(SubscriptionId(2), 2, ms(15));
283
284 sleep(ms(60).to_std()).await;
285 assert_eq!(
286 out.lock().clone(),
287 vec![vec![2]],
288 "a later-arriving nearer-deadline subscription preempts the long sleep and flushes first"
289 );
290
291 task.abort();
292 }
293
294 #[tokio::test]
295 async fn run_loop_emits_nothing_while_idle() {
296 let calls = Arc::new(AtomicUsize::new(0));
297 let calls_for_emit = calls.clone();
298 let emit: EmitFn<u64> = Arc::new(move |_| {
299 calls_for_emit.fetch_add(1, Ordering::SeqCst);
300 });
301 let batcher = ConnectionBatcher::new(sum_merge(), emit);
302
303 let task = spawn(batcher.clone().run());
304 sleep(ms(40).to_std()).await;
305 task.abort();
306
307 assert_eq!(calls.load(Ordering::SeqCst), 0, "an idle connection never wakes to emit");
308 }
309}