use aion_core::ActivityEvent;
use tokio::sync::mpsc;
use crate::activity_publisher::ActivityEventPublisher;
pub(crate) async fn publish_declared_transcript(
publisher: ActivityEventPublisher,
mut events: mpsc::UnboundedReceiver<ActivityEvent>,
) {
let mut dropped: u64 = 0;
while let Some(event) = events.recv().await {
if let Err(error) = publisher.publish(&event).await {
if dropped == 0 {
tracing::warn!(
%error,
operation = "declared_command_dispatch",
workflow_id = %event.workflow_id,
activity_id = %event.activity_id,
attempt = event.attempt,
"declared command transcript: the sequencer refused an output line; the \
command is unaffected and further refusals are counted, not logged"
);
}
dropped = dropped.saturating_add(1);
}
}
if dropped > 0 {
tracing::warn!(
dropped,
operation = "declared_command_dispatch",
"declared command transcript: output lines were not retained"
);
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use aion_core::{ActivityEvent, ActivityEventKind, ActivityId, MessageRole, RunId, WorkflowId};
use aion_store::{ActivityStreamKey, InMemoryObservabilityStore};
use uuid::Uuid;
use super::publish_declared_transcript;
use crate::activity_publisher::ActivityEventPublisher;
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn line_event(worker_seq: u64, text: &str) -> ActivityEvent {
ActivityEvent {
workflow_id: WorkflowId::new(Uuid::from_u128(7)),
run_id: RunId::new(Uuid::from_u128(0x70)),
activity_id: ActivityId::from_sequence_position(2),
attempt: 1,
agent_id: Uuid::nil(),
agent_role: "command stdout".to_owned(),
emitted_at: chrono::Utc::now(),
worker_seq,
store_seq: None,
ephemeral: false,
kind: ActivityEventKind::Message {
role: MessageRole::Tool,
text: text.to_owned(),
},
}
}
#[tokio::test]
async fn drained_lines_are_sequenced_into_the_durable_transcript() -> TestResult {
let store = Arc::new(InMemoryObservabilityStore::default());
let capacity = std::num::NonZeroUsize::new(16).ok_or("capacity must be non-zero")?;
let publisher = ActivityEventPublisher::new(store, capacity);
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
let pump = tokio::spawn(publish_declared_transcript(publisher.clone(), receiver));
sender.send(line_event(0, "first"))?;
sender.send(line_event(1, "second"))?;
drop(sender);
pump.await?;
let key = ActivityStreamKey::of(&line_event(0, ""));
let retained = publisher.replay_from(&key, 0).await?;
let texts = retained
.iter()
.filter_map(|record| match &record.event.kind {
ActivityEventKind::Message { text, .. } => Some(text.clone()),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(texts, vec!["first".to_owned(), "second".to_owned()]);
Ok(())
}
}