use std::mem;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::thread;
use std::time::Instant;
use serde_json::Value;
use tokio::runtime::Builder;
use tokio::sync::Notify;
use tokio::task::JoinHandle;
use super::queue::{Job, Kind, Queue, Request, Worker};
use super::request::publish_request;
use super::{DeliveryConfig, LineWriter, Observer, Publisher};
use crate::event::assemble::from_call;
use crate::pipeline::EventObject;
use crate::pipeline::process::{Redactors, process};
use crate::wire;
pub(super) const THREAD_NAME: &str = "agentcat-delivery";
const DELIVERY_FAILED: &str = "Failed to deliver AgentCat event after retries: ";
const START_FAILED: &str = "Failed to start the AgentCat delivery thread: ";
pub(super) struct Shared {
pub(super) project_id: String,
pub(super) redactors: Redactors,
pub(super) publisher: Arc<dyn Publisher>,
pub(super) observer: Option<Arc<dyn Observer>>,
pub(super) config: DeliveryConfig,
pub(super) write: LineWriter,
queue: Mutex<Queue>,
wake: Notify,
}
impl Shared {
pub(super) fn new(
project_id: String,
redactors: Redactors,
publisher: Arc<dyn Publisher>,
observer: Option<Arc<dyn Observer>>,
config: DeliveryConfig,
write: LineWriter,
) -> Self {
Self {
project_id,
redactors,
publisher,
observer,
config,
write,
queue: Mutex::new(Queue::new()),
wake: Notify::new(),
}
}
pub(super) fn lock(&self) -> MutexGuard<'_, Queue> {
self.queue.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(super) fn wake(&self) {
self.wake.notify_one();
}
fn write(&self, line: &str) {
(self.write)(line);
}
fn gone(&self) {
let (jobs, requests) = self.lock().take_all();
drop(jobs);
for request in requests {
request.reply.send(false);
}
}
}
pub(super) fn start(shared: &Arc<Shared>) {
let worker = Arc::clone(shared);
let spawned = thread::Builder::new()
.name(THREAD_NAME.to_owned())
.spawn(move || run_thread(&worker));
match spawned {
Ok(thread) => {
let id = thread.thread().id();
let mut queue = shared.lock();
if queue.worker == Worker::Starting {
queue.worker = Worker::Running(id);
}
}
Err(error) => {
shared.write(&format!("{START_FAILED}{error}"));
shared.gone();
}
}
}
struct Exit<'a>(&'a Shared);
impl Drop for Exit<'_> {
fn drop(&mut self) {
self.0.gone();
}
}
fn run_thread(shared: &Arc<Shared>) {
let _exit = Exit(shared);
for feature in json_features() {
shared.write(&feature_warning(feature));
}
let runtime = match Builder::new_current_thread()
.enable_time()
.enable_io()
.build()
{
Ok(runtime) => runtime,
Err(error) => {
shared.write(&format!("{START_FAILED}{error}"));
return;
}
};
runtime.block_on(run(shared));
}
async fn run(shared: &Arc<Shared>) {
let mut tasks: Vec<JoinHandle<()>> = Vec::new();
let mut waiting: Vec<Request> = Vec::new();
let mut stopped = false;
loop {
let (lines, jobs, idle, closed) = {
let mut queue = shared.lock();
waiting.append(&mut queue.requests);
let mut jobs = Vec::new();
while !stopped && queue.in_flight < shared.config.concurrency {
let Some(job) = queue.pending.pop_front() else {
break;
};
queue.in_flight += 1;
jobs.push(job);
}
let idle = queue.pending.is_empty() && queue.in_flight == 0;
(mem::take(&mut queue.lines), jobs, idle, queue.closed)
};
for line in &lines {
shared.write(line);
}
tasks.retain(|task| !task.is_finished());
for job in jobs {
let slot = Slot(Arc::clone(shared));
tasks.push(tokio::spawn(run_job(Arc::clone(shared), job, slot)));
}
let now = Instant::now();
let shutdown_due = waiting
.iter()
.any(|request| request.kind == Kind::Shutdown && (idle || request.deadline <= now));
if !stopped && shutdown_due {
stop(shared, &mut tasks).await;
stopped = true;
for request in waiting.drain(..) {
request.reply.send(idle);
}
}
if stopped {
for request in waiting.drain(..) {
request.reply.send(true);
}
if closed {
return;
}
} else {
let (due, open): (Vec<Request>, Vec<Request>) =
waiting.drain(..).partition(|request| {
request.kind == Kind::Flush && (idle || request.deadline <= now)
});
waiting = open;
for request in due {
request.reply.send(idle);
}
}
let woken = shared.wake.notified();
match waiting.iter().map(|request| request.deadline).min() {
Some(deadline) => {
let _ = tokio::time::timeout_at(deadline.into(), woken).await;
}
None => woken.await,
}
}
}
async fn stop(shared: &Shared, tasks: &mut Vec<JoinHandle<()>>) {
let abandoned = mem::take(&mut shared.lock().pending);
if !abandoned.is_empty() {
shared.write(&format!(
"Shutting down with {} events still in queue",
abandoned.len()
));
}
drop(abandoned);
for task in tasks.iter() {
task.abort();
}
for task in tasks.drain(..) {
let _ = task.await;
}
}
struct Slot(Arc<Shared>);
impl Drop for Slot {
fn drop(&mut self) {
let mut queue = self.0.lock();
queue.in_flight = queue.in_flight.saturating_sub(1);
drop(queue);
self.0.wake();
}
}
async fn run_job(shared: Arc<Shared>, job: Job, _slot: Slot) {
let job = match job {
Job::Call(record) => from_call(*record, &shared.project_id),
Job::Event(job) => job,
};
let mut log = Vec::new();
let event = process(job, &shared.redactors, shared.config.hook_budget, &mut log).await;
for line in &log {
shared.write(line);
}
let Some(event) = event else {
return;
};
if let Some(observer) = &shared.observer {
observer.finished(&event);
}
let has_project = event
.get("projectId")
.and_then(Value::as_str)
.is_some_and(|project| !project.is_empty());
if has_project {
post(&shared, &event).await;
}
}
async fn post(shared: &Shared, event: &EventObject) {
let mut log = Vec::new();
let prepared = shared.publisher.prepare(&mut log);
for line in &log {
shared.write(line);
}
if let Err(reason) = prepared {
shared.write(&format!("{DELIVERY_FAILED}{reason}"));
return;
}
let request = publish_request(event);
let id = wire::coerce_to_string(event.get("id"));
let mut delays = shared.config.retry_delays.iter();
loop {
match shared.publisher.publish(request.clone()).await {
Ok(()) => {
shared.write(&success_line(event));
return;
}
Err(error) => {
shared.write(&format!(
"Failed to send event {id}, retrying... [Error: {error}]"
));
let Some(delay) = delays.next() else {
shared.write(&format!("{DELIVERY_FAILED}{error}"));
return;
};
tokio::time::sleep(*delay).await;
}
}
}
}
fn success_line(event: &EventObject) -> String {
let field = |key: &str| wire::coerce_to_string(event.get(key));
let actor = match event.get("identifyActorGivenId") {
Some(actor) if wire::truthy(actor) => wire::coerce_to_string(Some(actor)),
_ => "anonymous".to_owned(),
};
format!(
"Successfully sent event {} | {} | session {} | {} | {} ms | {actor}",
field("id"),
field("eventType"),
field("sessionId"),
field("projectId"),
field("duration"),
)
}
pub(super) fn json_features() -> Vec<&'static str> {
const ROUNDTRIP_PROBES: [&str; 3] =
["9007199254740993.0", "0.9999999999999999", "7.038531e-26"];
let mut features = Vec::new();
if "1e400".parse::<serde_json::Number>().is_ok() {
features.push("arbitrary_precision");
}
if ROUNDTRIP_PROBES
.iter()
.all(|text| serde_json::from_str::<f64>(text).ok() == text.parse::<f64>().ok())
{
features.push("float_roundtrip");
}
features
}
pub(super) fn feature_warning(feature: &str) -> String {
format!(
"Warning: serde_json feature \"{feature}\" is enabled in this build; AgentCat token \
estimates and size checks may differ from builds without it"
)
}