1use crate::models::StreamBufferConfig;
110use crate::traits::{
111 BatchCommitFunc, BoxFuture, ConsumerError, EndpointStatus, MessageConsumer, MessageDisposition,
112 MessagePublisher, PublisherError, ReceivedBatch, Sent, SentBatch,
113};
114use crate::CanonicalMessage;
115use anyhow::anyhow;
116use async_channel::{bounded, Receiver, Sender};
117use async_trait::async_trait;
118use once_cell::sync::Lazy;
119use std::any::Any;
120use std::collections::HashMap;
121use std::sync::{Arc, Mutex};
122use tracing::{trace, warn};
123
124const DEFAULT_CAPACITY: usize = 100;
125
126#[derive(Clone)]
127struct StreamPartition {
128 sender: Sender<Vec<CanonicalMessage>>,
129 receiver: Receiver<Vec<CanonicalMessage>>,
130}
131
132impl StreamPartition {
133 fn new(capacity: usize) -> Self {
134 let (sender, receiver) = bounded(capacity);
135 Self { sender, receiver }
136 }
137}
138
139type StreamTopic = HashMap<String, StreamPartition>;
140
141static STREAM_BUFFERS: Lazy<Mutex<HashMap<String, Arc<Mutex<StreamTopic>>>>> =
142 Lazy::new(|| Mutex::new(HashMap::new()));
143
144fn get_or_create_topic(topic: &str) -> Arc<Mutex<StreamTopic>> {
145 let mut buffers = STREAM_BUFFERS
146 .lock()
147 .expect("stream buffer registry poisoned");
148 buffers
149 .entry(topic.to_string())
150 .or_insert_with(|| Arc::new(Mutex::new(HashMap::new())))
151 .clone()
152}
153
154fn get_or_create_partition(topic: &str, correlation_id: &str, capacity: usize) -> StreamPartition {
155 let topic = get_or_create_topic(topic);
156 let mut partitions = topic.lock().expect("stream buffer topic poisoned");
157 partitions
158 .entry(correlation_id.to_string())
159 .or_insert_with(|| StreamPartition::new(capacity))
160 .clone()
161}
162
163fn remove_partition_if_current(
164 topic: &str,
165 correlation_id: &str,
166 sender: &Sender<Vec<CanonicalMessage>>,
167) {
168 let topic = get_or_create_topic(topic);
169 let mut partitions = topic.lock().expect("stream buffer topic poisoned");
170 let should_remove = partitions
171 .get(correlation_id)
172 .is_some_and(|partition| partition.sender.same_channel(sender));
173 if should_remove {
174 partitions.remove(correlation_id);
175 sender.close();
176 }
177}
178
179#[derive(Debug, Clone)]
186pub struct StreamBufferPublisher {
187 topic: String,
188 capacity: usize,
189}
190
191impl StreamBufferPublisher {
192 pub fn new(config: &StreamBufferConfig) -> anyhow::Result<Self> {
193 Ok(Self {
194 topic: config.topic.clone(),
195 capacity: config.capacity.unwrap_or(DEFAULT_CAPACITY),
196 })
197 }
198}
199
200#[async_trait]
201impl MessagePublisher for StreamBufferPublisher {
202 async fn send(&self, message: CanonicalMessage) -> Result<Sent, PublisherError> {
203 let correlation_id = message
204 .metadata
205 .get("correlation_id")
206 .cloned()
207 .ok_or_else(|| {
208 PublisherError::NonRetryable(anyhow!(
209 "stream_buffer publisher requires message metadata 'correlation_id'"
210 ))
211 })?;
212 let partition = get_or_create_partition(&self.topic, &correlation_id, self.capacity);
213 partition
214 .sender
215 .send(vec![message])
216 .await
217 .map_err(|e| anyhow!("Failed to send to stream_buffer: {}", e))?;
218 Ok(Sent::Ack)
219 }
220
221 async fn send_batch(
222 &self,
223 messages: Vec<CanonicalMessage>,
224 ) -> Result<SentBatch, PublisherError> {
225 let mut messages = messages;
226 let Some(first) = messages.first() else {
227 return Ok(SentBatch::Ack);
228 };
229 let correlation_id = first
230 .metadata
231 .get("correlation_id")
232 .cloned()
233 .ok_or_else(|| {
234 PublisherError::NonRetryable(anyhow!(
235 "stream_buffer publisher requires message metadata 'correlation_id'"
236 ))
237 })?;
238 if messages
239 .iter()
240 .any(|message| message.metadata.get("correlation_id") != Some(&correlation_id))
241 {
242 return Err(PublisherError::NonRetryable(anyhow!(
243 "stream_buffer publisher batch requires a single shared correlation_id"
244 )));
245 }
246 let partition = get_or_create_partition(&self.topic, &correlation_id, self.capacity);
247 if partition.sender.is_closed() {
248 return Err(PublisherError::Retryable(anyhow!(
249 "stream_buffer partition is closed"
250 )));
251 }
252 partition
253 .sender
254 .send(std::mem::take(&mut messages))
255 .await
256 .map_err(|e| anyhow!("Failed to send to stream_buffer: {}", e))?;
257 Ok(SentBatch::Ack)
258 }
259
260 async fn status(&self) -> EndpointStatus {
261 EndpointStatus {
262 healthy: true,
263 target: self.topic.clone(),
264 ..Default::default()
265 }
266 }
267
268 fn as_any(&self) -> &dyn Any {
269 self
270 }
271}
272
273#[derive(Debug)]
279pub struct StreamBufferConsumer {
280 topic: String,
281 correlation_id: String,
282 sender: Sender<Vec<CanonicalMessage>>,
283 receiver: Receiver<Vec<CanonicalMessage>>,
284 buffer: Vec<CanonicalMessage>,
285}
286
287impl StreamBufferConsumer {
288 pub fn new(config: &StreamBufferConfig) -> anyhow::Result<Self> {
289 let correlation_id = config.correlation_id.clone().ok_or_else(|| {
290 anyhow!("stream_buffer consumer requires 'correlation_id' in its config")
291 })?;
292 let partition = get_or_create_partition(
293 &config.topic,
294 &correlation_id,
295 config.capacity.unwrap_or(DEFAULT_CAPACITY),
296 );
297 Ok(Self {
298 topic: config.topic.clone(),
299 correlation_id,
300 sender: partition.sender,
301 receiver: partition.receiver,
302 buffer: Vec::new(),
303 })
304 }
305
306 async fn get_buffered_messages(
307 &mut self,
308 max_messages: usize,
309 ) -> Result<Vec<CanonicalMessage>, ConsumerError> {
310 let max_messages = max_messages.max(1);
311 if self.buffer.is_empty() {
312 self.buffer = match self.receiver.recv().await {
313 Ok(batch) => batch,
314 Err(_) => return Err(ConsumerError::EndOfStream),
315 };
316 self.buffer.reverse();
317 }
318
319 let num_to_take = self.buffer.len().min(max_messages);
320 let split_at = self.buffer.len() - num_to_take;
321 let mut messages = self.buffer.split_off(split_at);
322 messages.reverse();
323 Ok(messages)
324 }
325}
326
327impl Drop for StreamBufferConsumer {
328 fn drop(&mut self) {
329 if !self.buffer.is_empty() {
330 requeue_messages(self.sender.clone(), std::mem::take(&mut self.buffer));
331 }
332 }
333}
334
335struct RequeueGuard {
336 sender: Sender<Vec<CanonicalMessage>>,
337 messages: Vec<CanonicalMessage>,
338}
339
340impl Drop for RequeueGuard {
341 fn drop(&mut self) {
342 if !self.messages.is_empty() {
343 requeue_messages(self.sender.clone(), std::mem::take(&mut self.messages));
344 }
345 }
346}
347
348fn requeue_messages(sender: Sender<Vec<CanonicalMessage>>, messages: Vec<CanonicalMessage>) {
349 if messages.is_empty() {
350 return;
351 }
352 match sender.try_send(messages) {
353 Ok(_) => {}
354 Err(error) => {
355 let messages = match error {
356 async_channel::TrySendError::Full(messages) => messages,
357 async_channel::TrySendError::Closed(messages) => messages,
358 };
359 if let Ok(handle) = tokio::runtime::Handle::try_current() {
360 handle.spawn(async move {
361 if let Err(error) = sender.send(messages).await {
362 tracing::error!("Failed to requeue stream_buffer messages: {}", error);
363 }
364 });
365 } else {
366 tracing::error!(
367 "No active runtime found, could not requeue stream_buffer messages"
368 );
369 }
370 }
371 }
372}
373
374#[async_trait]
375impl MessageConsumer for StreamBufferConsumer {
376 async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
377 let mut messages = self.get_buffered_messages(max_messages).await?;
378 while messages.len() < max_messages.max(1) / 2 {
379 if let Ok(mut next_batch) = self.receiver.try_recv() {
380 let max_messages = max_messages.max(1);
381 if next_batch.len() + messages.len() > max_messages {
382 let needed = max_messages - messages.len();
383 let mut to_buffer = next_batch.split_off(needed);
384 messages.append(&mut next_batch);
385 self.buffer.append(&mut to_buffer);
386 self.buffer.reverse();
387 break;
388 }
389 messages.append(&mut next_batch);
390 } else {
391 break;
392 }
393 }
394
395 trace!(
396 topic = %self.topic,
397 correlation_id = %self.correlation_id,
398 count = messages.len(),
399 "Received stream_buffer messages"
400 );
401
402 let topic = self.topic.clone();
403 let correlation_id = self.correlation_id.clone();
404 let sender = self.sender.clone();
405 let expected_count = messages.len();
406 let mut guard = RequeueGuard {
407 sender: sender.clone(),
408 messages: messages.clone(),
409 };
410
411 let commit = Box::new(move |dispositions: Vec<MessageDisposition>| {
412 Box::pin(async move {
413 if dispositions.len() != expected_count {
414 return Err(anyhow!(
415 "stream_buffer commit received mismatched disposition count: expected {}, got {}",
416 expected_count,
417 dispositions.len()
418 ));
419 }
420
421 let mut to_requeue = Vec::new();
422 let mut saw_acked_end_marker = false;
423 for (index, disposition) in dispositions.into_iter().enumerate() {
424 match disposition {
425 MessageDisposition::Ack | MessageDisposition::Reply(_) => {
426 if guard
427 .messages
428 .get(index)
429 .and_then(|message| message.metadata.get("http_stream_end"))
430 .is_some_and(|value| value == "true")
431 {
432 saw_acked_end_marker = true;
433 }
434 }
435 MessageDisposition::Nack => {
436 if let Some(message) = guard.messages.get(index) {
437 warn!(
438 topic = %topic,
439 correlation_id = %correlation_id,
440 index,
441 "Requeueing nacked stream_buffer message"
442 );
443 to_requeue.push(message.clone());
444 }
445 }
446 }
447 }
448
449 std::mem::take(&mut guard.messages);
450 if !to_requeue.is_empty() {
451 requeue_messages(sender.clone(), to_requeue);
452 } else if saw_acked_end_marker {
453 remove_partition_if_current(&topic, &correlation_id, &sender);
454 }
455
456 Ok(())
457 }) as BoxFuture<'static, anyhow::Result<()>>
458 }) as BatchCommitFunc;
459
460 Ok(ReceivedBatch { messages, commit })
461 }
462
463 async fn status(&self) -> EndpointStatus {
464 EndpointStatus {
465 healthy: !self.receiver.is_closed(),
466 target: format!("{}/{}", self.topic, self.correlation_id),
467 pending: Some(self.receiver.len()),
468 capacity: Some(self.receiver.capacity().unwrap_or(0)),
469 ..Default::default()
470 }
471 }
472
473 fn as_any(&self) -> &dyn Any {
474 self
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use crate::traits::MessageConsumer;
482
483 fn config(topic: &str, correlation_id: Option<&str>) -> StreamBufferConfig {
484 StreamBufferConfig {
485 topic: topic.to_string(),
486 correlation_id: correlation_id.map(str::to_string),
487 capacity: Some(10),
488 }
489 }
490
491 #[tokio::test]
492 async fn test_stream_buffer_isolates_by_correlation_id() {
493 let topic = format!("stream_buffer_iso_{}", fast_uuid_v7::gen_id_str());
494 let publisher = StreamBufferPublisher::new(&config(&topic, None)).unwrap();
495
496 publisher
497 .send(CanonicalMessage::from_vec("a1").with_metadata_kv("correlation_id", "a"))
498 .await
499 .unwrap();
500 publisher
501 .send(CanonicalMessage::from_vec("b1").with_metadata_kv("correlation_id", "b"))
502 .await
503 .unwrap();
504 publisher
505 .send(CanonicalMessage::from_vec("a2").with_metadata_kv("correlation_id", "a"))
506 .await
507 .unwrap();
508
509 let mut consumer_a = StreamBufferConsumer::new(&config(&topic, Some("a"))).unwrap();
510 let mut consumer_b = StreamBufferConsumer::new(&config(&topic, Some("b"))).unwrap();
511
512 let first_a = consumer_a.receive().await.unwrap();
513 assert_eq!(first_a.message.get_payload_str(), "a1");
514 (first_a.commit)(MessageDisposition::Ack).await.unwrap();
515
516 let first_b = consumer_b.receive().await.unwrap();
517 assert_eq!(first_b.message.get_payload_str(), "b1");
518 (first_b.commit)(MessageDisposition::Ack).await.unwrap();
519
520 let second_a = consumer_a.receive().await.unwrap();
521 assert_eq!(second_a.message.get_payload_str(), "a2");
522 (second_a.commit)(MessageDisposition::Ack).await.unwrap();
523 }
524
525 #[tokio::test]
526 async fn test_stream_buffer_requeues_nacked_message() {
527 let topic = format!("stream_buffer_nack_{}", fast_uuid_v7::gen_id_str());
528 let publisher = StreamBufferPublisher::new(&config(&topic, None)).unwrap();
529 publisher
530 .send(CanonicalMessage::from_vec("retry").with_metadata_kv("correlation_id", "c"))
531 .await
532 .unwrap();
533
534 let mut consumer = StreamBufferConsumer::new(&config(&topic, Some("c"))).unwrap();
535 let received = consumer.receive().await.unwrap();
536 assert_eq!(received.message.get_payload_str(), "retry");
537 (received.commit)(MessageDisposition::Nack).await.unwrap();
538
539 let retried = consumer.receive().await.unwrap();
540 assert_eq!(retried.message.get_payload_str(), "retry");
541 (retried.commit)(MessageDisposition::Ack).await.unwrap();
542 }
543
544 #[tokio::test]
545 async fn test_stream_buffer_requeues_dropped_message() {
546 let topic = format!("stream_buffer_drop_{}", fast_uuid_v7::gen_id_str());
547 let publisher = StreamBufferPublisher::new(&config(&topic, None)).unwrap();
548 publisher
549 .send(CanonicalMessage::from_vec("held").with_metadata_kv("correlation_id", "c"))
550 .await
551 .unwrap();
552
553 let mut consumer = StreamBufferConsumer::new(&config(&topic, Some("c"))).unwrap();
554 {
555 let received = consumer.receive().await.unwrap();
556 assert_eq!(received.message.get_payload_str(), "held");
557 }
558
559 let redelivered = consumer.receive().await.unwrap();
560 assert_eq!(redelivered.message.get_payload_str(), "held");
561 (redelivered.commit)(MessageDisposition::Ack).await.unwrap();
562 }
563
564 #[tokio::test]
565 async fn test_stream_buffer_end_marker_ack_cleans_partition() {
566 let topic = format!("stream_buffer_end_{}", fast_uuid_v7::gen_id_str());
567 let publisher = StreamBufferPublisher::new(&config(&topic, None)).unwrap();
568 publisher
569 .send(
570 CanonicalMessage::from_vec("")
571 .with_metadata_kv("correlation_id", "done")
572 .with_metadata_kv("http_stream_end", "true"),
573 )
574 .await
575 .unwrap();
576
577 let mut consumer = StreamBufferConsumer::new(&config(&topic, Some("done"))).unwrap();
578 let end = consumer.receive().await.unwrap();
579 assert!(end.message.payload.is_empty());
580 (end.commit)(MessageDisposition::Ack).await.unwrap();
581
582 let result =
583 tokio::time::timeout(std::time::Duration::from_millis(100), consumer.receive()).await;
584 assert!(matches!(result, Ok(Err(ConsumerError::EndOfStream))));
585 }
586}