1use crate::wire::encode_frame;
23use kevy_resp::ArgvView;
24#[cfg(test)]
25use kevy_resp::Argv;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Frame {
30 pub offset: u64,
32 pub bytes: Vec<u8>,
34}
35
36#[derive(Debug, PartialEq, Eq)]
39pub enum FromOffset {
40 TooOld,
43 Future,
47}
48
49pub struct ReplicationSource {
51 next_offset: u64,
52 bytes_in_buf: usize,
53 max_bytes: usize,
54 buf: std::collections::VecDeque<Frame>,
55}
56
57impl ReplicationSource {
58 pub fn new(max_bytes: usize) -> Self {
68 assert!(max_bytes > 0, "ReplicationSource max_bytes must be > 0");
69 Self {
70 next_offset: 0,
71 bytes_in_buf: 0,
72 max_bytes,
73 buf: std::collections::VecDeque::new(),
74 }
75 }
76
77 pub fn set_next_offset(&mut self, next: u64) {
86 assert!(self.buf.is_empty(), "set_next_offset on non-empty backlog");
87 self.next_offset = next;
88 }
89
90 pub fn max_bytes(&self) -> usize {
92 self.max_bytes
93 }
94
95 pub fn next_offset(&self) -> u64 {
98 self.next_offset
99 }
100
101 pub fn oldest_offset(&self) -> Option<u64> {
103 self.buf.front().map(|f| f.offset)
104 }
105
106 pub fn newest_offset(&self) -> Option<u64> {
108 self.buf.back().map(|f| f.offset)
109 }
110
111 pub fn buffered_bytes(&self) -> usize {
113 self.bytes_in_buf
114 }
115
116 pub fn len(&self) -> usize {
118 self.buf.len()
119 }
120
121 pub fn is_empty(&self) -> bool {
123 self.buf.is_empty()
124 }
125
126 #[allow(clippy::missing_panics_doc)]
138 pub fn push_mutation<A: ArgvView + ?Sized>(&mut self, argv: &A) -> u64 {
139 let offset = self.next_offset;
140 let bytes = encode_frame(offset, argv);
141 let frame_len = bytes.len();
142
143 while self.bytes_in_buf + frame_len > self.max_bytes && !self.buf.is_empty() {
146 let dropped = self.buf.pop_front().expect("non-empty checked");
147 self.bytes_in_buf -= dropped.bytes.len();
148 }
149
150 self.bytes_in_buf += frame_len;
151 self.buf.push_back(Frame { offset, bytes });
152 self.next_offset = self
153 .next_offset
154 .checked_add(1)
155 .expect("replication offset wrap — i64::MAX guard tripped");
156 offset
157 }
158
159 #[allow(clippy::missing_panics_doc)]
171 pub fn drop_up_to(&mut self, watermark: u64) {
172 while let Some(front) = self.buf.front() {
173 if front.offset >= watermark {
174 break;
175 }
176 let dropped = self.buf.pop_front().expect("front-of-loop");
177 self.bytes_in_buf -= dropped.bytes.len();
178 }
179 }
180
181 pub fn frames_from(&self, from: u64) -> Result<FramesIter<'_>, FromOffset> {
191 if from > self.next_offset {
192 return Err(FromOffset::Future);
193 }
194 if from == self.next_offset {
196 return Ok(FramesIter {
197 buf: &self.buf,
198 cursor: self.buf.len(),
199 });
200 }
201 match self.oldest_offset() {
208 Some(oldest) if from < oldest => return Err(FromOffset::TooOld),
209 None => return Err(FromOffset::TooOld),
210 _ => {}
211 }
212 let start = self.buf.iter().position(|f| f.offset >= from);
215 Ok(FramesIter {
216 buf: &self.buf,
217 cursor: start.unwrap_or(self.buf.len()),
218 })
219 }
220}
221
222pub struct FramesIter<'a> {
224 buf: &'a std::collections::VecDeque<Frame>,
225 cursor: usize,
226}
227
228impl<'a> Iterator for FramesIter<'a> {
229 type Item = &'a Frame;
230 fn next(&mut self) -> Option<&'a Frame> {
231 let item = self.buf.get(self.cursor)?;
232 self.cursor += 1;
233 Some(item)
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::wire::decode_frame;
241
242 fn argv(args: &[&[u8]]) -> Argv {
243 let mut a = Argv::default();
244 for arg in args {
245 a.push(arg);
246 }
247 a
248 }
249
250 #[test]
251 fn fresh_source_is_empty() {
252 let s = ReplicationSource::new(1024);
253 assert!(s.is_empty());
254 assert_eq!(s.len(), 0);
255 assert_eq!(s.next_offset(), 0);
256 assert_eq!(s.oldest_offset(), None);
257 assert_eq!(s.newest_offset(), None);
258 assert_eq!(s.buffered_bytes(), 0);
259 }
260
261 #[test]
262 fn push_assigns_monotonic_offsets() {
263 let mut s = ReplicationSource::new(64 * 1024);
264 let o0 = s.push_mutation(&argv(&[b"SET", b"a", b"1"]));
265 let o1 = s.push_mutation(&argv(&[b"SET", b"b", b"2"]));
266 let o2 = s.push_mutation(&argv(&[b"DEL", b"a"]));
267 assert_eq!((o0, o1, o2), (0, 1, 2));
268 assert_eq!(s.oldest_offset(), Some(0));
269 assert_eq!(s.newest_offset(), Some(2));
270 assert_eq!(s.next_offset(), 3);
271 assert_eq!(s.len(), 3);
272 }
273
274 #[test]
275 fn pushed_frames_decode_back_to_the_pushed_argv() {
276 let mut s = ReplicationSource::new(1024);
277 let a = argv(&[b"HSET", b"h", b"f", b"v"]);
278 let off = s.push_mutation(&a);
279 let frame = s.buf.front().expect("one frame");
280 assert_eq!(frame.offset, off);
281 let (decoded_off, decoded_argv, used) = decode_frame(&frame.bytes).expect("decode");
282 assert_eq!(decoded_off, off);
283 assert_eq!(decoded_argv, a);
284 assert_eq!(used, frame.bytes.len());
285 }
286
287 #[test]
288 fn eviction_drops_oldest_when_budget_exceeded() {
289 let mut s = ReplicationSource::new(80);
292 let _ = s.push_mutation(&argv(&[b"SET", b"a", b"1"]));
293 let _ = s.push_mutation(&argv(&[b"SET", b"b", b"2"]));
294 assert_eq!(s.oldest_offset(), Some(0));
295 let _ = s.push_mutation(&argv(&[b"SET", b"c", b"3"]));
296 assert_eq!(s.oldest_offset(), Some(1));
297 assert_eq!(s.newest_offset(), Some(2));
298 assert!(s.buffered_bytes() <= 80);
299 assert_eq!(s.next_offset(), 3);
301 }
302
303 #[test]
304 fn oversized_single_frame_is_retained_against_budget() {
305 let mut s = ReplicationSource::new(8);
309 let off = s.push_mutation(&argv(&[b"SET", b"k", b"v"]));
310 assert_eq!(s.len(), 1);
311 assert_eq!(s.oldest_offset(), Some(off));
312 assert!(s.buffered_bytes() > 8); let off2 = s.push_mutation(&argv(&[b"DEL", b"k"]));
315 assert_eq!(s.len(), 1);
316 assert_eq!(s.oldest_offset(), Some(off2));
317 }
318
319 #[test]
320 fn frames_from_at_exact_offset_returns_that_frame_first() {
321 let mut s = ReplicationSource::new(1024);
322 for i in 0..5 {
323 let _ = s.push_mutation(&argv(&[b"SET", b"k", format!("{i}").as_bytes()]));
324 }
325 let mut it = s.frames_from(2).unwrap();
326 let f = it.next().expect("frame");
327 assert_eq!(f.offset, 2);
328 let remaining: Vec<u64> = it.map(|f| f.offset).collect();
329 assert_eq!(remaining, vec![3, 4]);
330 }
331
332 #[test]
333 fn frames_from_at_next_offset_is_empty_caught_up() {
334 let mut s = ReplicationSource::new(1024);
335 let _ = s.push_mutation(&argv(&[b"PING"]));
336 let _ = s.push_mutation(&argv(&[b"PING"]));
337 let it = s.frames_from(s.next_offset()).unwrap();
338 assert_eq!(it.count(), 0);
339 }
340
341 #[test]
342 fn frames_from_too_old_after_eviction() {
343 let mut s = ReplicationSource::new(80);
345 for _ in 0..5 {
346 let _ = s.push_mutation(&argv(&[b"SET", b"k", b"v"]));
347 }
348 assert!(s.oldest_offset().unwrap() > 0);
350 assert!(matches!(s.frames_from(0), Err(FromOffset::TooOld)));
351 }
352
353 #[test]
354 fn frames_from_future_offset_rejected() {
355 let mut s = ReplicationSource::new(1024);
356 let _ = s.push_mutation(&argv(&[b"PING"]));
357 assert!(matches!(s.frames_from(2), Err(FromOffset::Future)));
359 }
360
361 #[test]
362 fn frames_from_empty_source_at_zero_is_caught_up_not_too_old() {
363 let s = ReplicationSource::new(1024);
367 assert_eq!(s.frames_from(0).unwrap().count(), 0);
368 assert!(matches!(s.frames_from(1), Err(FromOffset::Future)));
370 }
371
372 #[test]
373 fn push_mutation_accepts_argv_borrowed_from_dispatcher_hot_path() {
374 let resp = b"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n";
381 let (borrowed, consumed) = kevy_resp::parse_command_borrowed(resp)
382 .expect("parse ok")
383 .expect("complete frame");
384 assert_eq!(consumed, resp.len());
385
386 let mut s = ReplicationSource::new(1024);
387 let off = s.push_mutation(&borrowed);
388 assert_eq!(off, 0);
389
390 let frame = s.buf.front().expect("one frame");
391 let (decoded_off, decoded_argv, _) =
392 crate::wire::decode_frame(&frame.bytes).expect("decode");
393 assert_eq!(decoded_off, 0);
394 assert_eq!(decoded_argv, argv(&[b"SET", b"foo", b"bar"]));
395 }
396
397 #[test]
398 fn buffered_bytes_tracks_actual_frame_total() {
399 let mut s = ReplicationSource::new(1024);
400 let _ = s.push_mutation(&argv(&[b"SET", b"k", b"v"]));
401 let _ = s.push_mutation(&argv(&[b"DEL", b"k"]));
402 let actual: usize = s.buf.iter().map(|f| f.bytes.len()).sum();
403 assert_eq!(s.buffered_bytes(), actual);
404 }
405
406 #[test]
407 fn drop_up_to_evicts_below_watermark() {
408 let mut s = ReplicationSource::new(64 * 1024);
410 for i in 0..5 {
411 let v = format!("v{i}");
412 let _ = s.push_mutation(&argv(&[b"SET", b"k", v.as_bytes()]));
413 }
414 assert_eq!(s.len(), 5);
415 let bytes_before = s.buffered_bytes();
416 s.drop_up_to(3);
418 assert_eq!(s.len(), 2);
419 assert_eq!(s.oldest_offset(), Some(3));
420 assert_eq!(s.newest_offset(), Some(4));
421 assert!(s.buffered_bytes() < bytes_before);
423 let kept: Vec<_> = s.frames_from(3).unwrap().collect();
425 assert_eq!(kept.len(), 2);
426 }
427
428 #[test]
429 fn drop_up_to_below_oldest_is_noop() {
430 let mut s = ReplicationSource::new(64 * 1024);
431 let _ = s.push_mutation(&argv(&[b"SET", b"k", b"v"]));
432 let _ = s.push_mutation(&argv(&[b"SET", b"k", b"v"]));
433 assert_eq!(s.oldest_offset(), Some(0));
434 s.drop_up_to(0); assert_eq!(s.len(), 2);
436 }
437
438 #[test]
439 fn drop_up_to_at_or_past_newest_drops_everything() {
440 let mut s = ReplicationSource::new(64 * 1024);
441 for _ in 0..3 {
442 let _ = s.push_mutation(&argv(&[b"SET", b"k", b"v"]));
443 }
444 s.drop_up_to(99);
445 assert!(s.is_empty());
446 assert_eq!(s.buffered_bytes(), 0);
447 }
448}