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> {
133 assert!(capacity.is_power_of_two() && capacity >= 2,
134 "capacity must be pow2 >= 2");
135 let total = pubsub_ring_file_size(capacity);
136 let (file, mut mmap) = crate::mmf_attach::create_or_attach(
137 path.as_ref(),
138 total,
139 |ptr| init_pubsub_layout(ptr, capacity),
140 |ptr| unsafe { (*(ptr as *const PubSubHeader)).magic == PUBSUB_MAGIC },
141 )?;
142 let raw_ptr = mmap.as_mut_ptr();
143 let header = unsafe { &*(raw_ptr as *const PubSubHeader) };
144 if header.magic != PUBSUB_MAGIC
145 || header.capacity != capacity as u64
146 || header.slot_size != PUBSUB_SLOT_SIZE as u64
147 {
148 return Err(std::io::Error::new(
149 std::io::ErrorKind::InvalidData,
150 "pubsub file layout mismatch",
151 ));
152 }
153 Ok(Self {
154 _backing: PubSubBacking::File(file, mmap),
155 raw_ptr, capacity,
156 })
157 }
158
159 pub fn reset(path: impl AsRef<Path>, capacity: usize) -> std::io::Result<Self> {
163 assert!(capacity.is_power_of_two() && capacity >= 2,
164 "capacity must be pow2 >= 2");
165 let total = pubsub_ring_file_size(capacity);
166 let (file, mut mmap) = crate::mmf_attach::reset(
167 path.as_ref(),
168 total,
169 |ptr| init_pubsub_layout(ptr, capacity),
170 )?;
171 let raw_ptr = mmap.as_mut_ptr();
172 Ok(Self {
173 _backing: PubSubBacking::File(file, mmap),
174 raw_ptr, capacity,
175 })
176 }
177
178 pub fn open(path: impl AsRef<Path>, expected_capacity: usize) -> std::io::Result<Self> {
181 let total = pubsub_ring_file_size(expected_capacity);
182 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
183 if (file.metadata()?.len() as usize) < total {
184 return Err(std::io::Error::new(
185 std::io::ErrorKind::InvalidData,
186 "pubsub file too small for expected capacity",
187 ));
188 }
189 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
190 let raw_ptr = mmap.as_mut_ptr();
191 let header = unsafe { &*(raw_ptr as *const PubSubHeader) };
192 if header.magic != PUBSUB_MAGIC
193 || header.capacity != expected_capacity as u64
194 || header.slot_size != PUBSUB_SLOT_SIZE as u64
195 {
196 return Err(std::io::Error::new(
197 std::io::ErrorKind::InvalidData,
198 "pubsub file layout mismatch",
199 ));
200 }
201 Ok(Self {
202 _backing: PubSubBacking::File(file, mmap),
203 raw_ptr, capacity: expected_capacity,
204 })
205 }
206
207 pub fn create_from_shm(
210 mut shm: crate::shm_file::ShmFile,
211 capacity: usize,
212 ) -> std::io::Result<Self> {
213 assert!(capacity.is_power_of_two() && capacity >= 2,
214 "capacity must be pow2 >= 2");
215 let total = pubsub_ring_file_size(capacity);
216 if shm.len() < total {
217 return Err(std::io::Error::new(
218 std::io::ErrorKind::InvalidData,
219 "shm region too small for pubsub layout",
220 ));
221 }
222 let raw_ptr = shm.as_mut_slice().as_mut_ptr();
223 init_pubsub_layout(raw_ptr, capacity);
224 Ok(Self {
225 _backing: PubSubBacking::Shm(shm),
226 raw_ptr, capacity,
227 })
228 }
229
230 pub fn open_from_shm(
232 mut shm: crate::shm_file::ShmFile,
233 expected_capacity: usize,
234 ) -> std::io::Result<Self> {
235 let total = pubsub_ring_file_size(expected_capacity);
236 if shm.len() < total {
237 return Err(std::io::Error::new(
238 std::io::ErrorKind::InvalidData,
239 "shm region too small for expected capacity",
240 ));
241 }
242 let raw_ptr = shm.as_mut_slice().as_mut_ptr();
243 let header = unsafe { &*(raw_ptr as *const PubSubHeader) };
244 if header.magic != PUBSUB_MAGIC
245 || header.capacity != expected_capacity as u64
246 || header.slot_size != PUBSUB_SLOT_SIZE as u64
247 {
248 return Err(std::io::Error::new(
249 std::io::ErrorKind::InvalidData,
250 "shm layout mismatch",
251 ));
252 }
253 Ok(Self {
254 _backing: PubSubBacking::Shm(shm),
255 raw_ptr, capacity: expected_capacity,
256 })
257 }
258
259 fn header(&self) -> &PubSubHeader {
260 unsafe { &*(self.raw_ptr as *const PubSubHeader) }
261 }
262
263 fn slot(&self, idx: usize) -> &PubSubSlot {
264 let slots_base = unsafe {
265 self.raw_ptr.add(std::mem::size_of::<PubSubHeader>())
266 };
267 let masked = idx & (self.capacity - 1);
268 unsafe { &*(slots_base.add(masked * PUBSUB_SLOT_SIZE) as *const PubSubSlot) }
269 }
270
271 pub fn head(&self) -> u64 {
274 self.header().head.load(Ordering::Acquire)
275 }
276
277 pub fn capacity(&self) -> usize { self.capacity }
279
280 pub fn publish(&self, payload: &[u8]) -> u64 {
283 assert!(payload.len() <= PUBSUB_PAYLOAD_BYTES);
284 let header = self.header();
285 let head = header.head.load(Ordering::Relaxed);
286 let slot = self.slot(head as usize);
287 unsafe {
289 let dst = (*slot.payload.get()).as_mut_ptr();
290 std::ptr::copy_nonoverlapping(payload.as_ptr(), dst, payload.len());
291 if payload.len() < PUBSUB_PAYLOAD_BYTES {
292 std::ptr::write_bytes(
293 dst.add(payload.len()), 0,
294 PUBSUB_PAYLOAD_BYTES - payload.len(),
295 );
296 }
297 }
298 slot.sequence.store(head + 1, Ordering::Release);
301 header.head.store(head + 1, Ordering::Release);
304 head
305 }
306
307 pub fn read_at(
314 &self,
315 position: u64,
316 out: &mut [u8],
317 ) -> Result<(), PubSubReadError> {
318 assert!(out.len() >= PUBSUB_PAYLOAD_BYTES);
319 let slot = self.slot(position as usize);
320 let observed_seq = slot.sequence.load(Ordering::Acquire);
321 let expected_seq = position + 1;
322 if observed_seq == expected_seq {
323 unsafe {
324 let src = (*slot.payload.get()).as_ptr();
325 std::ptr::copy_nonoverlapping(
326 src, out.as_mut_ptr(), PUBSUB_PAYLOAD_BYTES,
327 );
328 }
329 Ok(())
330 } else if observed_seq > expected_seq {
331 Err(PubSubReadError::Lost)
332 } else {
333 Err(PubSubReadError::Pending)
334 }
335 }
336}
337
338pub struct PubSubSubscriber {
341 ring: Arc<PubSubRing>,
342 position: SubscriberPosition,
343}
344
345impl PubSubSubscriber {
346 pub fn new(ring: Arc<PubSubRing>, position: SubscriberPosition) -> Self {
348 Self { ring, position }
349 }
350
351 pub fn position(&self) -> u64 { self.position.get() }
353
354 pub fn ring(&self) -> &Arc<PubSubRing> { &self.ring }
356
357 pub fn skip(&self, n: u64) -> u64 {
361 self.position.advance(n)
362 }
363
364 pub fn try_next(&self, out: &mut [u8]) -> Result<(), PubSubReadError> {
369 let pos = self.position.get();
370 match self.ring.read_at(pos, out) {
371 Ok(()) => {
372 self.position.advance(1);
373 Ok(())
374 }
375 Err(PubSubReadError::Lost) => {
376 self.position.set(self.ring.head());
378 Err(PubSubReadError::Lost)
379 }
380 Err(other) => Err(other),
381 }
382 }
383}
384
385fn init_pubsub_layout(ptr: *mut u8, capacity: usize) {
391 unsafe {
392 std::ptr::write_bytes(ptr, 0, pubsub_ring_file_size(capacity));
393 let header_ptr = ptr as *mut PubSubHeader;
394 (*header_ptr).capacity = capacity as u64;
395 (*header_ptr).slot_size = PUBSUB_SLOT_SIZE as u64;
396 std::ptr::write_volatile(&raw mut (*header_ptr).magic, PUBSUB_MAGIC);
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 fn tmp_pos(name: &str) -> std::path::PathBuf {
405 let mut p = std::env::temp_dir();
406 let pid = std::process::id();
407 let nonce = std::time::SystemTime::now()
408 .duration_since(std::time::UNIX_EPOCH)
409 .map(|d| d.as_nanos())
410 .unwrap_or(0);
411 p.push(format!("pubsub_pos_{pid}_{nonce}_{name}.bin"));
412 p
413 }
414
415 #[test]
416 fn publish_then_read_at() {
417 let ring = PubSubRing::create_anon(8).expect("create");
418 let payload = [0xABu8; PUBSUB_PAYLOAD_BYTES];
419 let pos = ring.publish(&payload);
420 assert_eq!(pos, 0);
421 assert_eq!(ring.head(), 1);
422
423 let mut out = [0u8; PUBSUB_PAYLOAD_BYTES];
424 ring.read_at(0, &mut out).expect("read at 0");
425 assert_eq!(out, payload);
426 }
427
428 #[test]
431 fn second_create_attaches_and_keeps_published() {
432 let p = tmp_pos("attach");
433 let ring = PubSubRing::create(&p, 8).expect("create");
434 let payload = [0x77u8; PUBSUB_PAYLOAD_BYTES];
435 ring.publish(&payload);
436
437 let ring2 = PubSubRing::create(&p, 8).expect("second create");
438 assert_eq!(ring2.head(), 1, "attach lost the head");
439 let mut out = [0u8; PUBSUB_PAYLOAD_BYTES];
440 ring2.read_at(0, &mut out).expect("read after attach");
441 assert_eq!(out, payload, "attach lost a published slot");
442 assert!(PubSubRing::create(&p, 4).is_err());
443
444 drop(ring);
447 drop(ring2);
448 let fresh = PubSubRing::reset(&p, 8).expect("reset");
449 assert_eq!(fresh.head(), 0, "reset kept the head");
450 assert_eq!(fresh.read_at(0, &mut out), Err(PubSubReadError::Pending),
451 "reset kept a published slot");
452 drop(fresh);
453 std::fs::remove_file(&p).ok();
454 }
455
456 #[test]
457 fn read_pending_for_unpublished_position() {
458 let ring = PubSubRing::create_anon(8).expect("create");
459 let mut out = [0u8; PUBSUB_PAYLOAD_BYTES];
460 assert_eq!(ring.read_at(0, &mut out), Err(PubSubReadError::Pending));
461 }
462
463 #[test]
464 fn read_lost_for_overwritten_position() {
465 let ring = PubSubRing::create_anon(4).expect("create");
466 for i in 0u64..8 {
469 let mut payload = [0u8; PUBSUB_PAYLOAD_BYTES];
470 payload[..8].copy_from_slice(&i.to_le_bytes());
471 ring.publish(&payload);
472 }
473 let mut out = [0u8; PUBSUB_PAYLOAD_BYTES];
474 assert_eq!(ring.read_at(0, &mut out), Err(PubSubReadError::Lost));
477 ring.read_at(7, &mut out).expect("read at 7");
479 assert_eq!(&out[..8], &7u64.to_le_bytes());
480 }
481
482 #[test]
483 fn two_subscribers_independent_positions() {
484 let ring = Arc::new(PubSubRing::create_anon(16).expect("create"));
485 for i in 0u64..5 {
486 let mut payload = [0u8; PUBSUB_PAYLOAD_BYTES];
487 payload[..8].copy_from_slice(&i.to_le_bytes());
488 ring.publish(&payload);
489 }
490
491 let pos_a = SubscriberPosition::create(tmp_pos("sub_a"), 0).expect("pos a");
492 let pos_b = SubscriberPosition::create(tmp_pos("sub_b"), 0).expect("pos b");
493 let sub_a = PubSubSubscriber::new(ring.clone(), pos_a);
494 let sub_b = PubSubSubscriber::new(ring.clone(), pos_b);
495
496 let mut buf = [0u8; PUBSUB_PAYLOAD_BYTES];
498 sub_a.try_next(&mut buf).expect("a 0"); assert_eq!(&buf[..8], &0u64.to_le_bytes());
499 sub_a.try_next(&mut buf).expect("a 1"); assert_eq!(&buf[..8], &1u64.to_le_bytes());
500 sub_b.try_next(&mut buf).expect("b 0"); assert_eq!(&buf[..8], &0u64.to_le_bytes());
502 assert_eq!(sub_a.position(), 2);
503 assert_eq!(sub_b.position(), 1);
504 }
505
506 #[test]
507 fn subscriber_skips_past_lost_items() {
508 let ring = Arc::new(PubSubRing::create_anon(4).expect("create"));
509 let pos = SubscriberPosition::create(tmp_pos("lost"), 0).expect("pos");
510 let sub = PubSubSubscriber::new(ring.clone(), pos);
511
512 for i in 0u64..8 {
514 let mut payload = [0u8; PUBSUB_PAYLOAD_BYTES];
515 payload[..8].copy_from_slice(&i.to_le_bytes());
516 ring.publish(&payload);
517 }
518
519 let mut buf = [0u8; PUBSUB_PAYLOAD_BYTES];
521 assert_eq!(sub.try_next(&mut buf), Err(PubSubReadError::Lost));
522 assert_eq!(sub.position(), 8);
524 assert_eq!(sub.try_next(&mut buf), Err(PubSubReadError::Pending));
526 }
527}