1use crate::wire::encode_frame;
23#[cfg(test)]
24use kevy_resp::Argv;
25use kevy_resp::ArgvView;
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
49#[derive(Debug)]
51pub struct ReplicationSource {
52 next_offset: u64,
53 bytes_in_buf: usize,
54 max_bytes: usize,
55 buf: std::collections::VecDeque<Frame>,
56}
57
58impl ReplicationSource {
59 pub fn new(max_bytes: usize) -> Self {
69 assert!(max_bytes > 0, "ReplicationSource max_bytes must be > 0");
70 Self { next_offset: 0, bytes_in_buf: 0, max_bytes, buf: std::collections::VecDeque::new() }
71 }
72
73 pub fn set_next_offset(&mut self, next: u64) {
82 assert!(self.buf.is_empty(), "set_next_offset on non-empty backlog");
83 self.next_offset = next;
84 }
85
86 pub fn max_bytes(&self) -> usize {
88 self.max_bytes
89 }
90
91 pub fn next_offset(&self) -> u64 {
94 self.next_offset
95 }
96
97 pub fn oldest_offset(&self) -> Option<u64> {
99 self.buf.front().map(|f| f.offset)
100 }
101
102 pub fn newest_offset(&self) -> Option<u64> {
104 self.buf.back().map(|f| f.offset)
105 }
106
107 pub fn buffered_bytes(&self) -> usize {
109 self.bytes_in_buf
110 }
111
112 pub fn len(&self) -> usize {
114 self.buf.len()
115 }
116
117 pub fn is_empty(&self) -> bool {
119 self.buf.is_empty()
120 }
121
122 #[allow(clippy::missing_panics_doc)]
134 pub fn push_mutation<A: ArgvView + ?Sized>(&mut self, argv: &A) -> u64 {
135 let offset = self.next_offset;
136 let bytes = encode_frame(offset, argv);
137 let frame_len = bytes.len();
138
139 while self.bytes_in_buf + frame_len > self.max_bytes && !self.buf.is_empty() {
142 let dropped = self.buf.pop_front().expect("non-empty checked");
143 self.bytes_in_buf -= dropped.bytes.len();
144 }
145
146 self.bytes_in_buf += frame_len;
147 self.buf.push_back(Frame { offset, bytes });
148 self.next_offset = self
149 .next_offset
150 .checked_add(1)
151 .expect("replication offset wrap — i64::MAX guard tripped");
152 offset
153 }
154
155 #[allow(clippy::missing_panics_doc)]
167 pub fn drop_up_to(&mut self, watermark: u64) {
168 while let Some(front) = self.buf.front() {
169 if front.offset >= watermark {
170 break;
171 }
172 let dropped = self.buf.pop_front().expect("front-of-loop");
173 self.bytes_in_buf -= dropped.bytes.len();
174 }
175 }
176
177 pub fn frames_from(&self, from: u64) -> Result<FramesIter<'_>, FromOffset> {
187 if from > self.next_offset {
188 return Err(FromOffset::Future);
189 }
190 if from == self.next_offset {
192 return Ok(FramesIter { buf: &self.buf, cursor: self.buf.len() });
193 }
194 match self.oldest_offset() {
201 Some(oldest) if from < oldest => return Err(FromOffset::TooOld),
202 None => return Err(FromOffset::TooOld),
203 _ => {}
204 }
205 let (a, b) = self.buf.as_slices();
215 let start = match a.partition_point(|f| f.offset < from) {
216 i if i < a.len() => i,
217 _ => a.len() + b.partition_point(|f| f.offset < from),
218 };
219 Ok(FramesIter { buf: &self.buf, cursor: start })
220 }
221}
222
223#[derive(Debug)]
225pub struct FramesIter<'a> {
226 buf: &'a std::collections::VecDeque<Frame>,
227 cursor: usize,
228}
229
230impl<'a> Iterator for FramesIter<'a> {
231 type Item = &'a Frame;
232 fn next(&mut self) -> Option<&'a Frame> {
233 let item = self.buf.get(self.cursor)?;
234 self.cursor += 1;
235 Some(item)
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::wire::decode_frame;
243
244 fn argv(args: &[&[u8]]) -> Argv {
245 let mut a = Argv::default();
246 for arg in args {
247 a.push(arg);
248 }
249 a
250 }
251
252 #[test]
253 fn fresh_source_is_empty() {
254 let s = ReplicationSource::new(1024);
255 assert!(s.is_empty());
256 assert_eq!(s.len(), 0);
257 assert_eq!(s.next_offset(), 0);
258 assert_eq!(s.oldest_offset(), None);
259 assert_eq!(s.newest_offset(), None);
260 assert_eq!(s.buffered_bytes(), 0);
261 }
262
263 #[test]
264 fn push_assigns_monotonic_offsets() {
265 let mut s = ReplicationSource::new(64 * 1024);
266 let o0 = s.push_mutation(&argv(&[b"SET", b"a", b"1"]));
267 let o1 = s.push_mutation(&argv(&[b"SET", b"b", b"2"]));
268 let o2 = s.push_mutation(&argv(&[b"DEL", b"a"]));
269 assert_eq!((o0, o1, o2), (0, 1, 2));
270 assert_eq!(s.oldest_offset(), Some(0));
271 assert_eq!(s.newest_offset(), Some(2));
272 assert_eq!(s.next_offset(), 3);
273 assert_eq!(s.len(), 3);
274 }
275
276 #[test]
277 fn pushed_frames_decode_back_to_the_pushed_argv() {
278 let mut s = ReplicationSource::new(1024);
279 let a = argv(&[b"HSET", b"h", b"f", b"v"]);
280 let off = s.push_mutation(&a);
281 let frame = s.buf.front().expect("one frame");
282 assert_eq!(frame.offset, off);
283 let (decoded_off, decoded_argv, used) = decode_frame(&frame.bytes).expect("decode");
284 assert_eq!(decoded_off, off);
285 assert_eq!(decoded_argv, a);
286 assert_eq!(used, frame.bytes.len());
287 }
288
289 #[test]
290 fn eviction_drops_oldest_when_budget_exceeded() {
291 let mut s = ReplicationSource::new(80);
294 let _ = s.push_mutation(&argv(&[b"SET", b"a", b"1"]));
295 let _ = s.push_mutation(&argv(&[b"SET", b"b", b"2"]));
296 assert_eq!(s.oldest_offset(), Some(0));
297 let _ = s.push_mutation(&argv(&[b"SET", b"c", b"3"]));
298 assert_eq!(s.oldest_offset(), Some(1));
299 assert_eq!(s.newest_offset(), Some(2));
300 assert!(s.buffered_bytes() <= 80);
301 assert_eq!(s.next_offset(), 3);
303 }
304
305 #[test]
306 fn oversized_single_frame_is_retained_against_budget() {
307 let mut s = ReplicationSource::new(8);
311 let off = s.push_mutation(&argv(&[b"SET", b"k", b"v"]));
312 assert_eq!(s.len(), 1);
313 assert_eq!(s.oldest_offset(), Some(off));
314 assert!(s.buffered_bytes() > 8); let off2 = s.push_mutation(&argv(&[b"DEL", b"k"]));
317 assert_eq!(s.len(), 1);
318 assert_eq!(s.oldest_offset(), Some(off2));
319 }
320
321 #[test]
322 fn frames_from_at_exact_offset_returns_that_frame_first() {
323 let mut s = ReplicationSource::new(1024);
324 for i in 0..5 {
325 let _ = s.push_mutation(&argv(&[b"SET", b"k", format!("{i}").as_bytes()]));
326 }
327 let mut it = s.frames_from(2).unwrap();
328 let f = it.next().expect("frame");
329 assert_eq!(f.offset, 2);
330 let remaining: Vec<u64> = it.map(|f| f.offset).collect();
331 assert_eq!(remaining, vec![3, 4]);
332 }
333
334 #[test]
335 fn frames_from_at_next_offset_is_empty_caught_up() {
336 let mut s = ReplicationSource::new(1024);
337 let _ = s.push_mutation(&argv(&[b"PING"]));
338 let _ = s.push_mutation(&argv(&[b"PING"]));
339 let it = s.frames_from(s.next_offset()).unwrap();
340 assert_eq!(it.count(), 0);
341 }
342
343 #[test]
344 fn frames_from_too_old_after_eviction() {
345 let mut s = ReplicationSource::new(80);
347 for _ in 0..5 {
348 let _ = s.push_mutation(&argv(&[b"SET", b"k", b"v"]));
349 }
350 assert!(s.oldest_offset().unwrap() > 0);
352 assert!(matches!(s.frames_from(0), Err(FromOffset::TooOld)));
353 }
354
355 #[test]
356 fn frames_from_future_offset_rejected() {
357 let mut s = ReplicationSource::new(1024);
358 let _ = s.push_mutation(&argv(&[b"PING"]));
359 assert!(matches!(s.frames_from(2), Err(FromOffset::Future)));
361 }
362
363 #[test]
364 fn frames_from_empty_source_at_zero_is_caught_up_not_too_old() {
365 let s = ReplicationSource::new(1024);
369 assert_eq!(s.frames_from(0).unwrap().count(), 0);
370 assert!(matches!(s.frames_from(1), Err(FromOffset::Future)));
372 }
373
374 #[test]
375 fn push_mutation_accepts_argv_borrowed_from_dispatcher_hot_path() {
376 let resp = b"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n";
383 let (borrowed, consumed) =
384 kevy_resp::parse_command_borrowed(resp).expect("parse ok").expect("complete frame");
385 assert_eq!(consumed, resp.len());
386
387 let mut s = ReplicationSource::new(1024);
388 let off = s.push_mutation(&borrowed);
389 assert_eq!(off, 0);
390
391 let frame = s.buf.front().expect("one frame");
392 let (decoded_off, decoded_argv, _) =
393 crate::wire::decode_frame(&frame.bytes).expect("decode");
394 assert_eq!(decoded_off, 0);
395 assert_eq!(decoded_argv, argv(&[b"SET", b"foo", b"bar"]));
396 }
397
398 #[test]
399 fn buffered_bytes_tracks_actual_frame_total() {
400 let mut s = ReplicationSource::new(1024);
401 let _ = s.push_mutation(&argv(&[b"SET", b"k", b"v"]));
402 let _ = s.push_mutation(&argv(&[b"DEL", b"k"]));
403 let actual: usize = s.buf.iter().map(|f| f.bytes.len()).sum();
404 assert_eq!(s.buffered_bytes(), actual);
405 }
406
407 #[test]
408 fn drop_up_to_evicts_below_watermark() {
409 let mut s = ReplicationSource::new(64 * 1024);
411 for i in 0..5 {
412 let v = format!("v{i}");
413 let _ = s.push_mutation(&argv(&[b"SET", b"k", v.as_bytes()]));
414 }
415 assert_eq!(s.len(), 5);
416 let bytes_before = s.buffered_bytes();
417 s.drop_up_to(3);
419 assert_eq!(s.len(), 2);
420 assert_eq!(s.oldest_offset(), Some(3));
421 assert_eq!(s.newest_offset(), Some(4));
422 assert!(s.buffered_bytes() < bytes_before);
424 let kept: Vec<_> = s.frames_from(3).unwrap().collect();
426 assert_eq!(kept.len(), 2);
427 }
428
429 #[test]
430 fn drop_up_to_below_oldest_is_noop() {
431 let mut s = ReplicationSource::new(64 * 1024);
432 let _ = s.push_mutation(&argv(&[b"SET", b"k", b"v"]));
433 let _ = s.push_mutation(&argv(&[b"SET", b"k", b"v"]));
434 assert_eq!(s.oldest_offset(), Some(0));
435 s.drop_up_to(0); assert_eq!(s.len(), 2);
437 }
438
439 #[test]
440 fn drop_up_to_at_or_past_newest_drops_everything() {
441 let mut s = ReplicationSource::new(64 * 1024);
442 for _ in 0..3 {
443 let _ = s.push_mutation(&argv(&[b"SET", b"k", b"v"]));
444 }
445 s.drop_up_to(99);
446 assert!(s.is_empty());
447 assert_eq!(s.buffered_bytes(), 0);
448 }
449}