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    unsafe fn read(&self, offset: i64, target: &mut [u8]) {
163        let right = target
164            .len()
165            .min(self.options.capacity - offset as usize % self.options.capacity);
166        ptr::copy_nonoverlapping(self.pointer(offset), target.as_mut_ptr(), right);
167        ptr::copy_nonoverlapping(
168            self.pointer(0),
169            target.as_mut_ptr().add(right),
170            target.len() - right,
171        );
172    }
173
174    unsafe fn clear(&self, offset: i64, length: usize) {
175        debug_assert!(length <= self.options.capacity);
176        let right = length.min(self.options.capacity - offset as usize % self.options.capacity);
177        ptr::write_bytes(self.pointer(offset), 0, right);
178        ptr::write_bytes(self.pointer(0), 0, length - right);
179    }
180
181    fn notify(&self) {
182        if self.header().notification.swap(1, SeqCst) == 0 {
183            // Notification is a hint: readers retry even if an OS post fails.
184            // A wakeup failure must never hide a committed send or consumed message.
185            let _ = self.post_notification();
186        }
187    }
188
189    #[inline]
190    fn post_notification(&self) -> Result<()> {
191        #[cfg(test)]
192        if self.fail_notification.load(Relaxed) {
193            return Err(std::io::Error::other("injected notification failure").into());
194        }
195        self.signal.post()
196    }
197
198    fn empty(&self) -> bool {
199        self.header().read.load(Acquire) == self.header().write.load(Acquire)
200    }
201}
202
203/// A concurrently usable publisher. Dropping it releases its registration.
204pub struct Publisher {
205    _lease: Lease,
206    shared: Shared,
207    id: i64,
208    slot: usize,
209}
210
211struct Active<'a>(&'a AtomicI32);
212impl Drop for Active<'_> {
213    fn drop(&mut self) {
214        self.0.fetch_sub(1, SeqCst);
215    }
216}
217
218impl Publisher {
219    /// Creates or joins a queue with the supplied identity and capacity.
220    ///
221    /// # Errors
222    /// Rejects invalid options, capacity mismatches, exhausted registrations,
223    /// publisher limits (publishers only), and operating system failures.
224    pub fn open(options: &Options) -> Result<Self> {
225        let shared = Shared::open(options.clone())?;
226        let (id, lease) = shared.register()?;
227        let slot = shared.slot(id)?;
228        Ok(Self {
229            _lease: lease,
230            shared,
231            id,
232            slot,
233        })
234    }
235
236    /// Returns [`Error::Full`] when there is insufficient space or recovery closes admission.
237    pub fn try_send(&self, message: &[u8]) -> Result<()> {
238        let count = self.shared.active(self.slot);
239        count.fetch_add(1, SeqCst);
240        let _active = Active(count);
241        if self.shared.gate().load(Acquire) != 0 {
242            return Err(Error::Full);
243        }
244        self.send_admitted(message)
245    }
246
247    /// Publishes an ordered prefix, amortizing publisher admission across a batch.
248    /// Returns the committed prefix length. A short count (including zero) means
249    /// full, recovery, or a mid-batch error; retry the unsent suffix to observe a
250    /// persistent error. An error before any commit is returned immediately.
251    pub fn try_send_batch(&self, messages: &[&[u8]]) -> Result<usize> {
252        let count = self.shared.active(self.slot);
253        count.fetch_add(1, SeqCst);
254        let _active = Active(count);
255        if self.shared.gate().load(Acquire) != 0 {
256            return Ok(0);
257        }
258        let mut sent = 0;
259        for message in messages {
260            match self.send_admitted(message) {
261                Ok(()) => sent += 1,
262                Err(Error::Full) => break,
263                // Preserve the committed prefix even if a lifetime counter runs
264                // out mid-batch. Retrying the remainder surfaces the error.
265                Err(_) if sent > 0 => break,
266                Err(error) => return Err(error),
267            }
268        }
269        Ok(sent)
270    }
271
272    fn send_admitted(&self, message: &[u8]) -> Result<()> {
273        if message.len() > i32::MAX as usize {
274            return Err(Error::Invalid("message exceeds the protocol length limit"));
275        }
276        let length = (message.len() + 15) & !7;
277        let Some(max_used) = self.shared.options.capacity.checked_sub(length) else {
278            return Err(Error::Full);
279        };
280        let header = self.shared.header();
281        loop {
282            let read = header.read.load(Acquire);
283            let write = header.write.load(Acquire);
284            let Some(used) = write.checked_sub(read) else {
285                return Err(Error::Corrupt);
286            };
287            if used < 0 || used > max_used as i64 {
288                return Err(Error::Full);
289            }
290            let next = write.checked_add(length as i64).ok_or(Error::Exhausted)?;
291            if header
292                .write
293                .compare_exchange(write, next, SeqCst, Acquire)
294                .is_err()
295            {
296                continue;
297            }
298            // The reservation belongs exclusively to this call until readiness is released.
299            unsafe {
300                self.shared.write(write + 8, message);
301                self.shared
302                    .pointer(write)
303                    .add(4)
304                    .cast::<i32>()
305                    .write(message.len() as i32);
306            }
307            self.shared.state(write).store(2, Release);
308            self.shared.notify();
309            return Ok(());
310        }
311    }
312}
313
314impl Drop for Publisher {
315    fn drop(&mut self) {
316        // A forked child must not release its parent's shared registration.
317        if !self._lease.is_current_process() {
318            return;
319        }
320        // Safe Rust cannot drop an endpoint while a call still borrows it.
321        let _ = self
322            .shared
323            .owner(self.slot)
324            .compare_exchange(self.id, 0, SeqCst, Acquire);
325    }
326}
327
328#[derive(Clone, Copy)]
329struct Pending {
330    started: u64,
331    read: i64,
332    tail: i64,
333}
334
335/// A subscriber. Multiple subscribers compete for messages; delivery is not broadcast.
336pub struct Subscriber {
337    _lease: Lease,
338    shared: Shared,
339    id: i64,
340    pending: UnsafeCell<Option<Pending>>,
341    next_check: AtomicU64,
342}
343
344// `pending` is only accessed after acquiring the shared reader lock. The unique
345// participant ID also prevents two calls on this same subscriber from owning it.
346unsafe impl Sync for Subscriber {}
347
348struct ReadGuard<'a> {
349    owner: &'a AtomicI64,
350    id: i64,
351}
352impl Drop for ReadGuard<'_> {
353    fn drop(&mut self) {
354        let _ = self.owner.compare_exchange(self.id, 0, SeqCst, Acquire);
355    }
356}
357struct GateGuard<'a>(&'a AtomicI32);
358impl Drop for GateGuard<'_> {
359    fn drop(&mut self) {
360        self.0.swap(0, SeqCst);
361    }
362}
363
364impl Subscriber {
365    /// Creates or joins a queue with the supplied identity and capacity.
366    ///
367    /// # Errors
368    /// Rejects invalid options, capacity mismatches, exhausted registrations,
369    /// publisher limits (publishers only), and operating system failures.
370    pub fn open(options: &Options) -> Result<Self> {
371        let shared = Shared::open(options.clone())?;
372        let (id, lease) = shared.register()?;
373        Ok(Self {
374            _lease: lease,
375            shared,
376            id,
377            pending: UnsafeCell::new(None),
378            next_check: AtomicU64::new(now_ms() + RECOVERY_MS),
379        })
380    }
381
382    /// Copies and consumes a ready message, allocating a result vector.
383    pub fn try_recv(&self) -> Result<Option<Vec<u8>>> {
384        self.receive_with(|shared, offset, length| {
385            let mut message = vec![0; length];
386            unsafe {
387                shared.read(offset, &mut message);
388            }
389            message
390        })
391    }
392
393    /// Copies into caller-owned storage. An undersized buffer truncates and consumes
394    /// the message, matching the .NET v3 API. The return value is bytes copied.
395    pub fn try_recv_into(&self, buffer: &mut [u8]) -> Result<Option<usize>> {
396        self.receive_with(|shared, offset, length| {
397            let length = length.min(buffer.len());
398            unsafe {
399                shared.read(offset, &mut buffer[..length]);
400            }
401            length
402        })
403    }
404
405    /// Waits for a message indefinitely.
406    pub fn recv(&self) -> Result<Vec<u8>> {
407        // The unbounded path only returns on delivery or error.
408        self.receive_wait(None)
409            .map(|message| message.expect("unbounded wait timed out"))
410    }
411
412    /// Waits for a message, or returns None after the timeout. A zero timeout
413    /// performs one attempt. Missed notifications retain the five-millisecond retry.
414    pub fn recv_timeout(&self, timeout: Duration) -> Result<Option<Vec<u8>>> {
415        self.receive_wait(Some(timeout))
416    }
417
418    fn receive_wait(&self, timeout: Option<Duration>) -> Result<Option<Vec<u8>>> {
419        let started = Instant::now();
420        let mut relay = false;
421        let result = (|| loop {
422            if let Some(message) = self.try_recv()? {
423                return Ok(Some(message));
424            }
425            let wait = match timeout {
426                Some(limit) => {
427                    let elapsed = started.elapsed();
428                    if elapsed >= limit {
429                        return Ok(None);
430                    }
431                    (limit - elapsed).min(Duration::from_millis(5))
432                }
433                None => Duration::from_millis(5),
434            };
435            if self.shared.signal.wait(wait)? {
436                self.shared.header().notification.swap(0, SeqCst);
437                relay = true;
438            }
439        })();
440        if relay && !self.shared.empty() {
441            self.shared.notify();
442        }
443        result
444    }
445
446    fn receive_with<T>(&self, copy: impl FnOnce(&Shared, i64, usize) -> T) -> Result<Option<T>> {
447        let shared = &self.shared;
448        let header = shared.header();
449        let owner = header.reader.load(Acquire);
450        if owner != 0 {
451            self.recover_reader(owner);
452            return Ok(None);
453        }
454        // Dead-reader repair must precede the empty check: recovery can die after
455        // advancing read to write but before reopening publisher admission.
456        if shared.empty()
457            || header
458                .reader
459                .compare_exchange(0, self.id, SeqCst, Acquire)
460                .is_err()
461        {
462            return Ok(None);
463        }
464        let _read_guard = ReadGuard {
465            owner: &header.reader,
466            id: self.id,
467        };
468        let read = header.read.load(Acquire);
469        let write = header.write.load(Acquire);
470        if read == write {
471            return Ok(None);
472        }
473        if read < 0 {
474            return Err(Error::Corrupt);
475        }
476        let pending = unsafe { &mut *self.pending.get() };
477        if shared
478            .state(read)
479            .compare_exchange(2, 1, SeqCst, Acquire)
480            .is_err()
481        {
482            let now = now_ms();
483            let previous = match *pending {
484                Some(p) if p.read == read => p,
485                _ => {
486                    *pending = Some(Pending {
487                        started: now,
488                        read,
489                        tail: header.write.load(Acquire),
490                    });
491                    return Ok(None);
492                }
493            };
494            if now.saturating_sub(previous.started) >= RECOVERY_MS {
495                shared.gate().swap(1, SeqCst);
496                let _gate_guard = GateGuard(shared.gate());
497                if shared.any_active() {
498                    *pending = Some(Pending {
499                        started: now,
500                        ..previous
501                    });
502                    return Ok(None);
503                }
504                let length = previous.tail.checked_sub(read).ok_or(Error::Corrupt)?;
505                if length < 0 || length as usize > shared.options.capacity {
506                    return Err(Error::Corrupt);
507                }
508                unsafe {
509                    shared.clear(read, length as usize);
510                }
511                header.read.swap(previous.tail, SeqCst);
512                *pending = None;
513            }
514            return Ok(None);
515        }
516        *pending = None;
517        let body = unsafe { shared.pointer(read).add(4).cast::<i32>().read() };
518        if body < 0 || body as usize > shared.options.capacity - 8 {
519            shared.state(read).store(2, Release);
520            return Err(Error::Corrupt);
521        }
522        let length = (body as usize + 15) & !7;
523        let Some(next) = read
524            .checked_add(length as i64)
525            .filter(|next| *next <= write)
526        else {
527            shared.state(read).store(2, Release);
528            return Err(Error::Corrupt);
529        };
530        let result = copy(shared, read + 8, body as usize);
531        unsafe {
532            shared.clear(read, length);
533        }
534        header.read.swap(next, SeqCst);
535        Ok(Some(result))
536    }
537
538    fn recover_reader(&self, owner: i64) {
539        if owner == self.id {
540            return;
541        }
542        let next = self.next_check.load(Acquire);
543        let now = now_ms();
544        if now < next
545            || self
546                .next_check
547                .compare_exchange(next, now + RECOVERY_MS, SeqCst, Acquire)
548                .is_err()
549        {
550            return;
551        }
552        let header = self.shared.header();
553        if !Lease::alive(&self.shared.options, owner)
554            && header
555                .reader
556                .compare_exchange(owner, self.id, SeqCst, Acquire)
557                .is_ok()
558        {
559            let _guard = ReadGuard {
560                owner: &header.reader,
561                id: self.id,
562            };
563            self.shared.gate().swap(0, SeqCst);
564        }
565    }
566}
567
568impl std::fmt::Debug for Publisher {
569    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
570        f.debug_struct("Publisher")
571            .field("options", &self.shared.options)
572            .field("id", &self.id)
573            .finish_non_exhaustive()
574    }
575}
576impl std::fmt::Debug for Subscriber {
577    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
578        f.debug_struct("Subscriber")
579            .field("options", &self.shared.options)
580            .field("id", &self.id)
581            .finish_non_exhaustive()
582    }
583}
584
585#[cfg(test)]
586mod tests;