use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex};
use std::time::Instant;
#[derive(Debug, Clone)]
pub struct JournalQueueConfig {
pub max_size: usize,
pub batch_size: usize,
pub flush_interval_ms: u64,
}
impl Default for JournalQueueConfig {
fn default() -> Self {
Self {
max_size: 5000,
batch_size: 100,
flush_interval_ms: 50,
}
}
}
impl JournalQueueConfig {
pub fn from_env() -> Self {
let max_size = std::env::var("AGNT5_JOURNAL_QUEUE_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(5000);
let batch_size = std::env::var("AGNT5_JOURNAL_BATCH_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(100);
let flush_interval_ms = std::env::var("AGNT5_JOURNAL_FLUSH_INTERVAL_MS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(50);
Self {
max_size,
batch_size,
flush_interval_ms,
}
}
}
#[derive(Debug, Clone)]
pub struct JournalEventMessage {
pub run_id: String,
pub event_type: String,
pub data: Vec<u8>,
pub correlation_id: String,
pub parent_correlation_id: String,
pub tenant_id: Option<String>,
pub source_timestamp_ns: i64,
pub metadata: HashMap<String, String>,
pub queued_at: Instant,
pub is_streaming: bool,
pub is_sse_only: bool,
pub content_index: i32,
pub sequence: i64,
}
impl Default for JournalEventMessage {
fn default() -> Self {
Self {
run_id: String::new(),
event_type: String::new(),
data: Vec::new(),
correlation_id: String::new(),
parent_correlation_id: String::new(),
tenant_id: None,
source_timestamp_ns: 0,
metadata: HashMap::new(),
queued_at: Instant::now(),
is_streaming: false,
is_sse_only: false,
content_index: 0,
sequence: 0,
}
}
}
impl JournalEventMessage {
pub fn is_sse_only_event_type(event_type: &str) -> bool {
event_type.starts_with("output.")
|| event_type.starts_with("lm.stream.")
|| event_type.starts_with("lm.content_block.")
|| event_type.starts_with("lm.message.")
|| event_type.starts_with("lm.thinking.")
|| event_type.starts_with("lm.tool_call.") || event_type.starts_with("progress.")
|| event_type.starts_with("log") }
pub fn is_checkpoint_event_type(event_type: &str) -> bool {
!Self::is_sse_only_event_type(event_type)
}
pub fn new(run_id: String, event_type: String, data: Vec<u8>) -> Self {
let is_sse_only = Self::is_sse_only_event_type(&event_type);
Self {
run_id,
event_type,
data,
is_sse_only,
queued_at: Instant::now(),
..Default::default()
}
}
}
#[derive(Debug, Default, Clone)]
pub struct JournalQueueMetrics {
pub events_queued: u64,
pub events_sent: u64,
pub events_dropped: u64,
pub send_errors: u64,
pub boundary_events_sent: u64,
pub sse_only_events_sent: u64,
}
#[derive(Clone)]
pub struct JournalEventQueue {
queue: Arc<Mutex<VecDeque<JournalEventMessage>>>,
config: JournalQueueConfig,
metrics: Arc<Mutex<JournalQueueMetrics>>,
}
impl JournalEventQueue {
pub fn new(config: JournalQueueConfig) -> Self {
log::info!(
"Creating unified journal event queue: max_size={}, batch_size={}, flush_interval_ms={}",
config.max_size,
config.batch_size,
config.flush_interval_ms
);
JournalEventQueue {
queue: Arc::new(Mutex::new(VecDeque::with_capacity(config.max_size))),
config,
metrics: Arc::new(Mutex::new(JournalQueueMetrics::default())),
}
}
pub fn push(&self, event: JournalEventMessage) -> Result<(), String> {
let mut queue = self
.queue
.lock()
.map_err(|e| format!("Failed to lock journal queue for push: {}", e))?;
if queue.len() >= self.config.max_size {
if let Some(dropped) = queue.pop_front() {
log::warn!(
"Journal queue full ({}), dropped oldest event: type={} run_id={}",
self.config.max_size,
dropped.event_type,
dropped.run_id
);
if let Ok(mut metrics) = self.metrics.lock() {
metrics.events_dropped += 1;
}
}
}
log::debug!(
"Queued journal event: type={} run_id={} is_sse_only={} queue_size={}",
event.event_type,
event.run_id,
event.is_sse_only,
queue.len() + 1
);
queue.push_back(event);
if let Ok(mut metrics) = self.metrics.lock() {
metrics.events_queued += 1;
}
Ok(())
}
pub fn pop(&self) -> Option<JournalEventMessage> {
let mut queue = self.queue.lock().ok()?;
queue.pop_front()
}
pub fn push_front(&self, event: JournalEventMessage) -> Result<(), String> {
let mut queue = self
.queue
.lock()
.map_err(|e| format!("Failed to lock journal queue for push_front: {}", e))?;
log::debug!(
"Re-queuing event at front: type={} run_id={}",
event.event_type,
event.run_id
);
queue.push_front(event);
Ok(())
}
pub fn drain_batch(&self, max: usize) -> Vec<JournalEventMessage> {
let mut queue = match self.queue.lock() {
Ok(q) => q,
Err(e) => {
log::error!("Failed to lock journal queue for drain_batch: {}", e);
return Vec::new();
}
};
let count = std::cmp::min(max, queue.len());
let mut batch = Vec::with_capacity(count);
for _ in 0..count {
if let Some(event) = queue.pop_front() {
batch.push(event);
}
}
if !batch.is_empty() {
log::debug!(
"Drained {} events from journal queue (remaining={})",
batch.len(),
queue.len()
);
}
batch
}
pub fn drain_all(&self) -> Vec<JournalEventMessage> {
let mut queue = match self.queue.lock() {
Ok(q) => q,
Err(e) => {
log::error!("Failed to lock journal queue for drain_all: {}", e);
return Vec::new();
}
};
let mut events = Vec::with_capacity(queue.len());
while let Some(event) = queue.pop_front() {
events.push(event);
}
log::debug!("Drained {} events from journal queue", events.len());
events
}
pub fn drain_run_events(&self, run_id: &str) -> Vec<JournalEventMessage> {
let mut queue = match self.queue.lock() {
Ok(q) => q,
Err(e) => {
log::error!("Failed to lock journal queue for drain_run_events: {}", e);
return Vec::new();
}
};
let mut matched = Vec::new();
let mut remaining = std::collections::VecDeque::with_capacity(queue.len());
while let Some(event) = queue.pop_front() {
if event.run_id == run_id {
matched.push(event);
} else {
remaining.push_back(event);
}
}
*queue = remaining;
if !matched.is_empty() {
log::debug!(
"Drained {} events for run_id={} (remaining={})",
matched.len(),
run_id,
queue.len()
);
}
matched
}
pub fn len(&self) -> usize {
self.queue.lock().map(|q| q.len()).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn record_sent(&self, is_sse_only: bool) {
if let Ok(mut metrics) = self.metrics.lock() {
metrics.events_sent += 1;
if is_sse_only {
metrics.sse_only_events_sent += 1;
} else {
metrics.boundary_events_sent += 1;
}
}
}
pub fn record_sent_batch(&self, count: usize, sse_only_count: usize) {
if let Ok(mut metrics) = self.metrics.lock() {
metrics.events_sent += count as u64;
metrics.sse_only_events_sent += sse_only_count as u64;
metrics.boundary_events_sent += (count - sse_only_count) as u64;
}
}
pub fn record_error(&self) {
if let Ok(mut metrics) = self.metrics.lock() {
metrics.send_errors += 1;
}
}
pub fn metrics(&self) -> JournalQueueMetrics {
self.metrics.lock().map(|m| m.clone()).unwrap_or_default()
}
pub fn get_metrics(&self) -> (u64, u64, u64, u64) {
if let Ok(metrics) = self.metrics.lock() {
(
metrics.events_queued,
metrics.events_sent,
metrics.events_dropped,
metrics.send_errors,
)
} else {
(0, 0, 0, 0)
}
}
pub fn oldest_age(&self) -> Option<std::time::Duration> {
let queue = self.queue.lock().ok()?;
queue.front().map(|event| event.queued_at.elapsed())
}
pub fn batch_size(&self) -> usize {
self.config.batch_size
}
pub fn flush_interval_ms(&self) -> u64 {
self.config.flush_interval_ms
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_event(event_type: &str, seq: i64) -> JournalEventMessage {
JournalEventMessage {
run_id: "test-run".to_string(),
event_type: event_type.to_string(),
data: format!("{{\"seq\": {}}}", seq).into_bytes(),
is_sse_only: JournalEventMessage::is_sse_only_event_type(event_type),
sequence: seq,
source_timestamp_ns: 1234567890000000000 + seq * 1000000,
queued_at: Instant::now(),
..Default::default()
}
}
#[test]
fn test_journal_queue_basic() {
let queue = JournalEventQueue::new(JournalQueueConfig {
max_size: 10,
..Default::default()
});
queue
.push(create_test_event("workflow.started", 1))
.unwrap();
assert_eq!(queue.len(), 1);
let event = queue.pop().unwrap();
assert_eq!(event.sequence, 1);
assert_eq!(queue.len(), 0);
}
#[test]
fn test_journal_queue_overflow() {
let queue = JournalEventQueue::new(JournalQueueConfig {
max_size: 3,
..Default::default()
});
queue
.push(create_test_event("workflow.started", 1))
.unwrap();
queue
.push(create_test_event("workflow.step.started", 2))
.unwrap();
queue
.push(create_test_event("workflow.step.completed", 3))
.unwrap();
assert_eq!(queue.len(), 3);
queue
.push(create_test_event("workflow.completed", 4))
.unwrap();
assert_eq!(queue.len(), 3);
let event = queue.pop().unwrap();
assert_eq!(event.sequence, 2); }
#[test]
fn test_sse_only_detection() {
assert!(!JournalEventMessage::is_sse_only_event_type(
"workflow.started"
));
assert!(!JournalEventMessage::is_sse_only_event_type(
"agent.completed"
));
assert!(!JournalEventMessage::is_sse_only_event_type(
"lm.call.started"
));
assert!(!JournalEventMessage::is_sse_only_event_type(
"tool.call.completed"
));
assert!(JournalEventMessage::is_sse_only_event_type("output.delta"));
assert!(JournalEventMessage::is_sse_only_event_type("output.start"));
assert!(JournalEventMessage::is_sse_only_event_type("output.stop"));
assert!(JournalEventMessage::is_sse_only_event_type(
"lm.stream.delta"
));
assert!(JournalEventMessage::is_sse_only_event_type(
"lm.content_block.started"
));
assert!(JournalEventMessage::is_sse_only_event_type(
"lm.content_block.delta"
));
assert!(JournalEventMessage::is_sse_only_event_type(
"lm.content_block.completed"
));
assert!(JournalEventMessage::is_sse_only_event_type(
"lm.message.delta"
));
assert!(JournalEventMessage::is_sse_only_event_type(
"lm.thinking.delta"
));
assert!(JournalEventMessage::is_sse_only_event_type(
"lm.tool_call.start"
));
assert!(JournalEventMessage::is_sse_only_event_type(
"lm.tool_call.delta"
));
assert!(JournalEventMessage::is_sse_only_event_type(
"lm.tool_call.stop"
));
assert!(JournalEventMessage::is_sse_only_event_type(
"progress.update"
));
assert!(JournalEventMessage::is_sse_only_event_type("log"));
assert!(JournalEventMessage::is_sse_only_event_type("log.info"));
assert!(JournalEventMessage::is_sse_only_event_type("log.warn"));
assert!(JournalEventMessage::is_sse_only_event_type("log.error"));
}
#[test]
fn test_checkpoint_event_detection() {
assert!(JournalEventMessage::is_checkpoint_event_type(
"workflow.started"
));
assert!(JournalEventMessage::is_checkpoint_event_type(
"workflow.completed"
));
assert!(JournalEventMessage::is_checkpoint_event_type(
"workflow.failed"
));
assert!(JournalEventMessage::is_checkpoint_event_type(
"workflow.paused"
));
assert!(JournalEventMessage::is_checkpoint_event_type(
"workflow.step.started"
));
assert!(JournalEventMessage::is_checkpoint_event_type(
"workflow.step.completed"
));
assert!(JournalEventMessage::is_checkpoint_event_type(
"workflow.step.paused"
));
assert!(JournalEventMessage::is_checkpoint_event_type(
"agent.started"
));
assert!(JournalEventMessage::is_checkpoint_event_type(
"agent.completed"
));
assert!(JournalEventMessage::is_checkpoint_event_type(
"approval.requested"
));
assert!(JournalEventMessage::is_checkpoint_event_type(
"approval.resolved"
));
assert!(JournalEventMessage::is_checkpoint_event_type(
"lm.call.started"
));
assert!(JournalEventMessage::is_checkpoint_event_type(
"lm.call.completed"
));
assert!(JournalEventMessage::is_checkpoint_event_type(
"tool.call.started"
));
assert!(JournalEventMessage::is_checkpoint_event_type(
"tool.call.completed"
));
assert!(!JournalEventMessage::is_checkpoint_event_type(
"output.delta"
));
assert!(!JournalEventMessage::is_checkpoint_event_type(
"lm.stream.delta"
));
assert!(!JournalEventMessage::is_checkpoint_event_type(
"lm.message.delta"
));
assert!(!JournalEventMessage::is_checkpoint_event_type(
"lm.thinking.delta"
));
assert!(!JournalEventMessage::is_checkpoint_event_type(
"progress.update"
));
assert!(!JournalEventMessage::is_checkpoint_event_type("log"));
}
#[test]
fn test_drain_batch() {
let queue = JournalEventQueue::new(JournalQueueConfig {
max_size: 10,
batch_size: 3,
..Default::default()
});
for i in 1..=5 {
queue
.push(create_test_event("workflow.step.completed", i))
.unwrap();
}
assert_eq!(queue.len(), 5);
let batch = queue.drain_batch(3);
assert_eq!(batch.len(), 3);
assert_eq!(queue.len(), 2);
assert_eq!(batch[0].sequence, 1);
assert_eq!(batch[1].sequence, 2);
assert_eq!(batch[2].sequence, 3);
let batch = queue.drain_batch(10);
assert_eq!(batch.len(), 2);
assert!(queue.is_empty());
}
#[test]
fn test_push_front() {
let queue = JournalEventQueue::new(JournalQueueConfig::default());
queue
.push(create_test_event("workflow.started", 1))
.unwrap();
queue
.push(create_test_event("workflow.completed", 2))
.unwrap();
let event = queue.pop().unwrap();
assert_eq!(event.sequence, 1);
queue.push_front(event).unwrap();
let event = queue.pop().unwrap();
assert_eq!(event.sequence, 1);
}
#[test]
fn test_metrics() {
let queue = JournalEventQueue::new(JournalQueueConfig::default());
queue
.push(create_test_event("workflow.started", 1))
.unwrap();
queue.push(create_test_event("output.delta", 2)).unwrap();
let (queued, sent, dropped, errors) = queue.get_metrics();
assert_eq!(queued, 2);
assert_eq!(sent, 0);
assert_eq!(dropped, 0);
assert_eq!(errors, 0);
queue.record_sent(false); queue.record_sent(true);
let metrics = queue.metrics();
assert_eq!(metrics.events_sent, 2);
assert_eq!(metrics.boundary_events_sent, 1);
assert_eq!(metrics.sse_only_events_sent, 1);
queue.record_error();
let (_, _, _, errors) = queue.get_metrics();
assert_eq!(errors, 1);
}
#[test]
fn test_fifo_order() {
let queue = JournalEventQueue::new(JournalQueueConfig::default());
for i in 1..=5 {
queue
.push(create_test_event("workflow.step.completed", i))
.unwrap();
}
for i in 1..=5 {
let event = queue.pop().unwrap();
assert_eq!(event.sequence, i);
}
assert!(queue.is_empty());
}
#[test]
fn test_new_with_auto_classification() {
let boundary = JournalEventMessage::new(
"run-1".to_string(),
"workflow.started".to_string(),
b"{}".to_vec(),
);
assert!(!boundary.is_sse_only);
let sse_only = JournalEventMessage::new(
"run-1".to_string(),
"output.delta".to_string(),
b"hello".to_vec(),
);
assert!(sse_only.is_sse_only);
}
}