Skip to main content

cloudtoid_interprocess/
queue.rs

1use crate::{
2    platform::{Lease, Mapping, Signal},
3    Error, Options, Result,
4};
5use std::{
6    cell::UnsafeCell,
7    ptr,
8    sync::{
9        atomic::{AtomicI32, AtomicI64, AtomicU64, Ordering::*},
10        OnceLock,
11    },
12    time::{Duration, Instant},
13};
14
15/// Maximum number of concurrently connected publisher objects.
16pub const MAX_PUBLISHERS: usize = 2048;
17const SLOT_SIZE: usize = 128;
18const TABLE_OFFSET: usize = 256;
19pub(crate) const BUFFER_OFFSET: usize = TABLE_OFFSET + MAX_PUBLISHERS * SLOT_SIZE;
20const RECOVERY_MS: u64 = 10_000;
21
22fn now_ms() -> u64 {
23    static START: OnceLock<Instant> = OnceLock::new();
24    START.get_or_init(Instant::now).elapsed().as_millis() as u64
25}
26
27#[repr(C)]
28struct Header {
29    read: AtomicI64,
30    write: AtomicI64,
31    reader: AtomicI64,
32    notification: AtomicI32,
33    participant: AtomicI32,
34}
35
36struct Shared {
37    // Drop the semaphore before releasing the mapping's participant lifetime lock.
38    signal: Signal,
39    mapping: Mapping,
40    options: Options,
41    #[cfg(test)]
42    fail_notification: std::sync::atomic::AtomicBool,
43}
44
45impl Shared {
46    fn open(options: Options) -> Result<Self> {
47        options.validate()?;
48        let mapping = Mapping::open(&options)?;
49        let signal = Signal::open(&options.name)?;
50        Ok(Self {
51            signal,
52            mapping,
53            options,
54            #[cfg(test)]
55            fail_notification: std::sync::atomic::AtomicBool::new(false),
56        })
57    }
58
59    #[inline]
60    fn header(&self) -> &Header {
61        unsafe { &*self.mapping.ptr.as_ptr().cast() }
62    }
63    #[inline]
64    fn gate(&self) -> &AtomicI32 {
65        unsafe { &*self.mapping.ptr.as_ptr().add(128).cast() }
66    }
67    #[inline]
68    fn owner(&self, slot: usize) -> &AtomicI64 {
69        unsafe {
70            &*self
71                .mapping
72                .ptr
73                .as_ptr()
74                .add(TABLE_OFFSET + slot * SLOT_SIZE)
75                .cast()
76        }
77    }
78    #[inline]
79    fn active(&self, slot: usize) -> &AtomicI32 {
80        unsafe {
81            &*self
82                .mapping
83                .ptr
84                .as_ptr()
85                .add(TABLE_OFFSET + slot * SLOT_SIZE + 8)
86                .cast()
87        }
88    }
89
90    fn register(&self) -> Result<(i64, Lease)> {
91        let counter = &self.header().participant;
92        let id = loop {
93            let previous = counter.load(Acquire);
94            let next = previous.checked_add(1).ok_or(Error::Exhausted)?;
95            if counter
96                .compare_exchange(previous, next, SeqCst, Acquire)
97                .is_ok()
98            {
99                break next as i64;
100            }
101        };
102        Ok((id, Lease::new(&self.options, id)?))
103    }
104
105    fn slot(&self, id: i64) -> Result<usize> {
106        for pass in 0..2 {
107            for slot in 0..MAX_PUBLISHERS {
108                let owner = self.owner(slot).load(Acquire);
109                if owner != 0 && (pass == 0 || Lease::alive(&self.options, owner)) {
110                    continue;
111                }
112                if self
113                    .owner(slot)
114                    .compare_exchange(owner, id, SeqCst, Acquire)
115                    .is_ok()
116                {
117                    self.active(slot).swap(0, SeqCst);
118                    return Ok(slot);
119                }
120            }
121        }
122        Err(Error::PublisherLimit)
123    }
124
125    fn any_active(&self) -> bool {
126        // The gate is closed before this scan. A newly registered publisher cannot
127        // start writing after its slot has been inspected.
128        (0..MAX_PUBLISHERS).any(|slot| {
129            let owner = self.owner(slot).load(Acquire);
130            owner != 0 && self.active(slot).load(Acquire) != 0 && Lease::alive(&self.options, owner)
131        })
132    }
133
134    #[inline]
135    fn pointer(&self, offset: i64) -> *mut u8 {
136        debug_assert!(offset >= 0);
137        unsafe {
138            self.mapping
139                .ptr
140                .as_ptr()
141                .add(BUFFER_OFFSET + offset as usize % self.options.capacity)
142        }
143    }
144
145    #[inline]
146    fn state(&self, offset: i64) -> &AtomicI32 {
147        unsafe { &*self.pointer(offset).cast() }
148    }
149
150    unsafe fn write(&self, offset: i64, source: &[u8]) {
151        let right = source
152            .len()
153            .min(self.options.capacity - offset as usize % self.options.capacity);
154        ptr::copy_nonoverlapping(source.as_ptr(), self.pointer(offset), right);
155        ptr::copy_nonoverlapping(
156            source.as_ptr().add(right),
157            self.pointer(0),
158            source.len() - right,
159        );
160    }
161
162    // `target` must be writable for `length` bytes and must not overlap the mapping.
163    unsafe fn read(&self, offset: i64, target: *mut u8, length: usize) {
164        let right = length.min(self.options.capacity - offset as usize % self.options.capacity);
165        ptr::copy_nonoverlapping(self.pointer(offset), target, right);
166        ptr::copy_nonoverlapping(self.pointer(0), target.add(right), length - right);
167    }
168
169    unsafe fn clear(&self, offset: i64, length: usize) {
170        debug_assert!(length <= self.options.capacity);
171        let right = length.min(self.options.capacity - offset as usize % self.options.capacity);
172        ptr::write_bytes(self.pointer(offset), 0, right);
173        ptr::write_bytes(self.pointer(0), 0, length - right);
174    }
175
176    fn notify(&self) {
177        if self.header().notification.swap(1, SeqCst) == 0 {
178            // Notification is a hint: readers retry even if an OS post fails.
179            // A wakeup failure must never hide a committed send or consumed message.
180            let _ = self.post_notification();
181        }
182    }
183
184    #[inline]
185    fn post_notification(&self) -> Result<()> {
186        #[cfg(test)]
187        if self.fail_notification.load(Relaxed) {
188            return Err(std::io::Error::other("injected notification failure").into());
189        }
190        self.signal.post()
191    }
192
193    fn empty(&self) -> bool {
194        self.header().read.load(Acquire) == self.header().write.load(Acquire)
195    }
196}
197
198/// A concurrently usable publisher. Dropping it releases its registration.
199pub struct Publisher {
200    _lease: Lease,
201    shared: Shared,
202    id: i64,
203    slot: usize,
204}
205
206struct Active<'a>(&'a AtomicI32);
207impl Drop for Active<'_> {
208    fn drop(&mut self) {
209        self.0.fetch_sub(1, SeqCst);
210    }
211}
212
213impl Publisher {
214    /// Creates or joins a queue with the supplied identity and capacity.
215    ///
216    /// # Errors
217    /// Rejects invalid options, capacity mismatches, exhausted registrations,
218    /// publisher limits (publishers only), and operating system failures.
219    pub fn open(options: &Options) -> Result<Self> {
220        let shared = Shared::open(options.clone())?;
221        let (id, lease) = shared.register()?;
222        let slot = shared.slot(id)?;
223        Ok(Self {
224            _lease: lease,
225            shared,
226            id,
227            slot,
228        })
229    }
230
231    /// Returns [`Error::Full`] when there is insufficient space or recovery closes admission.
232    pub fn try_send(&self, message: &[u8]) -> Result<()> {
233        let count = self.shared.active(self.slot);
234        count.fetch_add(1, SeqCst);
235        let _active = Active(count);
236        if self.shared.gate().load(Acquire) != 0 {
237            return Err(Error::Full);
238        }
239        self.send_admitted(message)
240    }
241
242    /// Publishes an ordered prefix, amortizing publisher admission across a batch.
243    /// Returns the committed prefix length. A short count (including zero) means
244    /// full, recovery, or a mid-batch error; retry the unsent suffix to observe a
245    /// persistent error. An error before any commit is returned immediately.
246    pub fn try_send_batch(&self, messages: &[&[u8]]) -> Result<usize> {
247        let count = self.shared.active(self.slot);
248        count.fetch_add(1, SeqCst);
249        let _active = Active(count);
250        if self.shared.gate().load(Acquire) != 0 {
251            return Ok(0);
252        }
253        let mut sent = 0;
254        for message in messages {
255            match self.send_admitted(message) {
256                Ok(()) => sent += 1,
257                Err(Error::Full) => break,
258                // Preserve the committed prefix even if a lifetime counter runs
259                // out mid-batch. Retrying the remainder surfaces the error.
260                Err(_) if sent > 0 => break,
261                Err(error) => return Err(error),
262            }
263        }
264        Ok(sent)
265    }
266
267    fn send_admitted(&self, message: &[u8]) -> Result<()> {
268        if message.len() > i32::MAX as usize {
269            return Err(Error::Invalid("message exceeds the protocol length limit"));
270        }
271        let length = (message.len() + 15) & !7;
272        let Some(max_used) = self.shared.options.capacity.checked_sub(length) else {
273            return Err(Error::Full);
274        };
275        let header = self.shared.header();
276        loop {
277            let read = header.read.load(Acquire);
278            let write = header.write.load(Acquire);
279            let Some(used) = write.checked_sub(read) else {
280                return Err(Error::Corrupt);
281            };
282            if used < 0 || used > max_used as i64 {
283                return Err(Error::Full);
284            }
285            let next = write.checked_add(length as i64).ok_or(Error::Exhausted)?;
286            if header
287                .write
288                .compare_exchange(write, next, SeqCst, Acquire)
289                .is_err()
290            {
291                continue;
292            }
293            // The reservation belongs exclusively to this call until readiness is released.
294            unsafe {
295                self.shared.write(write + 8, message);
296                self.shared
297                    .pointer(write)
298                    .add(4)
299                    .cast::<i32>()
300                    .write(message.len() as i32);
301            }
302            self.shared.state(write).store(2, Release);
303            self.shared.notify();
304            return Ok(());
305        }
306    }
307}
308
309impl Drop for Publisher {
310    fn drop(&mut self) {
311        // A forked child must not release its parent's shared registration.
312        if !self._lease.is_current_process() {
313            return;
314        }
315        // Safe Rust cannot drop an endpoint while a call still borrows it.
316        let _ = self
317            .shared
318            .owner(self.slot)
319            .compare_exchange(self.id, 0, SeqCst, Acquire);
320    }
321}
322
323#[derive(Clone, Copy)]
324struct Pending {
325    started: u64,
326    read: i64,
327    tail: i64,
328}
329
330/// A subscriber. Multiple subscribers compete for messages; delivery is not broadcast.
331pub struct Subscriber {
332    _lease: Lease,
333    shared: Shared,
334    id: i64,
335    pending: UnsafeCell<Option<Pending>>,
336    next_check: AtomicU64,
337}
338
339// `pending` is only accessed after acquiring the shared reader lock. The unique
340// participant ID also prevents two calls on this same subscriber from owning it.
341unsafe impl Sync for Subscriber {}
342
343struct ReadGuard<'a> {
344    owner: &'a AtomicI64,
345    id: i64,
346}
347impl Drop for ReadGuard<'_> {
348    fn drop(&mut self) {
349        let _ = self.owner.compare_exchange(self.id, 0, SeqCst, Acquire);
350    }
351}
352struct GateGuard<'a>(&'a AtomicI32);
353impl Drop for GateGuard<'_> {
354    fn drop(&mut self) {
355        self.0.swap(0, SeqCst);
356    }
357}
358
359impl Subscriber {
360    /// Creates or joins a queue with the supplied identity and capacity.
361    ///
362    /// # Errors
363    /// Rejects invalid options, capacity mismatches, exhausted registrations,
364    /// publisher limits (publishers only), and operating system failures.
365    pub fn open(options: &Options) -> Result<Self> {
366        let shared = Shared::open(options.clone())?;
367        let (id, lease) = shared.register()?;
368        Ok(Self {
369            _lease: lease,
370            shared,
371            id,
372            pending: UnsafeCell::new(None),
373            next_check: AtomicU64::new(now_ms() + RECOVERY_MS),
374        })
375    }
376
377    /// Copies and consumes a ready message, allocating a result vector.
378    pub fn try_recv(&self) -> Result<Option<Vec<u8>>> {
379        self.receive_with(|shared, offset, length| {
380            let mut message = Vec::with_capacity(length);
381            unsafe {
382                // The reader lock protects the source. Initialize every byte before
383                // exposing the vector's length, avoiding a redundant zero-fill.
384                shared.read(offset, message.as_mut_ptr(), length);
385                message.set_len(length);
386            }
387            message
388        })
389    }
390
391    /// Copies into caller-owned storage. An undersized buffer truncates and consumes
392    /// the message, matching the .NET v3 API. The return value is bytes copied.
393    pub fn try_recv_into(&self, buffer: &mut [u8]) -> Result<Option<usize>> {
394        self.receive_with(|shared, offset, length| {
395            let length = length.min(buffer.len());
396            unsafe {
397                shared.read(offset, buffer.as_mut_ptr(), length);
398            }
399            length
400        })
401    }
402
403    /// Waits for a message indefinitely.
404    pub fn recv(&self) -> Result<Vec<u8>> {
405        // The unbounded path only returns on delivery or error.
406        self.receive_wait(None)
407            .map(|message| message.expect("unbounded wait timed out"))
408    }
409
410    /// Waits for a message, or returns None after the timeout. A zero timeout
411    /// performs one attempt. Missed notifications retain the five-millisecond retry.
412    pub fn recv_timeout(&self, timeout: Duration) -> Result<Option<Vec<u8>>> {
413        self.receive_wait(Some(timeout))
414    }
415
416    fn receive_wait(&self, timeout: Option<Duration>) -> Result<Option<Vec<u8>>> {
417        let started = Instant::now();
418        let mut relay = false;
419        let result = (|| loop {
420            if let Some(message) = self.try_recv()? {
421                return Ok(Some(message));
422            }
423            let wait = match timeout {
424                Some(limit) => {
425                    let elapsed = started.elapsed();
426                    if elapsed >= limit {
427                        return Ok(None);
428                    }
429                    (limit - elapsed).min(Duration::from_millis(5))
430                }
431                None => Duration::from_millis(5),
432            };
433            if self.shared.signal.wait(wait)? {
434                self.shared.header().notification.swap(0, SeqCst);
435                relay = true;
436            }
437        })();
438        if relay && !self.shared.empty() {
439            self.shared.notify();
440        }
441        result
442    }
443
444    fn receive_with<T>(&self, copy: impl FnOnce(&Shared, i64, usize) -> T) -> Result<Option<T>> {
445        let shared = &self.shared;
446        let header = shared.header();
447        let owner = header.reader.load(Acquire);
448        if owner != 0 {
449            self.recover_reader(owner);
450            return Ok(None);
451        }
452        // Dead-reader repair must precede the empty check: recovery can die after
453        // advancing read to write but before reopening publisher admission.
454        if shared.empty()
455            || header
456                .reader
457                .compare_exchange(0, self.id, SeqCst, Acquire)
458                .is_err()
459        {
460            return Ok(None);
461        }
462        let _read_guard = ReadGuard {
463            owner: &header.reader,
464            id: self.id,
465        };
466        let read = header.read.load(Acquire);
467        let write = header.write.load(Acquire);
468        if read == write {
469            return Ok(None);
470        }
471        if read < 0 {
472            return Err(Error::Corrupt);
473        }
474        let pending = unsafe { &mut *self.pending.get() };
475        if shared
476            .state(read)
477            .compare_exchange(2, 1, SeqCst, Acquire)
478            .is_err()
479        {
480            let now = now_ms();
481            let previous = match *pending {
482                Some(p) if p.read == read => p,
483                _ => {
484                    *pending = Some(Pending {
485                        started: now,
486                        read,
487                        tail: header.write.load(Acquire),
488                    });
489                    return Ok(None);
490                }
491            };
492            if now.saturating_sub(previous.started) >= RECOVERY_MS {
493                shared.gate().swap(1, SeqCst);
494                let _gate_guard = GateGuard(shared.gate());
495                if shared.any_active() {
496                    *pending = Some(Pending {
497                        started: now,
498                        ..previous
499                    });
500                    return Ok(None);
501                }
502                let length = previous.tail.checked_sub(read).ok_or(Error::Corrupt)?;
503                if length < 0 || length as usize > shared.options.capacity {
504                    return Err(Error::Corrupt);
505                }
506                unsafe {
507                    shared.clear(read, length as usize);
508                }
509                header.read.swap(previous.tail, SeqCst);
510                *pending = None;
511            }
512            return Ok(None);
513        }
514        *pending = None;
515        let body = unsafe { shared.pointer(read).add(4).cast::<i32>().read() };
516        if body < 0 || body as usize > shared.options.capacity - 8 {
517            shared.state(read).store(2, Release);
518            return Err(Error::Corrupt);
519        }
520        let length = (body as usize + 15) & !7;
521        let Some(next) = read
522            .checked_add(length as i64)
523            .filter(|next| *next <= write)
524        else {
525            shared.state(read).store(2, Release);
526            return Err(Error::Corrupt);
527        };
528        let result = copy(shared, read + 8, body as usize);
529        unsafe {
530            shared.clear(read, length);
531        }
532        header.read.swap(next, SeqCst);
533        Ok(Some(result))
534    }
535
536    fn recover_reader(&self, owner: i64) {
537        if owner == self.id {
538            return;
539        }
540        let next = self.next_check.load(Acquire);
541        let now = now_ms();
542        if now < next
543            || self
544                .next_check
545                .compare_exchange(next, now + RECOVERY_MS, SeqCst, Acquire)
546                .is_err()
547        {
548            return;
549        }
550        let header = self.shared.header();
551        if !Lease::alive(&self.shared.options, owner)
552            && header
553                .reader
554                .compare_exchange(owner, self.id, SeqCst, Acquire)
555                .is_ok()
556        {
557            let _guard = ReadGuard {
558                owner: &header.reader,
559                id: self.id,
560            };
561            self.shared.gate().swap(0, SeqCst);
562        }
563    }
564}
565
566impl std::fmt::Debug for Publisher {
567    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
568        f.debug_struct("Publisher")
569            .field("options", &self.shared.options)
570            .field("id", &self.id)
571            .finish_non_exhaustive()
572    }
573}
574impl std::fmt::Debug for Subscriber {
575    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
576        f.debug_struct("Subscriber")
577            .field("options", &self.shared.options)
578            .field("id", &self.id)
579            .finish_non_exhaustive()
580    }
581}
582
583#[cfg(test)]
584mod tests;