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