1use std::cell::UnsafeCell;
37use std::fs::{File, OpenOptions};
38use std::path::Path;
39use std::sync::atomic::{AtomicU64, Ordering};
40use std::sync::Arc;
41
42use memmap2::{MmapMut, MmapOptions};
43
44use crate::replay_positions::SubscriberPosition;
45
46pub const PUBSUB_PAYLOAD_BYTES: usize = 56;
50
51const PUBSUB_SLOT_SIZE: usize = 64; const PUBSUB_MAGIC: u64 = 0xE7_E7_E7_E7_50_55_42_53; #[repr(C, align(64))]
55struct PubSubHeader {
56 magic: u64,
57 capacity: u64,
58 slot_size: u64,
59 _pad_meta: [u8; 64 - 24],
60 head: AtomicU64,
61 _pad_head: [u8; 64 - 8],
62}
63
64#[repr(C, align(64))]
65struct PubSubSlot {
66 sequence: AtomicU64,
67 payload: UnsafeCell<[u8; PUBSUB_PAYLOAD_BYTES]>,
68}
69
70pub struct PubSubRing {
73 _backing: PubSubBacking,
74 raw_ptr: *mut u8,
75 capacity: usize,
76}
77
78unsafe impl Send for PubSubRing {}
79unsafe impl Sync for PubSubRing {}
80
81#[allow(dead_code)]
87enum PubSubBacking {
88 Anon(MmapMut),
90 File(File, MmapMut),
92 Shm(crate::shm_file::ShmFile),
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum PubSubReadError {
100 Pending,
102 Lost,
105}
106
107pub const fn pubsub_ring_file_size(capacity: usize) -> usize {
108 std::mem::size_of::<PubSubHeader>() + capacity * PUBSUB_SLOT_SIZE
109}
110
111impl PubSubRing {
112 pub fn create_anon(capacity: usize) -> std::io::Result<Self> {
114 assert!(capacity.is_power_of_two() && capacity >= 2,
115 "capacity must be pow2 >= 2");
116 let total = pubsub_ring_file_size(capacity);
117 let mut mmap = MmapOptions::new().len(total).map_anon()?;
118 let raw_ptr = mmap.as_mut_ptr();
119 init_pubsub_layout(raw_ptr, capacity);
120 Ok(Self {
121 _backing: PubSubBacking::Anon(mmap),
122 raw_ptr, capacity,
123 })
124 }
125
126 pub fn create(path: impl AsRef<Path>, capacity: usize) -> std::io::Result<Self> {
129 assert!(capacity.is_power_of_two() && capacity >= 2,
130 "capacity must be pow2 >= 2");
131 let total = pubsub_ring_file_size(capacity);
132 let file = OpenOptions::new()
133 .read(true).write(true).create(true).truncate(true)
134 .open(path.as_ref())?;
135 file.set_len(total as u64)?;
136 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
137 let raw_ptr = mmap.as_mut_ptr();
138 init_pubsub_layout(raw_ptr, capacity);
139 Ok(Self {
140 _backing: PubSubBacking::File(file, mmap),
141 raw_ptr, capacity,
142 })
143 }
144
145 pub fn open(path: impl AsRef<Path>, expected_capacity: usize) -> std::io::Result<Self> {
148 let total = pubsub_ring_file_size(expected_capacity);
149 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
150 if (file.metadata()?.len() as usize) < total {
151 return Err(std::io::Error::new(
152 std::io::ErrorKind::InvalidData,
153 "pubsub file too small for expected capacity",
154 ));
155 }
156 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
157 let raw_ptr = mmap.as_mut_ptr();
158 let header = unsafe { &*(raw_ptr as *const PubSubHeader) };
159 if header.magic != PUBSUB_MAGIC
160 || header.capacity != expected_capacity as u64
161 || header.slot_size != PUBSUB_SLOT_SIZE as u64
162 {
163 return Err(std::io::Error::new(
164 std::io::ErrorKind::InvalidData,
165 "pubsub file layout mismatch",
166 ));
167 }
168 Ok(Self {
169 _backing: PubSubBacking::File(file, mmap),
170 raw_ptr, capacity: expected_capacity,
171 })
172 }
173
174 pub fn create_from_shm(
177 mut shm: crate::shm_file::ShmFile,
178 capacity: usize,
179 ) -> std::io::Result<Self> {
180 assert!(capacity.is_power_of_two() && capacity >= 2,
181 "capacity must be pow2 >= 2");
182 let total = pubsub_ring_file_size(capacity);
183 if shm.len() < total {
184 return Err(std::io::Error::new(
185 std::io::ErrorKind::InvalidData,
186 "shm region too small for pubsub layout",
187 ));
188 }
189 let raw_ptr = shm.as_mut_slice().as_mut_ptr();
190 init_pubsub_layout(raw_ptr, capacity);
191 Ok(Self {
192 _backing: PubSubBacking::Shm(shm),
193 raw_ptr, capacity,
194 })
195 }
196
197 pub fn open_from_shm(
199 mut shm: crate::shm_file::ShmFile,
200 expected_capacity: usize,
201 ) -> std::io::Result<Self> {
202 let total = pubsub_ring_file_size(expected_capacity);
203 if shm.len() < total {
204 return Err(std::io::Error::new(
205 std::io::ErrorKind::InvalidData,
206 "shm region too small for expected capacity",
207 ));
208 }
209 let raw_ptr = shm.as_mut_slice().as_mut_ptr();
210 let header = unsafe { &*(raw_ptr as *const PubSubHeader) };
211 if header.magic != PUBSUB_MAGIC
212 || header.capacity != expected_capacity as u64
213 || header.slot_size != PUBSUB_SLOT_SIZE as u64
214 {
215 return Err(std::io::Error::new(
216 std::io::ErrorKind::InvalidData,
217 "shm layout mismatch",
218 ));
219 }
220 Ok(Self {
221 _backing: PubSubBacking::Shm(shm),
222 raw_ptr, capacity: expected_capacity,
223 })
224 }
225
226 fn header(&self) -> &PubSubHeader {
227 unsafe { &*(self.raw_ptr as *const PubSubHeader) }
228 }
229
230 fn slot(&self, idx: usize) -> &PubSubSlot {
231 let slots_base = unsafe {
232 self.raw_ptr.add(std::mem::size_of::<PubSubHeader>())
233 };
234 let masked = idx & (self.capacity - 1);
235 unsafe { &*(slots_base.add(masked * PUBSUB_SLOT_SIZE) as *const PubSubSlot) }
236 }
237
238 pub fn head(&self) -> u64 {
241 self.header().head.load(Ordering::Acquire)
242 }
243
244 pub fn capacity(&self) -> usize { self.capacity }
246
247 pub fn publish(&self, payload: &[u8]) -> u64 {
250 assert!(payload.len() <= PUBSUB_PAYLOAD_BYTES);
251 let header = self.header();
252 let head = header.head.load(Ordering::Relaxed);
253 let slot = self.slot(head as usize);
254 unsafe {
256 let dst = (*slot.payload.get()).as_mut_ptr();
257 std::ptr::copy_nonoverlapping(payload.as_ptr(), dst, payload.len());
258 if payload.len() < PUBSUB_PAYLOAD_BYTES {
259 std::ptr::write_bytes(
260 dst.add(payload.len()), 0,
261 PUBSUB_PAYLOAD_BYTES - payload.len(),
262 );
263 }
264 }
265 slot.sequence.store(head + 1, Ordering::Release);
268 header.head.store(head + 1, Ordering::Release);
271 head
272 }
273
274 pub fn read_at(
281 &self,
282 position: u64,
283 out: &mut [u8],
284 ) -> Result<(), PubSubReadError> {
285 assert!(out.len() >= PUBSUB_PAYLOAD_BYTES);
286 let slot = self.slot(position as usize);
287 let observed_seq = slot.sequence.load(Ordering::Acquire);
288 let expected_seq = position + 1;
289 if observed_seq == expected_seq {
290 unsafe {
291 let src = (*slot.payload.get()).as_ptr();
292 std::ptr::copy_nonoverlapping(
293 src, out.as_mut_ptr(), PUBSUB_PAYLOAD_BYTES,
294 );
295 }
296 Ok(())
297 } else if observed_seq > expected_seq {
298 Err(PubSubReadError::Lost)
299 } else {
300 Err(PubSubReadError::Pending)
301 }
302 }
303}
304
305pub struct PubSubSubscriber {
308 ring: Arc<PubSubRing>,
309 position: SubscriberPosition,
310}
311
312impl PubSubSubscriber {
313 pub fn new(ring: Arc<PubSubRing>, position: SubscriberPosition) -> Self {
315 Self { ring, position }
316 }
317
318 pub fn position(&self) -> u64 { self.position.get() }
320
321 pub fn ring(&self) -> &Arc<PubSubRing> { &self.ring }
323
324 pub fn skip(&self, n: u64) -> u64 {
328 self.position.advance(n)
329 }
330
331 pub fn try_next(&self, out: &mut [u8]) -> Result<(), PubSubReadError> {
336 let pos = self.position.get();
337 match self.ring.read_at(pos, out) {
338 Ok(()) => {
339 self.position.advance(1);
340 Ok(())
341 }
342 Err(PubSubReadError::Lost) => {
343 self.position.set(self.ring.head());
345 Err(PubSubReadError::Lost)
346 }
347 Err(other) => Err(other),
348 }
349 }
350}
351
352fn init_pubsub_layout(ptr: *mut u8, capacity: usize) {
353 let header_ptr = ptr as *mut PubSubHeader;
354 unsafe {
355 std::ptr::write(header_ptr, PubSubHeader {
356 magic: PUBSUB_MAGIC,
357 capacity: capacity as u64,
358 slot_size: PUBSUB_SLOT_SIZE as u64,
359 _pad_meta: [0; 64 - 24],
360 head: AtomicU64::new(0),
361 _pad_head: [0; 64 - 8],
362 });
363 }
364 let slots_base = unsafe { ptr.add(std::mem::size_of::<PubSubHeader>()) };
365 for i in 0..capacity {
366 let slot_ptr = unsafe { slots_base.add(i * PUBSUB_SLOT_SIZE) as *mut PubSubSlot };
367 unsafe {
368 std::ptr::write(slot_ptr, PubSubSlot {
369 sequence: AtomicU64::new(0),
370 payload: UnsafeCell::new([0; PUBSUB_PAYLOAD_BYTES]),
371 });
372 }
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 fn tmp_pos(name: &str) -> std::path::PathBuf {
381 let mut p = std::env::temp_dir();
382 let pid = std::process::id();
383 let nonce = std::time::SystemTime::now()
384 .duration_since(std::time::UNIX_EPOCH)
385 .map(|d| d.as_nanos())
386 .unwrap_or(0);
387 p.push(format!("pubsub_pos_{pid}_{nonce}_{name}.bin"));
388 p
389 }
390
391 #[test]
392 fn publish_then_read_at() {
393 let ring = PubSubRing::create_anon(8).expect("create");
394 let payload = [0xABu8; PUBSUB_PAYLOAD_BYTES];
395 let pos = ring.publish(&payload);
396 assert_eq!(pos, 0);
397 assert_eq!(ring.head(), 1);
398
399 let mut out = [0u8; PUBSUB_PAYLOAD_BYTES];
400 ring.read_at(0, &mut out).expect("read at 0");
401 assert_eq!(out, payload);
402 }
403
404 #[test]
405 fn read_pending_for_unpublished_position() {
406 let ring = PubSubRing::create_anon(8).expect("create");
407 let mut out = [0u8; PUBSUB_PAYLOAD_BYTES];
408 assert_eq!(ring.read_at(0, &mut out), Err(PubSubReadError::Pending));
409 }
410
411 #[test]
412 fn read_lost_for_overwritten_position() {
413 let ring = PubSubRing::create_anon(4).expect("create");
414 for i in 0u64..8 {
417 let mut payload = [0u8; PUBSUB_PAYLOAD_BYTES];
418 payload[..8].copy_from_slice(&i.to_le_bytes());
419 ring.publish(&payload);
420 }
421 let mut out = [0u8; PUBSUB_PAYLOAD_BYTES];
422 assert_eq!(ring.read_at(0, &mut out), Err(PubSubReadError::Lost));
425 ring.read_at(7, &mut out).expect("read at 7");
427 assert_eq!(&out[..8], &7u64.to_le_bytes());
428 }
429
430 #[test]
431 fn two_subscribers_independent_positions() {
432 let ring = Arc::new(PubSubRing::create_anon(16).expect("create"));
433 for i in 0u64..5 {
434 let mut payload = [0u8; PUBSUB_PAYLOAD_BYTES];
435 payload[..8].copy_from_slice(&i.to_le_bytes());
436 ring.publish(&payload);
437 }
438
439 let pos_a = SubscriberPosition::create(tmp_pos("sub_a"), 0).expect("pos a");
440 let pos_b = SubscriberPosition::create(tmp_pos("sub_b"), 0).expect("pos b");
441 let sub_a = PubSubSubscriber::new(ring.clone(), pos_a);
442 let sub_b = PubSubSubscriber::new(ring.clone(), pos_b);
443
444 let mut buf = [0u8; PUBSUB_PAYLOAD_BYTES];
446 sub_a.try_next(&mut buf).expect("a 0"); assert_eq!(&buf[..8], &0u64.to_le_bytes());
447 sub_a.try_next(&mut buf).expect("a 1"); assert_eq!(&buf[..8], &1u64.to_le_bytes());
448 sub_b.try_next(&mut buf).expect("b 0"); assert_eq!(&buf[..8], &0u64.to_le_bytes());
450 assert_eq!(sub_a.position(), 2);
451 assert_eq!(sub_b.position(), 1);
452 }
453
454 #[test]
455 fn subscriber_skips_past_lost_items() {
456 let ring = Arc::new(PubSubRing::create_anon(4).expect("create"));
457 let pos = SubscriberPosition::create(tmp_pos("lost"), 0).expect("pos");
458 let sub = PubSubSubscriber::new(ring.clone(), pos);
459
460 for i in 0u64..8 {
462 let mut payload = [0u8; PUBSUB_PAYLOAD_BYTES];
463 payload[..8].copy_from_slice(&i.to_le_bytes());
464 ring.publish(&payload);
465 }
466
467 let mut buf = [0u8; PUBSUB_PAYLOAD_BYTES];
469 assert_eq!(sub.try_next(&mut buf), Err(PubSubReadError::Lost));
470 assert_eq!(sub.position(), 8);
472 assert_eq!(sub.try_next(&mut buf), Err(PubSubReadError::Pending));
474 }
475}