use std::future::ready;
use std::sync::{Arc, mpsc};
use std::time::Duration;
use agentcat_api::models::PublishEventRequest;
use tokio::sync::oneshot;
use crate::config::Options;
use crate::event::CallRecord;
use crate::event::assemble::EventJob;
use crate::logging;
use crate::pipeline::EventObject;
use crate::pipeline::process::Redactors;
use crate::sink::{BoxFuture, Sink};
use queue::{Job, Kind, Reply, Request};
use worker::Shared;
pub(crate) mod http;
mod queue;
pub(crate) mod request;
mod worker;
#[cfg(test)]
mod tests;
const ANSWER_GRACE: Duration = Duration::from_millis(100);
pub(crate) trait Publisher: Send + Sync + 'static {
fn publish(&self, request: PublishEventRequest) -> BoxFuture<'static, Result<(), String>>;
fn prepare(&self, _log: &mut Vec<String>) -> Result<(), String> {
Ok(())
}
}
pub(crate) trait Observer: Send + Sync + 'static {
fn finished(&self, event: &EventObject);
}
pub(crate) type LineWriter = Arc<dyn Fn(&str) + Send + Sync>;
#[derive(Debug, Clone, Copy)]
pub(crate) struct DeliveryConfig {
pub(crate) max_pending: usize,
pub(crate) concurrency: usize,
pub(crate) retry_delays: [Duration; 3],
pub(crate) hook_budget: Duration,
pub(crate) drop_drain: Duration,
}
impl Default for DeliveryConfig {
fn default() -> Self {
Self {
max_pending: 10_000,
concurrency: 5,
retry_delays: [
Duration::from_secs(1),
Duration::from_secs(2),
Duration::from_secs(4),
],
hook_budget: Duration::from_secs(30),
drop_drain: Duration::from_secs(5),
}
}
}
pub(crate) struct Delivery {
shared: Arc<Shared>,
}
impl Delivery {
pub(crate) fn new(
project_id: String,
options: &Options,
publisher: Arc<dyn Publisher>,
observer: Option<Arc<dyn Observer>>,
config: DeliveryConfig,
) -> Self {
Self::with_writer(
project_id,
options,
publisher,
observer,
config,
Arc::new(logging::write),
)
}
pub(crate) fn with_writer(
project_id: String,
options: &Options,
publisher: Arc<dyn Publisher>,
observer: Option<Arc<dyn Observer>>,
config: DeliveryConfig,
write: LineWriter,
) -> Self {
let redactors = Redactors {
string: options.string_redactor().cloned(),
event: options.event_redactor().cloned(),
};
Self {
shared: Arc::new(Shared::new(
project_id, redactors, publisher, observer, config, write,
)),
}
}
fn push(&self, job: Job) {
let (dropped, start) = {
let mut queue = self.shared.lock();
let dropped = queue.push_job(job, self.shared.config.max_pending);
(dropped, queue.claim_start())
};
drop(dropped);
self.wake(start);
}
fn wake(&self, start: bool) {
if start {
worker::start(&self.shared);
}
self.shared.wake();
}
fn request(&self, kind: Kind, timeout: Duration, reply: Reply) -> Option<bool> {
let answer = self
.shared
.lock()
.push_request(Request::new(kind, timeout, reply));
if answer.is_none() {
self.shared.wake();
}
answer
}
fn answer(&self, kind: Kind, timeout: Duration) -> BoxFuture<'static, bool> {
let (sender, receiver) = oneshot::channel();
match self.request(kind, timeout, Reply::Async(sender)) {
Some(answer) => Box::pin(ready(answer)),
None => Box::pin(async move { receiver.await.unwrap_or(false) }),
}
}
fn answer_blocking(&self, kind: Kind, timeout: Duration) -> bool {
let (sender, receiver) = mpsc::channel();
match self.request(kind, timeout, Reply::Blocking(sender)) {
Some(answer) => answer,
None => receiver
.recv_timeout(timeout.saturating_add(ANSWER_GRACE))
.unwrap_or(false),
}
}
}
impl Sink for Delivery {
fn log(&self, message: String) {
let start = {
let mut queue = self.shared.lock();
queue.push_line(message);
queue.claim_start()
};
self.wake(start);
}
fn record(&self, record: CallRecord) {
self.push(Job::Call(Box::new(record)));
}
fn event(&self, job: EventJob) {
self.push(Job::from(job));
}
fn event_then_log(&self, job: EventJob, message: String) {
let (dropped, start) = {
let mut queue = self.shared.lock();
let dropped = queue.push_job(Job::from(job), self.shared.config.max_pending);
queue.push_line(message);
(dropped, queue.claim_start())
};
drop(dropped);
self.wake(start);
}
fn flush(&self, timeout: Duration) -> BoxFuture<'static, bool> {
self.answer(Kind::Flush, timeout)
}
fn shutdown(&self, timeout: Duration) -> BoxFuture<'static, bool> {
self.answer(Kind::Shutdown, timeout)
}
fn flush_blocking(&self, timeout: Duration) -> bool {
self.answer_blocking(Kind::Flush, timeout)
}
fn shutdown_blocking(&self, timeout: Duration) -> bool {
self.answer_blocking(Kind::Shutdown, timeout)
}
}
impl Drop for Delivery {
fn drop(&mut self) {
let drain = self.shared.config.drop_drain;
let (sender, receiver) = mpsc::channel();
let wait = self.shared.lock().close(drain, sender);
self.shared.wake();
if wait {
let _ = receiver.recv_timeout(drain.saturating_add(ANSWER_GRACE));
}
}
}