Skip to main content

ruststream_rdkafka/testing/
subscriber.rs

1//! The in-process subscriber and its delivery type.
2
3use std::fmt;
4use std::sync::Arc;
5
6use futures::Stream;
7use ruststream::testing::Coordinator;
8use ruststream::{AckError, BatchSubscriber, Headers, IncomingMessage, Partitioned, Subscriber};
9
10use super::broker::TestBrokerState;
11use super::router::{DeliveryReceiver, DeliverySender, SubscriptionId, TestDelivery};
12use crate::error::KafkaError;
13
14/// In-process subscriber on one topic name.
15///
16/// Yielded messages settle like the routing contract expects: ack finalizes, `nack(true)`
17/// re-enqueues to this same subscription, `nack(false)` drops. The real transport's
18/// committed-position semantics (holes, watermarks, redelivery on rebalance) are deliberately
19/// not simulated.
20pub struct KafkaTestSubscriber {
21    state: Arc<TestBrokerState>,
22    ids: Vec<SubscriptionId>,
23    topic: String,
24    sender: DeliverySender,
25    receiver: DeliveryReceiver,
26    coordinator: Option<Coordinator>,
27}
28
29impl KafkaTestSubscriber {
30    pub(crate) fn open_many(state: &Arc<TestBrokerState>, topics: &[String]) -> Self {
31        let (ids, sender, receiver) = state.router.subscribe_many(topics);
32        let coordinator = state.coordinator();
33        Self {
34            state: Arc::clone(state),
35            ids,
36            topic: topics.join(","),
37            sender,
38            receiver,
39            coordinator,
40        }
41    }
42
43    /// The subscribed topic name(s), joined with `,` when there are several.
44    #[must_use]
45    pub fn topic(&self) -> &str {
46        &self.topic
47    }
48}
49
50impl Drop for KafkaTestSubscriber {
51    fn drop(&mut self) {
52        for id in &self.ids {
53            self.state.router.unsubscribe(*id);
54        }
55    }
56}
57
58impl fmt::Debug for KafkaTestSubscriber {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.debug_struct("KafkaTestSubscriber")
61            .field("topic", &self.topic)
62            .finish_non_exhaustive()
63    }
64}
65
66impl Subscriber for KafkaTestSubscriber {
67    type Message = KafkaTestMessage;
68    type Error = KafkaError;
69
70    /// Streams injected deliveries; never yields an error.
71    ///
72    /// # Cancel safety
73    ///
74    /// Cancel safe and re-enterable: the receiver is polled in place, so dropping the returned
75    /// stream loses nothing and `stream` can be called again.
76    fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
77        let Self {
78            receiver,
79            sender,
80            coordinator,
81            ..
82        } = self;
83        futures::stream::poll_fn(move |cx| {
84            receiver.poll_recv(cx).map(|delivery| {
85                delivery.map(|delivery| {
86                    Ok(KafkaTestMessage {
87                        delivery: Some(delivery),
88                        sender: sender.clone(),
89                        coordinator: coordinator.clone(),
90                    })
91                })
92            })
93        })
94    }
95}
96
97impl BatchSubscriber for KafkaTestSubscriber {
98    type Batch = Vec<KafkaTestMessage>;
99
100    /// Streams non-empty pages natively: each waits for one delivery, then drains whatever
101    /// else is already enqueued (mirroring the real subscriber's drain-what-is-fetched
102    /// behavior).
103    ///
104    /// # Cancel safety
105    ///
106    /// Same guarantees as [`Subscriber::stream`]: cancel safe between polls.
107    fn batches(
108        &mut self,
109    ) -> impl Stream<Item = Result<Self::Batch, <Self as Subscriber>::Error>> + Send + '_ {
110        let Self {
111            receiver,
112            sender,
113            coordinator,
114            ..
115        } = self;
116        futures::stream::poll_fn(move |cx| {
117            receiver.poll_recv(cx).map(|delivery| {
118                delivery.map(|first| {
119                    let mut batch = vec![KafkaTestMessage {
120                        delivery: Some(first),
121                        sender: sender.clone(),
122                        coordinator: coordinator.clone(),
123                    }];
124                    while let Ok(delivery) = receiver.try_recv() {
125                        batch.push(KafkaTestMessage {
126                            delivery: Some(delivery),
127                            sender: sender.clone(),
128                            coordinator: coordinator.clone(),
129                        });
130                    }
131                    Ok(batch)
132                })
133            })
134        })
135    }
136}
137
138/// One in-process delivery.
139pub struct KafkaTestMessage {
140    delivery: Option<TestDelivery>,
141    sender: DeliverySender,
142    coordinator: Option<Coordinator>,
143}
144
145impl KafkaTestMessage {
146    fn take(&mut self) -> TestDelivery {
147        // The settle methods consume `self`, so a second settle cannot compile; reaching this
148        // twice is an internal invariant violation.
149        self.delivery
150            .take()
151            .expect("KafkaTestMessage settled twice")
152    }
153}
154
155impl Drop for KafkaTestMessage {
156    fn drop(&mut self) {
157        // Balance the router's `enqueued` exactly once per delivery, whatever the dispatch
158        // path did (ack, nack, panic, or plain drop).
159        if let Some(coordinator) = self.coordinator.take() {
160            coordinator.consumed();
161        }
162    }
163}
164
165impl fmt::Debug for KafkaTestMessage {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        f.debug_struct("KafkaTestMessage")
168            .field("delivery", &self.delivery)
169            .finish_non_exhaustive()
170    }
171}
172
173impl IncomingMessage for KafkaTestMessage {
174    fn payload(&self) -> &[u8] {
175        &self
176            .delivery
177            .as_ref()
178            .expect("message accessed after settlement")
179            .payload
180    }
181
182    fn headers(&self) -> &Headers {
183        &self
184            .delivery
185            .as_ref()
186            .expect("message accessed after settlement")
187            .headers
188    }
189
190    /// The partition key from the `PARTITION_KEY_HEADER`, mirroring the real message so keyed
191    /// worker lanes behave the same in-process.
192    fn partition_key(&self) -> Option<&[u8]> {
193        self.headers().get(crate::PARTITION_KEY_HEADER)
194    }
195
196    /// Finalizes the delivery.
197    ///
198    /// # Errors
199    ///
200    /// Never fails; the in-process transport has no position to store.
201    async fn ack(mut self) -> Result<(), AckError> {
202        drop(self.take());
203        Ok(())
204    }
205
206    /// Re-enqueues to the same subscription (`requeue = true`) or drops (`requeue = false`).
207    ///
208    /// # Errors
209    ///
210    /// Never fails; the in-process transport has no position to store.
211    async fn nack(mut self, requeue: bool) -> Result<(), AckError> {
212        let delivery = self.take();
213        if requeue && self.sender.send(delivery).is_ok() {
214            // This bypasses the router fanout, so account for the new in-flight delivery here.
215            if let Some(coordinator) = &self.coordinator {
216                coordinator.enqueued();
217            }
218        }
219        Ok(())
220    }
221}
222
223impl Partitioned for KafkaTestMessage {
224    /// The partition key from the `PARTITION_KEY_HEADER`, mirroring the real message.
225    fn partition_key(&self) -> Option<&[u8]> {
226        self.headers().get(crate::PARTITION_KEY_HEADER)
227    }
228}