cortexkit_bus_inmemory/
stream.rs1use 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
13pub struct InMemoryStreamCursor {
18 bus: InMemoryBus,
19 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 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 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 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 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
233impl 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
269fn 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 assert!(matches!(
307 bus.consumer(DurableOwner::Agent("prefrontal-core")).await,
308 Err(BusError::Absent { .. })
309 ));
310 assert!(matches!(
312 bus.consumer(DurableOwner::Module("prefrontal.core")).await,
313 Err(BusError::Denied { .. })
314 ));
315 }
316}