Skip to main content

datum/concurrent/
topic.rs

1//! Stream-native many-publisher, many-subscriber broadcast topic.
2//!
3//! [`Topic`] mirrors FS2 `Topic` / ZIO `Hub` broadcast semantics for Datum:
4//! publishers broadcast to the subscriber set that is current at the publish
5//! linearization point, and each subscriber sees only elements published after
6//! its subscription is registered. Publishing with no subscribers succeeds and
7//! drops the element, matching FS2 rather than ZIO's buffering-for-future-
8//! subscribers behavior.
9//!
10//! The implementation follows M9's two-plane rule. A Ractor actor owns only the
11//! control plane: subscribe, unsubscribe, close, terminal state, and publishing
12//! `ArcSwap` subscriber-table snapshots. The element path is direct: publishers
13//! claim one global sequence turn, load the current table snapshot, enqueue an
14//! `Arc<T>` into each subscriber's lock-free slot queue, and wake a subscriber
15//! only on an empty-to-non-empty transition or if it is parked at the queue
16//! head. There is no actor message and no mutex on the accepted publish path;
17//! backpressured publishers park outside the actor.
18
19use std::{
20    collections::{HashMap, VecDeque},
21    fmt, hint,
22    marker::PhantomData,
23    sync::{
24        Arc, Condvar, Mutex, MutexGuard,
25        atomic::{AtomicBool, AtomicU8, AtomicU64, AtomicUsize, Ordering, fence},
26        mpsc,
27    },
28    thread,
29    time::Duration,
30};
31
32use arc_swap::ArcSwap;
33use crossbeam_queue::ArrayQueue;
34use ractor::{Actor, ActorProcessingErr, ActorRef};
35use tokio::sync::Notify;
36
37use crate::{
38    StreamError, StreamResult,
39    actor::block_on_ractor_runtime,
40    stream::{BoxStream, NotUsed, Source, current_stream_cancelled},
41};
42
43const TOPIC_OPEN: u8 = 0;
44const TOPIC_CLOSING: u8 = 1;
45const TOPIC_CLOSED: u8 = 2;
46const SLOT_OPEN: u8 = 0;
47const SLOT_COMPLETE: u8 = 1;
48const SLOT_ERROR: u8 = 2;
49const SLOT_WAIT_BACKSTOP: Duration = Duration::from_millis(10);
50const TOPIC_DRAIN_BATCH: usize = 256;
51
52type Ack = mpsc::Sender<StreamResult<()>>;
53
54/// Overflow policy for each [`Topic`] subscriber buffer.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum TopicOverflow {
57    /// Preserve every element for every active subscriber.
58    ///
59    /// `publish(value).await` waits until every subscriber in the publish
60    /// snapshot has capacity, then enqueues the element to all of them in the
61    /// topic's global publish order. `try_publish(value)` returns
62    /// [`TopicTryPublishError::Full`] instead of waiting if any subscriber in
63    /// the snapshot is full.
64    Backpressure,
65    /// Keep the newest bounded window per subscriber.
66    ///
67    /// When a subscriber buffer is full, publishing drops that subscriber's
68    /// oldest queued element and enqueues the new element. Other subscribers are
69    /// unaffected. Publishers never wait for subscriber capacity under this
70    /// policy.
71    Sliding,
72    /// Drop new elements for slow subscribers.
73    ///
74    /// When a subscriber buffer is full, the new element is skipped for that
75    /// subscriber only. Other subscribers still receive it. Publishers never
76    /// wait for subscriber capacity under this policy.
77    Dropping,
78}
79
80/// Error returned by [`Topic::publish`] when the topic closes before the value
81/// can be accepted.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum TopicPublishError<T> {
84    /// The topic is closed; the unpublished value is returned.
85    Closed(T),
86}
87
88/// Error returned by [`Topic::try_publish`].
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum TopicTryPublishError<T> {
91    /// The topic is closed; the unpublished value is returned.
92    Closed(T),
93    /// The call would need to wait for subscriber capacity under
94    /// [`TopicOverflow::Backpressure`]; the unpublished value is returned.
95    Full(T),
96    /// Another publisher currently owns an earlier global publish turn; the
97    /// unpublished value is returned rather than blocking behind it.
98    Busy(T),
99}
100
101/// A cloneable stream-native broadcast topic.
102///
103/// Clone the handle for publishers. Each call to [`Topic::subscribe`] returns a
104/// source blueprint; each materialization registers a fresh subscriber slot.
105/// Subscribers observe every element published after their registration, in the
106/// same global order as every other subscriber, subject to this topic's
107/// overflow policy.
108///
109/// Capacity is per subscriber and must be greater than zero.
110pub struct Topic<T: Send + Sync + 'static> {
111    inner: Arc<TopicInner<T>>,
112}
113
114struct TopicInner<T: Send + Sync + 'static> {
115    actor: ActorRef<TopicMessage<T>>,
116    shared: Arc<TopicShared<T>>,
117    next_subscriber_id: Arc<AtomicU64>,
118}
119
120struct TopicShared<T: Send + Sync + 'static> {
121    subscribers: Arc<ArcSwap<TopicSlotTable<T>>>,
122    capacity: usize,
123    overflow: TopicOverflow,
124    lifecycle: AtomicU8,
125    active_publishers: AtomicUsize,
126    next_sequence: AtomicU64,
127    delivered_sequence: AtomicU64,
128    space_waiters: AtomicUsize,
129    space_available: Notify,
130    closed_notified: Notify,
131}
132
133struct TopicSlotTable<T: Send + Sync + 'static> {
134    slots: Vec<Arc<TopicSlot<T>>>,
135}
136
137struct TopicSlot<T: Send + Sync + 'static> {
138    id: u64,
139    actor: ActorRef<TopicMessage<T>>,
140    buffer: ArrayQueue<Arc<T>>,
141    active: AtomicBool,
142    parked: AtomicBool,
143    terminal_state: AtomicU8,
144    terminal: Mutex<Option<TopicSlotTerminal>>,
145    available_lock: Mutex<()>,
146    available: Condvar,
147    async_available: Notify,
148}
149
150#[derive(Clone)]
151enum TopicSlotTerminal {
152    Complete,
153    Error(StreamError),
154}
155
156impl<T: Send + Sync + 'static> Clone for Topic<T> {
157    fn clone(&self) -> Self {
158        Self {
159            inner: Arc::clone(&self.inner),
160        }
161    }
162}
163
164impl<T: Send + Sync + 'static> Topic<T> {
165    /// Create a new topic with per-subscriber `capacity` and `overflow` policy.
166    ///
167    /// Panics if `capacity == 0`.
168    pub fn new(capacity: usize, overflow: TopicOverflow) -> StreamResult<Self> {
169        assert!(capacity > 0, "topic capacity must be greater than zero");
170        let shared = Arc::new(TopicShared {
171            subscribers: Arc::new(ArcSwap::from_pointee(TopicSlotTable { slots: Vec::new() })),
172            capacity,
173            overflow,
174            lifecycle: AtomicU8::new(TOPIC_OPEN),
175            active_publishers: AtomicUsize::new(0),
176            next_sequence: AtomicU64::new(0),
177            delivered_sequence: AtomicU64::new(0),
178            space_waiters: AtomicUsize::new(0),
179            space_available: Notify::new(),
180            closed_notified: Notify::new(),
181        });
182        let state = TopicActorState {
183            shared: Arc::clone(&shared),
184            subscribers: HashMap::new(),
185            closed: false,
186        };
187        let (actor, _handle) =
188            block_on_ractor_runtime(Actor::spawn(None, TopicActor::<T>::default(), state))?
189                .map_err(|error| {
190                    StreamError::Failed(format!("topic actor failed to spawn: {error}"))
191                })?;
192        Ok(Self {
193            inner: Arc::new(TopicInner {
194                actor,
195                shared,
196                next_subscriber_id: Arc::new(AtomicU64::new(1)),
197            }),
198        })
199    }
200
201    /// Publish one value, waiting only under [`TopicOverflow::Backpressure`].
202    ///
203    /// The publish linearizes when this call owns its global sequence turn and
204    /// loads the subscriber-table snapshot. Subscribers in that snapshot receive
205    /// the value according to the configured overflow policy; subscribers
206    /// registered later do not. If the topic has no subscribers, the value is
207    /// accepted and dropped.
208    pub async fn publish(&self, value: T) -> Result<(), TopicPublishError<T>> {
209        let Ok(_permit) = self.inner.shared.begin_publish() else {
210            return Err(TopicPublishError::Closed(value));
211        };
212        let sequence = self.inner.shared.claim_sequence();
213        self.inner.shared.wait_publish_turn(sequence);
214
215        let table = self.inner.shared.subscribers.load_full();
216        if self.inner.shared.overflow == TopicOverflow::Backpressure {
217            self.inner.shared.wait_for_capacity(&table).await;
218        }
219
220        self.inner
221            .shared
222            .publish_to_snapshot(&table, Arc::new(value));
223        self.inner.shared.finish_publish(sequence);
224        Ok(())
225    }
226
227    /// Try to publish one value without awaiting subscriber capacity or an
228    /// earlier in-flight publisher.
229    ///
230    /// Under [`TopicOverflow::Backpressure`], this returns
231    /// [`TopicTryPublishError::Full`] if any active subscriber in the publish
232    /// snapshot is full. Under all policies, it returns
233    /// [`TopicTryPublishError::Busy`] if another publisher currently owns an
234    /// earlier global publish turn.
235    pub fn try_publish(&self, value: T) -> Result<(), TopicTryPublishError<T>> {
236        let Ok(_permit) = self.inner.shared.begin_publish() else {
237            return Err(TopicTryPublishError::Closed(value));
238        };
239        let Some(sequence) = self.inner.shared.try_claim_sequence() else {
240            return Err(TopicTryPublishError::Busy(value));
241        };
242
243        let table = self.inner.shared.subscribers.load_full();
244        if self.inner.shared.overflow == TopicOverflow::Backpressure
245            && !self.inner.shared.snapshot_has_capacity(&table)
246        {
247            self.inner.shared.finish_publish(sequence);
248            return Err(TopicTryPublishError::Full(value));
249        }
250
251        self.inner
252            .shared
253            .publish_to_snapshot(&table, Arc::new(value));
254        self.inner.shared.finish_publish(sequence);
255        Ok(())
256    }
257
258    /// Return a source blueprint that registers a fresh subscriber when
259    /// materialized.
260    ///
261    /// The source emits only elements published after its registration. If the
262    /// topic is already closed, the materialized source completes immediately.
263    #[must_use]
264    pub fn subscribe(&self) -> Source<T>
265    where
266        T: Clone,
267    {
268        let topic = self.clone();
269        let actor = self.inner.actor.clone();
270        let next_subscriber_id = Arc::clone(&self.inner.next_subscriber_id);
271        Source::from_materialized_factory(move |_materializer| {
272            let id = next_subscriber_id.fetch_add(1, Ordering::Relaxed);
273            let capacity = topic.registered_capacity();
274            let slot = TopicSlot::new(id, actor.clone(), capacity);
275            topic.register_slot(Arc::clone(&slot), id)?;
276            let stream: BoxStream<T> = Box::new(TopicStream {
277                shared: Arc::clone(&topic.inner.shared),
278                slot,
279                pending: VecDeque::new(),
280                terminated: false,
281            });
282            Ok((stream, NotUsed))
283        })
284    }
285
286    /// Return the number of currently active subscribers.
287    #[must_use]
288    pub fn subscriber_count(&self) -> usize {
289        self.inner.shared.subscriber_count()
290    }
291
292    /// Gracefully close the topic.
293    ///
294    /// Current subscribers drain their queued elements before completing.
295    /// Publishers that begin after close fail with [`TopicPublishError::Closed`]
296    /// or [`TopicTryPublishError::Closed`].
297    pub fn close(&self) -> StreamResult<()> {
298        let (reply, receiver) = mpsc::channel();
299        self.inner
300            .actor
301            .send_message(TopicMessage::Close { reply })
302            .map_err(|error| StreamError::ActorAskSendFailed {
303                reason: error.to_string(),
304            })?;
305        receiver.recv().unwrap_or(Err(StreamError::ActorTerminated))
306    }
307
308    /// Return true after the topic has closed.
309    #[must_use]
310    pub fn is_closed(&self) -> bool {
311        self.inner.shared.is_closed()
312    }
313
314    /// Wait until the topic is closed.
315    pub async fn closed(&self) {
316        loop {
317            if self.is_closed() {
318                return;
319            }
320            let notified = self.inner.shared.closed_notified.notified();
321            let mut notified = std::pin::pin!(notified);
322            notified.as_mut().enable();
323            if self.is_closed() {
324                return;
325            }
326            notified.as_mut().await;
327        }
328    }
329
330    fn registered_capacity(&self) -> usize {
331        self.inner.shared.capacity
332    }
333
334    fn register_slot(&self, slot: Arc<TopicSlot<T>>, id: u64) -> StreamResult<()> {
335        let (reply, receiver) = mpsc::channel();
336        self.inner
337            .actor
338            .send_message(TopicMessage::Subscribe { id, slot, reply })
339            .map_err(|error| StreamError::ActorAskSendFailed {
340                reason: error.to_string(),
341            })?;
342        receiver.recv().unwrap_or(Err(StreamError::ActorTerminated))
343    }
344}
345
346impl<T: Clone + Send + Sync + 'static> Topic<T> {
347    #[doc(hidden)]
348    pub fn __benchmark_subscribe(&self) -> StreamResult<TopicBenchmarkStream<T>> {
349        let id = self
350            .inner
351            .next_subscriber_id
352            .fetch_add(1, Ordering::Relaxed);
353        let capacity = self.registered_capacity();
354        let slot = TopicSlot::new(id, self.inner.actor.clone(), capacity);
355        self.register_slot(Arc::clone(&slot), id)?;
356        Ok(TopicBenchmarkStream {
357            shared: Arc::clone(&self.inner.shared),
358            slot,
359            pending: VecDeque::new(),
360            terminated: false,
361        })
362    }
363}
364
365impl<T: Send + Sync + 'static> TopicShared<T> {
366    fn begin_publish(&self) -> StreamResult<PublishPermit<'_>> {
367        if self.lifecycle.load(Ordering::Acquire) != TOPIC_OPEN {
368            return Err(closed_error());
369        }
370        self.active_publishers.fetch_add(1, Ordering::AcqRel);
371        if self.lifecycle.load(Ordering::Acquire) == TOPIC_OPEN {
372            Ok(PublishPermit {
373                active_publishers: &self.active_publishers,
374            })
375        } else {
376            self.active_publishers.fetch_sub(1, Ordering::AcqRel);
377            Err(closed_error())
378        }
379    }
380
381    fn claim_sequence(&self) -> u64 {
382        self.next_sequence.fetch_add(1, Ordering::AcqRel) + 1
383    }
384
385    fn try_claim_sequence(&self) -> Option<u64> {
386        let delivered = self.delivered_sequence.load(Ordering::Acquire);
387        self.next_sequence
388            .compare_exchange(
389                delivered,
390                delivered + 1,
391                Ordering::AcqRel,
392                Ordering::Acquire,
393            )
394            .ok()
395            .map(|_| delivered + 1)
396    }
397
398    fn wait_publish_turn(&self, sequence: u64) {
399        let mut spins = 0_u32;
400        while self.delivered_sequence.load(Ordering::Acquire) + 1 != sequence {
401            spins = spins.wrapping_add(1);
402            if spins < 64 {
403                hint::spin_loop();
404            } else {
405                thread::yield_now();
406            }
407        }
408    }
409
410    fn finish_publish(&self, sequence: u64) {
411        self.delivered_sequence.store(sequence, Ordering::Release);
412    }
413
414    fn publish_to_snapshot(&self, table: &TopicSlotTable<T>, value: Arc<T>) {
415        match self.overflow {
416            TopicOverflow::Backpressure => {
417                for slot in &table.slots {
418                    slot.enqueue_backpressured(Arc::clone(&value));
419                }
420            }
421            TopicOverflow::Sliding => {
422                for slot in &table.slots {
423                    slot.enqueue_sliding(Arc::clone(&value));
424                }
425            }
426            TopicOverflow::Dropping => {
427                for slot in &table.slots {
428                    slot.enqueue_dropping(Arc::clone(&value));
429                }
430            }
431        }
432    }
433
434    async fn wait_for_capacity(&self, table: &TopicSlotTable<T>) {
435        loop {
436            if self.snapshot_has_capacity(table) {
437                return;
438            }
439
440            let notified = self.space_available.notified();
441            let mut notified = std::pin::pin!(notified);
442            notified.as_mut().enable();
443            self.space_waiters.fetch_add(1, Ordering::AcqRel);
444            if self.snapshot_has_capacity(table) {
445                self.space_waiters.fetch_sub(1, Ordering::AcqRel);
446                return;
447            }
448            notified.as_mut().await;
449            self.space_waiters.fetch_sub(1, Ordering::AcqRel);
450        }
451    }
452
453    fn snapshot_has_capacity(&self, table: &TopicSlotTable<T>) -> bool {
454        table.slots.iter().all(|slot| slot.has_capacity())
455    }
456
457    fn notify_space(&self) {
458        if self.space_waiters.load(Ordering::Acquire) != 0 {
459            self.space_available.notify_waiters();
460        }
461    }
462
463    fn subscriber_count(&self) -> usize {
464        let table = self.subscribers.load();
465        table.slots.iter().filter(|slot| slot.is_active()).count()
466    }
467
468    fn is_closed(&self) -> bool {
469        self.lifecycle.load(Ordering::Acquire) == TOPIC_CLOSED
470    }
471
472    fn wait_for_publishers_to_drain(&self) {
473        while self.active_publishers.load(Ordering::Acquire) != 0 {
474            thread::yield_now();
475        }
476    }
477
478    fn mark_actor_terminated(&self) {
479        self.lifecycle.store(TOPIC_CLOSED, Ordering::Release);
480        self.closed_notified.notify_waiters();
481        self.notify_space();
482    }
483}
484
485struct PublishPermit<'a> {
486    active_publishers: &'a AtomicUsize,
487}
488
489impl Drop for PublishPermit<'_> {
490    fn drop(&mut self) {
491        self.active_publishers.fetch_sub(1, Ordering::AcqRel);
492    }
493}
494
495impl<T: Send + Sync + 'static> TopicSlot<T> {
496    fn new(id: u64, actor: ActorRef<TopicMessage<T>>, capacity: usize) -> Arc<Self> {
497        Arc::new(Self {
498            id,
499            actor,
500            buffer: ArrayQueue::new(capacity),
501            active: AtomicBool::new(true),
502            parked: AtomicBool::new(false),
503            terminal_state: AtomicU8::new(SLOT_OPEN),
504            terminal: Mutex::new(None),
505            available_lock: Mutex::new(()),
506            available: Condvar::new(),
507            async_available: Notify::new(),
508        })
509    }
510
511    fn terminal_lock(&self) -> MutexGuard<'_, Option<TopicSlotTerminal>> {
512        self.terminal
513            .lock()
514            .unwrap_or_else(|poison| poison.into_inner())
515    }
516
517    fn is_active(&self) -> bool {
518        self.active.load(Ordering::Acquire)
519    }
520
521    fn has_capacity(&self) -> bool {
522        !self.is_active() || !self.buffer.is_full()
523    }
524
525    fn enqueue_backpressured(&self, value: Arc<T>) {
526        if !self.is_active() {
527            return;
528        }
529        let was_empty = self.buffer.is_empty();
530        if self.buffer.push(value).is_ok() && was_empty {
531            self.wake();
532        }
533    }
534
535    fn enqueue_sliding(&self, value: Arc<T>) {
536        if !self.is_active() {
537            return;
538        }
539        while self.buffer.is_full() {
540            if self.buffer.pop().is_none() {
541                break;
542            }
543        }
544        let was_empty = self.buffer.is_empty();
545        if self.buffer.push(value).is_ok() && was_empty {
546            self.wake();
547        }
548    }
549
550    fn enqueue_dropping(&self, value: Arc<T>) {
551        if !self.is_active() || self.buffer.is_full() {
552            return;
553        }
554        let was_empty = self.buffer.is_empty();
555        if self.buffer.push(value).is_ok() && was_empty {
556            self.wake();
557        }
558    }
559
560    fn pop(&self) -> Option<Arc<T>> {
561        self.buffer.pop()
562    }
563
564    fn park(&self) {
565        self.parked.store(true, Ordering::Release);
566    }
567
568    fn unpark(&self) {
569        self.parked.store(false, Ordering::Release);
570    }
571
572    fn wake(&self) {
573        if self.parked.swap(false, Ordering::AcqRel) {
574            let _guard = self
575                .available_lock
576                .lock()
577                .unwrap_or_else(|poison| poison.into_inner());
578            self.available.notify_one();
579            self.async_available.notify_waiters();
580        }
581    }
582
583    fn complete(&self) {
584        if self
585            .terminal_state
586            .compare_exchange(
587                SLOT_OPEN,
588                SLOT_COMPLETE,
589                Ordering::AcqRel,
590                Ordering::Acquire,
591            )
592            .is_err()
593        {
594            return;
595        }
596        self.active.store(false, Ordering::Release);
597        *self.terminal_lock() = Some(TopicSlotTerminal::Complete);
598        self.wake();
599    }
600
601    fn fail(&self, error: StreamError) {
602        if self
603            .terminal_state
604            .compare_exchange(SLOT_OPEN, SLOT_ERROR, Ordering::AcqRel, Ordering::Acquire)
605            .is_err()
606        {
607            return;
608        }
609        self.active.store(false, Ordering::Release);
610        *self.terminal_lock() = Some(TopicSlotTerminal::Error(error));
611        self.wake();
612    }
613
614    fn terminal(&self) -> Option<TopicSlotTerminal> {
615        if self.terminal_state.load(Ordering::Acquire) == SLOT_OPEN {
616            return None;
617        }
618        self.terminal_lock().clone()
619    }
620
621    fn deactivate(&self) {
622        self.active.store(false, Ordering::Release);
623        while self.buffer.pop().is_some() {}
624        self.wake();
625    }
626
627    fn unsubscribe(&self) {
628        self.deactivate();
629        let _ = self
630            .actor
631            .send_message(TopicMessage::Unsubscribe { id: self.id });
632    }
633}
634
635impl<T: Send + Sync + 'static> Drop for TopicInner<T> {
636    fn drop(&mut self) {
637        self.actor.stop(None);
638    }
639}
640
641enum TopicMessage<T: Send + Sync + 'static> {
642    Close {
643        reply: Ack,
644    },
645    Subscribe {
646        id: u64,
647        slot: Arc<TopicSlot<T>>,
648        reply: Ack,
649    },
650    Unsubscribe {
651        id: u64,
652    },
653}
654
655struct TopicActor<T> {
656    _marker: PhantomData<fn() -> T>,
657}
658
659impl<T> Default for TopicActor<T> {
660    fn default() -> Self {
661        Self {
662            _marker: PhantomData,
663        }
664    }
665}
666
667struct TopicActorState<T: Send + Sync + 'static> {
668    shared: Arc<TopicShared<T>>,
669    subscribers: HashMap<u64, Arc<TopicSlot<T>>>,
670    closed: bool,
671}
672
673impl<T: Send + Sync + 'static> Actor for TopicActor<T> {
674    type Msg = TopicMessage<T>;
675    type State = TopicActorState<T>;
676    type Arguments = TopicActorState<T>;
677
678    async fn pre_start(
679        &self,
680        _myself: ActorRef<Self::Msg>,
681        args: Self::Arguments,
682    ) -> Result<Self::State, ActorProcessingErr> {
683        Ok(args)
684    }
685
686    async fn handle(
687        &self,
688        _myself: ActorRef<Self::Msg>,
689        message: Self::Msg,
690        state: &mut Self::State,
691    ) -> Result<(), ActorProcessingErr> {
692        match message {
693            TopicMessage::Close { reply } => {
694                close_topic(state);
695                let _ = reply.send(Ok(()));
696            }
697            TopicMessage::Subscribe { id, slot, reply } => {
698                if state.closed || state.shared.lifecycle.load(Ordering::Acquire) == TOPIC_CLOSED {
699                    slot.complete();
700                } else {
701                    state.subscribers.insert(id, Arc::clone(&slot));
702                    publish_topic_slot_table(state);
703                }
704                let _ = reply.send(Ok(()));
705            }
706            TopicMessage::Unsubscribe { id } => {
707                state.subscribers.remove(&id);
708                publish_topic_slot_table(state);
709                state.shared.notify_space();
710            }
711        }
712        Ok(())
713    }
714
715    async fn post_stop(
716        &self,
717        _myself: ActorRef<Self::Msg>,
718        state: &mut Self::State,
719    ) -> Result<(), ActorProcessingErr> {
720        if !state.closed {
721            for slot in state.subscribers.values() {
722                slot.fail(StreamError::ActorTerminated);
723            }
724            state.subscribers.clear();
725            publish_topic_slot_table(state);
726            state.shared.mark_actor_terminated();
727        }
728        Ok(())
729    }
730}
731
732fn close_topic<T: Send + Sync + 'static>(state: &mut TopicActorState<T>) {
733    if state.closed {
734        return;
735    }
736    match state.shared.lifecycle.compare_exchange(
737        TOPIC_OPEN,
738        TOPIC_CLOSING,
739        Ordering::AcqRel,
740        Ordering::Acquire,
741    ) {
742        Ok(_) => {}
743        Err(TOPIC_CLOSED) => {
744            state.closed = true;
745            return;
746        }
747        Err(_) => {}
748    }
749    state.shared.wait_for_publishers_to_drain();
750    state
751        .shared
752        .lifecycle
753        .store(TOPIC_CLOSED, Ordering::Release);
754
755    for slot in state.subscribers.values() {
756        slot.complete();
757    }
758    state.subscribers.clear();
759    publish_topic_slot_table(state);
760    state.shared.notify_space();
761    state.shared.closed_notified.notify_waiters();
762    state.closed = true;
763}
764
765fn publish_topic_slot_table<T: Send + Sync + 'static>(state: &TopicActorState<T>) {
766    let slots = state.subscribers.values().cloned().collect::<Vec<_>>();
767    state
768        .shared
769        .subscribers
770        .store(Arc::new(TopicSlotTable { slots }));
771}
772
773fn closed_error() -> StreamError {
774    StreamError::Failed("topic is closed".into())
775}
776
777struct TopicStream<T: Clone + Send + Sync + 'static> {
778    shared: Arc<TopicShared<T>>,
779    slot: Arc<TopicSlot<T>>,
780    pending: VecDeque<Arc<T>>,
781    terminated: bool,
782}
783
784#[doc(hidden)]
785pub struct TopicBenchmarkStream<T: Clone + Send + Sync + 'static> {
786    shared: Arc<TopicShared<T>>,
787    slot: Arc<TopicSlot<T>>,
788    pending: VecDeque<Arc<T>>,
789    terminated: bool,
790}
791
792impl<T: Clone + Send + Sync + 'static> Iterator for TopicStream<T> {
793    type Item = StreamResult<T>;
794
795    fn next(&mut self) -> Option<Self::Item> {
796        if self.terminated {
797            return None;
798        }
799
800        loop {
801            if let Some(value) = self.pending.pop_front() {
802                return Some(Ok(value.as_ref().clone()));
803            }
804
805            if let Some(value) = self.drain_batch() {
806                return Some(Ok(value.as_ref().clone()));
807            }
808
809            if let Some(terminal) = self.slot.terminal() {
810                self.terminated = true;
811                return match terminal {
812                    TopicSlotTerminal::Complete => None,
813                    TopicSlotTerminal::Error(error) => Some(Err(error)),
814                };
815            }
816
817            if current_stream_cancelled()
818                .as_ref()
819                .is_some_and(|cancelled| cancelled.load(Ordering::SeqCst))
820            {
821                self.terminated = true;
822                return Some(Err(StreamError::Cancelled));
823            }
824
825            self.wait_for_wake();
826        }
827    }
828}
829
830impl<T: Clone + Send + Sync + 'static> TopicStream<T> {
831    fn drain_batch(&mut self) -> Option<Arc<T>> {
832        let first = self.slot.pop()?;
833        let mut drained = 1_usize;
834        while drained < TOPIC_DRAIN_BATCH {
835            let Some(value) = self.slot.pop() else {
836                break;
837            };
838            self.pending.push_back(value);
839            drained += 1;
840        }
841        self.shared.notify_space();
842        Some(first)
843    }
844
845    fn wait_for_wake(&self) {
846        let guard = self
847            .slot
848            .available_lock
849            .lock()
850            .unwrap_or_else(|poison| poison.into_inner());
851        self.slot.park();
852        fence(Ordering::SeqCst);
853        if !self.slot.buffer.is_empty() || self.slot.terminal().is_some() {
854            self.slot.unpark();
855            return;
856        }
857        let _guard = self
858            .slot
859            .available
860            .wait_timeout(guard, SLOT_WAIT_BACKSTOP)
861            .unwrap_or_else(|poison| poison.into_inner())
862            .0;
863        self.slot.unpark();
864    }
865}
866
867impl<T: Clone + Send + Sync + 'static> Drop for TopicStream<T> {
868    fn drop(&mut self) {
869        self.slot.unsubscribe();
870        self.shared.notify_space();
871    }
872}
873
874impl<T: Clone + Send + Sync + 'static> TopicBenchmarkStream<T> {
875    #[doc(hidden)]
876    pub async fn next(&mut self) -> Option<StreamResult<T>> {
877        if self.terminated {
878            return None;
879        }
880
881        loop {
882            if let Some(value) = self.pending.pop_front() {
883                return Some(Ok(value.as_ref().clone()));
884            }
885
886            if let Some(value) = self.drain_batch() {
887                return Some(Ok(value.as_ref().clone()));
888            }
889
890            if let Some(terminal) = self.slot.terminal() {
891                self.terminated = true;
892                return match terminal {
893                    TopicSlotTerminal::Complete => None,
894                    TopicSlotTerminal::Error(error) => Some(Err(error)),
895                };
896            }
897
898            self.wait_for_wake().await;
899        }
900    }
901
902    #[doc(hidden)]
903    pub async fn count_items(&mut self, target: u64) -> StreamResult<u64> {
904        let mut count = 0_u64;
905        while count < target {
906            if self.terminated {
907                return Err(StreamError::Failed(
908                    "topic stream ended before requested count".into(),
909                ));
910            }
911
912            if !self.pending.is_empty() {
913                let drained = self.pending.len().min((target - count) as usize);
914                self.pending.drain(..drained);
915                count += drained as u64;
916                continue;
917            }
918
919            if let Some(drained) = self.drain_available_count((target - count) as usize) {
920                count += drained as u64;
921                continue;
922            }
923
924            if let Some(terminal) = self.slot.terminal() {
925                self.terminated = true;
926                return match terminal {
927                    TopicSlotTerminal::Complete => Err(StreamError::Failed(
928                        "topic stream completed before requested count".into(),
929                    )),
930                    TopicSlotTerminal::Error(error) => Err(error),
931                };
932            }
933
934            self.wait_for_wake().await;
935        }
936        Ok(count)
937    }
938
939    fn drain_batch(&mut self) -> Option<Arc<T>> {
940        let first = self.slot.pop()?;
941        let mut drained = 1_usize;
942        while drained < TOPIC_DRAIN_BATCH {
943            let Some(value) = self.slot.pop() else {
944                break;
945            };
946            self.pending.push_back(value);
947            drained += 1;
948        }
949        self.shared.notify_space();
950        Some(first)
951    }
952
953    fn drain_available_count(&mut self, limit: usize) -> Option<usize> {
954        let mut drained = 0_usize;
955        let limit = limit.min(TOPIC_DRAIN_BATCH);
956        while drained < limit {
957            let Some(_value) = self.slot.pop() else {
958                break;
959            };
960            drained += 1;
961        }
962        if drained == 0 {
963            return None;
964        }
965        self.shared.notify_space();
966        Some(drained)
967    }
968
969    async fn wait_for_wake(&self) {
970        let notified = self.slot.async_available.notified();
971        tokio::pin!(notified);
972        notified.as_mut().enable();
973
974        self.slot.park();
975        fence(Ordering::SeqCst);
976        if !self.slot.buffer.is_empty() || self.slot.terminal().is_some() {
977            self.slot.unpark();
978            return;
979        }
980
981        notified.as_mut().await;
982        self.slot.unpark();
983    }
984}
985
986impl<T: Clone + Send + Sync + 'static> Drop for TopicBenchmarkStream<T> {
987    fn drop(&mut self) {
988        self.slot.unsubscribe();
989        self.shared.notify_space();
990    }
991}
992
993impl<T: Send + Sync + 'static> fmt::Debug for Topic<T> {
994    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
995        f.debug_struct("Topic")
996            .field("closed", &self.is_closed())
997            .field("subscribers", &self.subscriber_count())
998            .finish_non_exhaustive()
999    }
1000}
1001
1002impl<T> fmt::Display for TopicPublishError<T> {
1003    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1004        match self {
1005            TopicPublishError::Closed(_) => f.write_str("topic is closed"),
1006        }
1007    }
1008}
1009
1010impl<T> fmt::Display for TopicTryPublishError<T> {
1011    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1012        match self {
1013            TopicTryPublishError::Closed(_) => f.write_str("topic is closed"),
1014            TopicTryPublishError::Full(_) => f.write_str("topic subscriber buffer is full"),
1015            TopicTryPublishError::Busy(_) => f.write_str("topic publish turn is busy"),
1016        }
1017    }
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022    use super::*;
1023    use crate::{Sink, stream::Materializer};
1024    use futures::executor::block_on;
1025    use std::{
1026        collections::HashSet,
1027        sync::{
1028            Arc,
1029            atomic::{AtomicBool, AtomicUsize},
1030        },
1031        thread,
1032        time::{Duration, Instant},
1033    };
1034
1035    fn wait<T>(completion: crate::StreamCompletion<T>) -> T {
1036        completion.wait().unwrap()
1037    }
1038
1039    fn wait_until<F>(timeout: Duration, mut condition: F) -> bool
1040    where
1041        F: FnMut() -> bool,
1042    {
1043        let deadline = Instant::now() + timeout;
1044        while Instant::now() < deadline {
1045            if condition() {
1046                return true;
1047            }
1048            thread::yield_now();
1049            thread::sleep(Duration::from_millis(1));
1050        }
1051        condition()
1052    }
1053
1054    fn materialize_topic<T: Clone + Send + Sync + 'static>(topic: &Topic<T>) -> BoxStream<T> {
1055        let materializer = Materializer::new();
1056        let (stream, _) = topic.subscribe().factory.create(&materializer).unwrap();
1057        stream
1058    }
1059
1060    #[test]
1061    fn every_subscriber_sees_every_post_subscription_element() {
1062        const SUBSCRIBERS: usize = 4;
1063        const PUBLISHERS: usize = 4;
1064        const PER_PUBLISHER: usize = 128;
1065        let topic = Topic::new(1_024, TopicOverflow::Backpressure).unwrap();
1066        let completions = (0..SUBSCRIBERS)
1067            .map(|_| topic.subscribe().run_with(Sink::collect()).unwrap())
1068            .collect::<Vec<_>>();
1069        assert!(wait_until(Duration::from_secs(1), || topic
1070            .subscriber_count()
1071            == SUBSCRIBERS));
1072
1073        let mut handles = Vec::new();
1074        for publisher in 0..PUBLISHERS {
1075            let topic = topic.clone();
1076            handles.push(thread::spawn(move || {
1077                for seq in 0..PER_PUBLISHER {
1078                    let value = ((publisher as u64) << 32) | seq as u64;
1079                    block_on(topic.publish(value)).unwrap();
1080                }
1081            }));
1082        }
1083        for handle in handles {
1084            handle.join().unwrap();
1085        }
1086        topic.close().unwrap();
1087
1088        let observed = completions.into_iter().map(wait).collect::<Vec<_>>();
1089        let first = observed.first().unwrap();
1090        assert_eq!(first.len(), PUBLISHERS * PER_PUBLISHER);
1091        let unique = first.iter().copied().collect::<HashSet<_>>();
1092        assert_eq!(unique.len(), PUBLISHERS * PER_PUBLISHER);
1093        for values in &observed[1..] {
1094            assert_eq!(values, first, "subscribers disagreed on global order");
1095        }
1096    }
1097
1098    #[test]
1099    fn late_subscriber_sees_nothing_prior_and_zero_subscriber_publish_drops() {
1100        let topic = Topic::new(8, TopicOverflow::Backpressure).unwrap();
1101        assert_eq!(topic.subscriber_count(), 0);
1102        block_on(topic.publish(1_u64)).unwrap();
1103        topic.try_publish(2).unwrap();
1104
1105        let completion = topic.subscribe().run_with(Sink::collect()).unwrap();
1106        assert!(wait_until(Duration::from_secs(1), || topic
1107            .subscriber_count()
1108            == 1));
1109        block_on(topic.publish(3)).unwrap();
1110        topic.close().unwrap();
1111        assert_eq!(wait(completion), vec![3]);
1112    }
1113
1114    #[test]
1115    fn sliding_overflow_drops_oldest_for_slow_subscriber() {
1116        let topic = Topic::new(2, TopicOverflow::Sliding).unwrap();
1117        let mut stream = materialize_topic(&topic);
1118        topic.try_publish(1_u64).unwrap();
1119        topic.try_publish(2).unwrap();
1120        topic.try_publish(3).unwrap();
1121        topic.try_publish(4).unwrap();
1122        topic.close().unwrap();
1123
1124        assert_eq!(stream.next(), Some(Ok(3)));
1125        assert_eq!(stream.next(), Some(Ok(4)));
1126        assert_eq!(stream.next(), None);
1127    }
1128
1129    #[test]
1130    fn dropping_overflow_drops_newest_for_slow_subscriber() {
1131        let topic = Topic::new(2, TopicOverflow::Dropping).unwrap();
1132        let mut stream = materialize_topic(&topic);
1133        topic.try_publish(1_u64).unwrap();
1134        topic.try_publish(2).unwrap();
1135        topic.try_publish(3).unwrap();
1136        topic.try_publish(4).unwrap();
1137        topic.close().unwrap();
1138
1139        assert_eq!(stream.next(), Some(Ok(1)));
1140        assert_eq!(stream.next(), Some(Ok(2)));
1141        assert_eq!(stream.next(), None);
1142    }
1143
1144    #[test]
1145    fn backpressure_stalls_publisher_until_slow_subscriber_drains() {
1146        let topic = Topic::new(1, TopicOverflow::Backpressure).unwrap();
1147        let mut stream = materialize_topic(&topic);
1148        block_on(topic.publish(1_u64)).unwrap();
1149
1150        let completed = Arc::new(AtomicBool::new(false));
1151        let publisher_completed = Arc::clone(&completed);
1152        let publisher = {
1153            let topic = topic.clone();
1154            thread::spawn(move || {
1155                block_on(topic.publish(2)).unwrap();
1156                publisher_completed.store(true, Ordering::SeqCst);
1157            })
1158        };
1159
1160        assert!(!wait_until(Duration::from_millis(25), || completed
1161            .load(Ordering::SeqCst)));
1162        assert_eq!(stream.next(), Some(Ok(1)));
1163        assert!(wait_until(Duration::from_secs(1), || completed.load(Ordering::SeqCst)));
1164        publisher.join().unwrap();
1165        topic.close().unwrap();
1166        assert_eq!(stream.next(), Some(Ok(2)));
1167        assert_eq!(stream.next(), None);
1168    }
1169
1170    #[test]
1171    fn dropping_source_unsubscribes_and_frees_backpressured_slot() {
1172        let topic = Topic::new(1, TopicOverflow::Backpressure).unwrap();
1173        let stream = materialize_topic(&topic);
1174        block_on(topic.publish(1_u64)).unwrap();
1175        assert_eq!(topic.subscriber_count(), 1);
1176        drop(stream);
1177        assert!(wait_until(Duration::from_secs(1), || topic
1178            .subscriber_count()
1179            == 0));
1180        block_on(topic.publish(2)).unwrap();
1181        topic.close().unwrap();
1182    }
1183
1184    #[test]
1185    fn close_drains_then_completes_and_rejects_late_publishes() {
1186        let topic = Topic::new(4, TopicOverflow::Backpressure).unwrap();
1187        let mut stream = materialize_topic(&topic);
1188        block_on(topic.publish(1_u64)).unwrap();
1189        block_on(topic.publish(2)).unwrap();
1190        topic.close().unwrap();
1191
1192        assert_eq!(stream.next(), Some(Ok(1)));
1193        assert_eq!(stream.next(), Some(Ok(2)));
1194        assert_eq!(stream.next(), None);
1195        assert_eq!(topic.try_publish(3), Err(TopicTryPublishError::Closed(3)));
1196        assert_eq!(
1197            block_on(topic.publish(4)),
1198            Err(TopicPublishError::Closed(4))
1199        );
1200    }
1201
1202    #[test]
1203    fn closed_future_wakes_on_close() {
1204        let topic = Topic::<u64>::new(1, TopicOverflow::Backpressure).unwrap();
1205        let waiting = Arc::new(AtomicBool::new(false));
1206        let waiter_started = Arc::clone(&waiting);
1207        let waiter = {
1208            let topic = topic.clone();
1209            thread::spawn(move || {
1210                waiter_started.store(true, Ordering::SeqCst);
1211                block_on(topic.closed());
1212            })
1213        };
1214
1215        assert!(wait_until(Duration::from_secs(1), || waiting.load(Ordering::SeqCst)));
1216        topic.close().unwrap();
1217        waiter.join().unwrap();
1218        assert!(topic.is_closed());
1219    }
1220
1221    #[test]
1222    fn publisher_subscriber_churn_hammer() {
1223        const ROUNDS: usize = 200;
1224        const PUBLISHERS: usize = 4;
1225        let topic = Topic::new(8, TopicOverflow::Sliding).unwrap();
1226        let published = Arc::new(AtomicUsize::new(0));
1227
1228        let mut publisher_handles = Vec::new();
1229        for publisher in 0..PUBLISHERS {
1230            let topic = topic.clone();
1231            let published = Arc::clone(&published);
1232            publisher_handles.push(thread::spawn(move || {
1233                for seq in 0..ROUNDS {
1234                    let value = ((publisher as u64) << 32) | seq as u64;
1235                    block_on(topic.publish(value)).unwrap();
1236                    published.fetch_add(1, Ordering::Relaxed);
1237                }
1238            }));
1239        }
1240
1241        let churn_topic = topic.clone();
1242        let churn = thread::spawn(move || {
1243            for _ in 0..ROUNDS {
1244                let stream = materialize_topic(&churn_topic);
1245                drop(stream);
1246            }
1247        });
1248
1249        for handle in publisher_handles {
1250            handle.join().unwrap();
1251        }
1252        churn.join().unwrap();
1253        topic.close().unwrap();
1254        assert_eq!(published.load(Ordering::Relaxed), PUBLISHERS * ROUNDS);
1255    }
1256
1257    #[test]
1258    fn post_close_subscribe_completes_empty() {
1259        let topic = Topic::<u64>::new(8, TopicOverflow::Backpressure).unwrap();
1260        topic.close().unwrap();
1261        assert_eq!(topic.subscribe().run_collect().unwrap(), Vec::<u64>::new());
1262    }
1263}