Skip to main content

cortexkit_bus_inmemory/
stream.rs

1use std::time::{Duration, Instant};
2
3use async_trait::async_trait;
4use cortexkit_bus_trait::{
5    BusError, BusResult, ContentDigest, DurableOwner, Headers, Message, PublishAck, Stream,
6    StreamCursor, StreamDelivery,
7};
8
9use crate::backend::{
10    durable_name, BackendEvent, DurableState, InMemoryBus, Lease, QueueEntry, StoredMessage,
11};
12
13/// A cursor on one durable. Several cursors may share a durable, as several
14/// pullers may share a JetStream durable: an unsettled delivery is withheld
15/// from the others until its ack wait expires, then redelivered to whichever
16/// pulls next.
17pub struct InMemoryStreamCursor {
18    bus: InMemoryBus,
19    /// The durable's name.
20    key: String,
21    id: u64,
22    in_flight_index: Option<usize>,
23}
24
25#[async_trait]
26impl Stream for InMemoryBus {
27    type Cursor = InMemoryStreamCursor;
28
29    async fn publish(
30        &self,
31        subject: &str,
32        id: &str,
33        digest: ContentDigest,
34        headers: Headers,
35    ) -> BusResult<PublishAck> {
36        let mut state = self.inner.lock().expect("in-memory bus poisoned");
37        if state.config.denied_subjects.contains(subject) {
38            state
39                .permission_violations
40                .push_back(cortexkit_bus_trait::AsyncPermissionViolation {
41                    subject: subject.into(),
42                    reason: "fixture-configured publish denial".into(),
43                });
44            return Err(BusError::denied(
45                subject,
46                "fixture-configured publish denial",
47            ));
48        }
49
50        let message = Message {
51            subject: subject.into(),
52            id: id.into(),
53            digest,
54            headers,
55        };
56        if let Some(config) = &state.config.work_queue {
57            if subject == config.subject {
58                let queued_bytes = state
59                    .queue
60                    .iter()
61                    .map(|entry| entry.message.wire_size())
62                    .sum::<usize>();
63                if queued_bytes + message.wire_size() > config.max_bytes {
64                    return Err(BusError::unavailable_limit(
65                        "max_bytes",
66                        format!("work queue {} is full", config.subject),
67                    ));
68                }
69            }
70        }
71
72        let stream_seq = state.next_stream_seq;
73        state.next_stream_seq += 1;
74        state.stream_messages.push(StoredMessage {
75            stream_seq,
76            message: message.clone(),
77        });
78        if state
79            .config
80            .work_queue
81            .as_ref()
82            .is_some_and(|config| subject == config.subject)
83        {
84            let token = cortexkit_bus_trait::DeliveryToken(state.next_delivery_token);
85            state.next_delivery_token += 1;
86            state.queue.push_back(QueueEntry {
87                token,
88                message: message.clone(),
89                delivery_count: 0,
90                available_at: Instant::now(),
91                in_flight: false,
92            });
93        }
94        state.events.push(BackendEvent::Published {
95            subject: subject.into(),
96            id: id.into(),
97        });
98        Ok(PublishAck { stream_seq })
99    }
100
101    async fn consumer(&self, owner: DurableOwner<'_>) -> BusResult<Self::Cursor> {
102        let key = durable_name(owner)?;
103        let mut state = self.inner.lock().expect("in-memory bus poisoned");
104        let Some(durable_state) = state.durable_cursors.get_mut(&key) else {
105            return Err(BusError::absent(format!("durable consumer {key}")));
106        };
107        durable_state.next_cursor_id += 1;
108        let id = durable_state.next_cursor_id;
109        Ok(InMemoryStreamCursor {
110            bus: self.clone(),
111            key,
112            id,
113            in_flight_index: None,
114        })
115    }
116}
117
118#[async_trait]
119impl StreamCursor for InMemoryStreamCursor {
120    async fn next(&mut self) -> BusResult<Option<StreamDelivery>> {
121        if self.in_flight_index.is_some() {
122            return Err(BusError::unavailable(
123                "the current delivery must be acknowledged, negatively acknowledged or terminated",
124            ));
125        }
126
127        let mut guard = self.bus.inner.lock().expect("in-memory bus poisoned");
128        let state = &mut *guard;
129        let ack_wait = state.config.stream_ack_wait;
130        let durable = state
131            .durable_cursors
132            .get_mut(&self.key)
133            .expect("durable disappeared after binding");
134        let now = Instant::now();
135        // Another cursor holds the head message and its ack wait has not run out.
136        if durable
137            .lease
138            .as_ref()
139            .is_some_and(|lease| lease.deadline > now)
140        {
141            return Ok(None);
142        }
143        if durable
144            .available_at
145            .is_some_and(|available_at| available_at > now)
146        {
147            return Ok(None);
148        }
149        let Some(stored) = state.stream_messages.get(durable.next_index) else {
150            return Ok(None);
151        };
152        durable.delivery_count += 1;
153        durable.available_at = None;
154        durable.lease = Some(Lease {
155            cursor_id: self.id,
156            deadline: now + ack_wait,
157        });
158        self.in_flight_index = Some(durable.next_index);
159        Ok(Some(StreamDelivery {
160            stream_seq: stored.stream_seq,
161            delivery_count: durable.delivery_count,
162            message: stored.message.clone(),
163        }))
164    }
165
166    async fn ack(&mut self) -> BusResult<()> {
167        let index = self.take_in_flight()?;
168        let mut state = self.bus.inner.lock().expect("in-memory bus poisoned");
169        let durable = durable_mut(&mut state.durable_cursors, &self.key);
170        // A late ack, after the ack wait expired and another cursor settled
171        // the message, finds the head already moved on and changes nothing.
172        if durable.next_index == index {
173            advance(durable);
174        }
175        Ok(())
176    }
177
178    async fn nak(&mut self, delay: Duration) -> BusResult<()> {
179        let index = self.take_in_flight()?;
180        let mut state = self.bus.inner.lock().expect("in-memory bus poisoned");
181        let durable = durable_mut(&mut state.durable_cursors, &self.key);
182        if durable.next_index == index && holds_lease(durable, self.id) {
183            durable.lease = None;
184            durable.available_at = Some(Instant::now() + delay);
185        }
186        Ok(())
187    }
188
189    async fn term(&mut self, reason: Option<&str>) -> BusResult<()> {
190        let index = self.take_in_flight()?;
191        let mut guard = self.bus.inner.lock().expect("in-memory bus poisoned");
192        let state = &mut *guard;
193        let durable = durable_mut(&mut state.durable_cursors, &self.key);
194        if durable.next_index == index {
195            // Terminating moves the durable past the message exactly as an ack
196            // does, so it is never delivered again; only the record differs.
197            advance(durable);
198            state.events.push(BackendEvent::StreamTerminated {
199                stream_seq: state.stream_messages[index].stream_seq,
200                reason: reason.map(str::to_owned),
201            });
202        }
203        Ok(())
204    }
205
206    async fn in_progress(&mut self) -> BusResult<()> {
207        let index = self
208            .in_flight_index
209            .ok_or_else(|| BusError::absent("in-flight stream delivery"))?;
210        let mut guard = self.bus.inner.lock().expect("in-memory bus poisoned");
211        let state = &mut *guard;
212        let ack_wait = state.config.stream_ack_wait;
213        let durable = durable_mut(&mut state.durable_cursors, &self.key);
214        // Once the message has been redelivered to another cursor, this cursor
215        // no longer holds it and its report changes nothing.
216        if durable.next_index == index && holds_lease(durable, self.id) {
217            if let Some(lease) = durable.lease.as_mut() {
218                lease.deadline = Instant::now() + ack_wait;
219            }
220        }
221        Ok(())
222    }
223}
224
225impl InMemoryStreamCursor {
226    fn take_in_flight(&mut self) -> BusResult<usize> {
227        self.in_flight_index
228            .take()
229            .ok_or_else(|| BusError::absent("in-flight stream delivery"))
230    }
231}
232
233/// A dropped cursor gives its unsettled delivery back at once instead of after
234/// the ack wait, so a reopened cursor in the same process replays it without
235/// waiting (JetStream would wait for the ack wait to run out). The delivery
236/// still counts towards the message's delivery count.
237impl Drop for InMemoryStreamCursor {
238    fn drop(&mut self) {
239        let Some(index) = self.in_flight_index else {
240            return;
241        };
242        let Ok(mut state) = self.bus.inner.lock() else {
243            return;
244        };
245        if let Some(durable) = state.durable_cursors.get_mut(&self.key) {
246            if durable.next_index == index && holds_lease(durable, self.id) {
247                durable.lease = None;
248            }
249        }
250    }
251}
252
253fn durable_mut<'a>(
254    durables: &'a mut std::collections::HashMap<String, DurableState>,
255    key: &str,
256) -> &'a mut DurableState {
257    durables
258        .get_mut(key)
259        .expect("durable disappeared after binding")
260}
261
262fn holds_lease(durable: &DurableState, cursor_id: u64) -> bool {
263    durable
264        .lease
265        .as_ref()
266        .is_some_and(|lease| lease.cursor_id == cursor_id)
267}
268
269/// Moves the durable past its head message, which is never delivered again.
270fn advance(durable: &mut DurableState) {
271    durable.next_index += 1;
272    durable.available_at = None;
273    durable.delivery_count = 0;
274    durable.lease = None;
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use crate::InMemoryConfig;
281
282    #[tokio::test]
283    async fn a_cursor_binds_the_durable_of_the_kind_its_caller_names() {
284        let bus = InMemoryBus::new(
285            InMemoryConfig::default().with_durable(DurableOwner::Module("prefrontal-core")),
286        );
287        bus.publish(
288            "ck.box.room.room_a.post",
289            "room:room_a:1",
290            ContentDigest::of_bytes(b"post"),
291            Headers::new(),
292        )
293        .await
294        .expect("publish");
295
296        let mut cursor = bus
297            .consumer(DurableOwner::Module("prefrontal-core"))
298            .await
299            .expect("the module durable binds");
300        let delivery = cursor.next().await.expect("next").expect("a delivery");
301        assert_eq!(delivery.message.id, "room:room_a:1");
302        cursor.ack().await.expect("ack");
303
304        // The same id as an agent names `c_prefrontal-core`, which was never
305        // created: the kind, not the string, decides the durable.
306        assert!(matches!(
307            bus.consumer(DurableOwner::Agent("prefrontal-core")).await,
308            Err(BusError::Absent { .. })
309        ));
310        // An id that is not a single subject token is refused before lookup.
311        assert!(matches!(
312            bus.consumer(DurableOwner::Module("prefrontal.core")).await,
313            Err(BusError::Denied { .. })
314        ));
315    }
316}