use std::collections::{HashMap, HashSet, 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<JournalQueueState>>,
config: JournalQueueConfig,
metrics: Arc<Mutex<JournalQueueMetrics>>,
held_runs: Arc<Mutex<HashSet<String>>>,
}
#[derive(Default)]
struct JournalQueueState {
order: VecDeque<u64>,
events: HashMap<u64, JournalEventMessage>,
by_run: HashMap<String, VecDeque<u64>>,
next_id: u64,
}
impl JournalQueueState {
fn with_capacity(capacity: usize) -> Self {
Self {
order: VecDeque::with_capacity(capacity),
events: HashMap::with_capacity(capacity),
by_run: HashMap::new(),
next_id: 0,
}
}
fn insert(&mut self, event: JournalEventMessage, front: bool) {
let id = self.next_id;
self.next_id = self
.next_id
.checked_add(1)
.expect("journal queue event id exhausted");
let run_id = event.run_id.clone();
self.events.insert(id, event);
if front {
self.order.push_front(id);
self.by_run.entry(run_id).or_default().push_front(id);
} else {
self.order.push_back(id);
self.by_run.entry(run_id).or_default().push_back(id);
}
}
fn remove_run_reference(&mut self, run_id: &str, id: u64) {
let mut remove_run = false;
if let Some(ids) = self.by_run.get_mut(run_id) {
if ids.front() == Some(&id) {
ids.pop_front();
} else if let Some(position) = ids.iter().position(|queued_id| *queued_id == id) {
ids.remove(position);
}
remove_run = ids.is_empty();
}
if remove_run {
self.by_run.remove(run_id);
}
}
fn remove_event(&mut self, id: u64) -> Option<JournalEventMessage> {
let event = self.events.remove(&id)?;
self.remove_run_reference(&event.run_id, id);
Some(event)
}
fn maybe_compact_order(&mut self) {
const MIN_TOMBSTONES_BEFORE_COMPACTION: usize = 1024;
let tombstones = self.order.len().saturating_sub(self.events.len());
if tombstones >= MIN_TOMBSTONES_BEFORE_COMPACTION
&& self.order.len() > self.events.len().saturating_mul(2)
{
self.order.retain(|id| self.events.contains_key(id));
}
}
}
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(JournalQueueState::with_capacity(
config.max_size,
))),
config,
metrics: Arc::new(Mutex::new(JournalQueueMetrics::default())),
held_runs: Arc::new(Mutex::new(HashSet::new())),
}
}
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))?;
while queue.events.len() >= self.config.max_size {
let oldest_telemetry = queue.order.iter().enumerate().find_map(|(index, id)| {
queue.events.get(id).and_then(|queued| {
JournalEventMessage::is_sse_only_event_type(&queued.event_type)
.then_some((index, *id))
})
});
if let Some((index, id)) = oldest_telemetry {
queue.order.remove(index);
let dropped = queue
.remove_event(id)
.expect("telemetry id came from the same queue");
log::warn!(
"Journal queue full ({}), dropped oldest telemetry 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;
}
} else if JournalEventMessage::is_sse_only_event_type(&event.event_type) {
log::warn!(
"Journal queue full ({}), dropped incoming telemetry event: type={} run_id={}",
self.config.max_size,
event.event_type,
event.run_id
);
if let Ok(mut metrics) = self.metrics.lock() {
metrics.events_dropped += 1;
}
return Ok(());
} else {
return Err(format!(
"Journal queue full ({}) with correctness events; rejected event: type={} run_id={}",
self.config.max_size, event.event_type, event.run_id
));
}
}
log::debug!(
"Queued journal event: type={} run_id={} is_sse_only={} queue_size={}",
event.event_type,
event.run_id,
event.is_sse_only,
queue.events.len() + 1
);
queue.insert(event, false);
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()?;
while let Some(id) = queue.order.pop_front() {
if let Some(event) = queue.remove_event(id) {
return Some(event);
}
}
None
}
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.insert(event, true);
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 mut batch = Vec::with_capacity(std::cmp::min(max, queue.events.len()));
while batch.len() < max {
let Some(id) = queue.order.pop_front() else {
break;
};
if let Some(event) = queue.remove_event(id) {
batch.push(event);
}
}
if !batch.is_empty() {
log::debug!(
"Drained {} events from journal queue (remaining={})",
batch.len(),
queue.events.len()
);
}
batch
}
pub fn peek_batch_run_ids(&self, max: usize) -> Vec<String> {
let queue = match self.queue.lock() {
Ok(queue) => queue,
Err(error) => {
log::error!("Failed to lock journal queue for peek_batch_run_ids: {error}");
return Vec::new();
}
};
let held = self.held_runs_snapshot();
let mut run_ids = Vec::new();
let mut seen = HashSet::new();
for event in queue
.order
.iter()
.filter_map(|id| queue.events.get(id))
.take(max)
{
if held.contains(&event.run_id) {
continue;
}
if seen.insert(event.run_id.clone()) {
run_ids.push(event.run_id.clone());
}
}
run_ids
}
pub fn hold_run(&self, run_id: &str) {
self.held_runs
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.insert(run_id.to_string());
}
pub fn release_run(&self, run_id: &str) -> bool {
self.held_runs
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.remove(run_id)
}
pub fn is_held(&self, run_id: &str) -> bool {
self.held_runs
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.contains(run_id)
}
fn held_runs_snapshot(&self) -> HashSet<String> {
self.held_runs
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
pub fn drain_batch_for_runs(
&self,
max: usize,
allowed_run_ids: &HashSet<String>,
) -> Vec<JournalEventMessage> {
let mut queue = match self.queue.lock() {
Ok(queue) => queue,
Err(error) => {
log::error!("Failed to lock journal queue for drain_batch_for_runs: {error}");
return Vec::new();
}
};
let mut batch = Vec::with_capacity(std::cmp::min(max, queue.events.len()));
let mut skipped = VecDeque::new();
let mut examined = 0;
while examined < max {
let Some(id) = queue.order.pop_front() else {
break;
};
let Some(event) = queue.events.get(&id) else {
continue;
};
examined += 1;
if allowed_run_ids.contains(&event.run_id) {
if let Some(event) = queue.remove_event(id) {
batch.push(event);
}
} else {
skipped.push_back(id);
}
}
while let Some(id) = skipped.pop_back() {
queue.order.push_front(id);
}
if !batch.is_empty() {
log::debug!(
"Drained {} barrier-protected events from journal queue (remaining={})",
batch.len(),
queue.events.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.events.len());
while let Some(id) = queue.order.pop_front() {
if let Some(event) = queue.remove_event(id) {
events.push(event);
}
}
queue.by_run.clear();
queue.events.clear();
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 ids = queue.by_run.remove(run_id).unwrap_or_default();
let mut matched = Vec::with_capacity(ids.len());
for id in ids {
if let Some(event) = queue.events.remove(&id) {
matched.push(event);
}
}
queue.maybe_compact_order();
if !matched.is_empty() {
log::debug!(
"Drained {} events for run_id={} (remaining={})",
matched.len(),
run_id,
queue.events.len()
);
}
matched
}
pub fn len(&self) -> usize {
self.queue.lock().map(|q| q.events.len()).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn contains_run(&self, run_id: &str) -> bool {
self.queue
.lock()
.map(|queue| queue.by_run.contains_key(run_id))
.unwrap_or(true)
}
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
.order
.iter()
.find_map(|id| queue.events.get(id))
.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()
}
}
fn create_run_event(run_id: &str, event_type: &str, seq: i64) -> JournalEventMessage {
JournalEventMessage {
run_id: run_id.to_string(),
..create_test_event(event_type, seq)
}
}
#[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_drops_oldest_telemetry() {
let queue = JournalEventQueue::new(JournalQueueConfig {
max_size: 3,
..Default::default()
});
queue.push(create_test_event("output.delta", 1)).unwrap();
queue.push(create_test_event("log.info", 2)).unwrap();
queue.push(create_test_event("progress.update", 3)).unwrap();
assert_eq!(queue.len(), 3);
queue.push(create_test_event("output.delta", 4)).unwrap();
assert_eq!(queue.len(), 3);
let event = queue.pop().unwrap();
assert_eq!(event.sequence, 2);
assert_eq!(queue.metrics().events_dropped, 1);
}
#[test]
fn test_journal_queue_overflow_never_evicts_correctness_event() {
let queue = JournalEventQueue::new(JournalQueueConfig {
max_size: 3,
..Default::default()
});
queue
.push(create_test_event("workflow.started", 1))
.unwrap();
queue.push(create_test_event("output.delta", 2)).unwrap();
queue
.push(create_test_event("workflow.step.started", 3))
.unwrap();
queue
.push(create_test_event("workflow.step.completed", 4))
.unwrap();
let sequences: Vec<_> = queue
.drain_all()
.into_iter()
.map(|event| event.sequence)
.collect();
assert_eq!(sequences, vec![1, 3, 4]);
assert_eq!(queue.metrics().events_dropped, 1);
}
#[test]
fn test_journal_queue_rejects_correctness_when_only_correctness_is_buffered() {
let queue = JournalEventQueue::new(JournalQueueConfig {
max_size: 2,
..Default::default()
});
queue
.push(create_test_event("workflow.started", 1))
.unwrap();
queue
.push(create_test_event("workflow.step.started", 2))
.unwrap();
let error = queue
.push(create_test_event("workflow.step.completed", 3))
.unwrap_err();
assert!(error.contains("rejected event: type=workflow.step.completed"));
let sequences: Vec<_> = queue
.drain_all()
.into_iter()
.map(|event| event.sequence)
.collect();
assert_eq!(sequences, vec![1, 2]);
assert_eq!(queue.metrics().events_dropped, 0);
}
#[test]
fn test_journal_queue_drops_incoming_telemetry_before_correctness() {
let queue = JournalEventQueue::new(JournalQueueConfig {
max_size: 2,
..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("output.delta", 3)).unwrap();
let sequences: Vec<_> = queue
.drain_all()
.into_iter()
.map(|event| event.sequence)
.collect();
assert_eq!(sequences, vec![1, 2]);
assert_eq!(queue.metrics().events_dropped, 1);
}
#[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(
"lm.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(
"tool_call.started"
));
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_drain_run_events_uses_index_and_preserves_global_fifo() {
let queue = JournalEventQueue::new(JournalQueueConfig {
max_size: 10,
..Default::default()
});
queue
.push(create_run_event("run-a", "output.delta", 1))
.unwrap();
queue
.push(create_run_event("run-b", "output.delta", 2))
.unwrap();
queue
.push(create_run_event("run-a", "workflow.step.completed", 3))
.unwrap();
queue
.push(create_run_event("run-c", "output.delta", 4))
.unwrap();
let run_a: Vec<_> = queue
.drain_run_events("run-a")
.into_iter()
.map(|event| event.sequence)
.collect();
assert_eq!(run_a, vec![1, 3]);
assert!(!queue.contains_run("run-a"));
assert!(queue.contains_run("run-b"));
assert_eq!(queue.len(), 2);
let remaining: Vec<_> = queue
.drain_batch(10)
.into_iter()
.map(|event| event.sequence)
.collect();
assert_eq!(remaining, vec![2, 4]);
assert!(queue.is_empty());
}
#[test]
fn test_drain_batch_for_runs_does_not_take_unlocked_front_event() {
let queue = JournalEventQueue::new(JournalQueueConfig {
max_size: 10,
..Default::default()
});
queue
.push(create_run_event("run-a", "output.delta", 1))
.unwrap();
queue
.push(create_run_event("run-b", "output.delta", 2))
.unwrap();
queue
.push(create_run_event("run-a", "output.delta", 3))
.unwrap();
assert_eq!(queue.peek_batch_run_ids(2), vec!["run-a", "run-b"]);
let allowed = HashSet::from(["run-a".to_string()]);
let drained: Vec<_> = queue
.drain_batch_for_runs(2, &allowed)
.into_iter()
.map(|event| event.sequence)
.collect();
assert_eq!(drained, vec![1]);
let remaining: Vec<_> = queue
.drain_all()
.into_iter()
.map(|event| event.sequence)
.collect();
assert_eq!(remaining, vec![2, 3]);
}
#[test]
fn held_runs_are_skipped_by_the_periodic_peek_but_drained_explicitly() {
let queue = JournalEventQueue::new(JournalQueueConfig::default());
queue.hold_run("held");
for (run_id, event_type) in [
("held", "run.started"),
("other", "run.started"),
("held", "function.completed"),
] {
queue
.push(JournalEventMessage::new(
run_id.to_string(),
event_type.to_string(),
Vec::new(),
))
.unwrap();
}
assert!(queue.is_held("held"));
assert_eq!(queue.peek_batch_run_ids(10), vec!["other".to_string()]);
let allowed: HashSet<String> = queue.peek_batch_run_ids(10).into_iter().collect();
let periodic = queue.drain_batch_for_runs(10, &allowed);
assert_eq!(periodic.len(), 1);
assert_eq!(periodic[0].run_id, "other");
assert!(queue.contains_run("held"), "held events stay queued");
let held = queue.drain_run_events("held");
assert_eq!(
held.iter()
.map(|event| event.event_type.as_str())
.collect::<Vec<_>>(),
["run.started", "function.completed"]
);
assert!(queue.release_run("held"));
assert!(!queue.release_run("held"));
assert!(!queue.is_held("held"));
}
#[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);
}
}