#[cfg(feature = "stats")]
use std::sync::atomic::AtomicUsize;
use std::{
sync::{Arc, atomic::Ordering},
time::Duration,
};
use tokio::{io::AsyncWriteExt, select, task::JoinHandle};
use tokio_util::sync::CancellationToken;
use crate::{
buffers::Bytes,
pipeline::{
status,
tx::{PipelineTxIn, PipelineTxOut, PullConfig, PushConfig, SendError},
},
protocol::{BatchSize, QoS},
sync::AtomicDuration,
};
#[derive(Clone)]
pub struct Sender {
inner: PipelineTxIn,
config: PushConfig,
token: CancellationToken,
}
impl Sender {
pub fn qos(&mut self, qos: QoS) -> &mut Self {
self.config.qos = qos;
self
}
pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
self.config.timeout = timeout;
self
}
pub fn timeout_progress(&mut self, wait: Duration) -> &mut Self {
self.config.timeout_batch = wait;
self
}
pub fn is_congested(&self) -> bool {
self.inner.status().any(status::congested(self.config.qos.priority()))
}
pub fn is_closed(&self) -> bool {
false
}
pub async fn send<T>(&mut self, msg: T) -> Result<(), SendError>
where
T: AsRef<Bytes>,
{
self.inner.push(msg.as_ref(), &self.config).await
}
pub async fn stop(self) {
self.inner.disable().await;
self.token.cancel();
}
}
#[cfg(feature = "stats")]
struct SenderTaskStats {
batches_count: AtomicUsize,
bytes_count: AtomicUsize,
dropped: AtomicUsize,
}
#[cfg(feature = "stats")]
impl SenderTaskStats {
fn new() -> Self {
Self {
batches_count: AtomicUsize::new(0),
bytes_count: AtomicUsize::new(0),
dropped: AtomicUsize::new(0),
}
}
}
struct SenderTaskInner {
write_timeout: AtomicDuration,
keep_alive: AtomicDuration,
max_batch_age: AtomicDuration,
#[cfg(feature = "stats")]
stats: SenderTaskStats,
}
#[cfg(feature = "stats")]
#[non_exhaustive]
pub struct SenderStats {
pub batches: usize,
pub bytes: usize,
pub dropped: usize,
}
pub struct SenderTask<W> {
handle: JoinHandle<W>,
token: CancellationToken,
inner: Arc<SenderTaskInner>,
}
impl<W> SenderTask<W> {
pub fn set_write_timeout(&self, timeout: Duration) {
self.inner.write_timeout.store(timeout, Ordering::Relaxed);
}
pub fn set_keep_alive(&self, interval: Duration) {
self.inner.keep_alive.store(interval, Ordering::Relaxed);
}
pub fn set_max_batch_age(&self, age: Duration) {
self.inner.max_batch_age.store(age, Ordering::Relaxed);
}
#[cfg(feature = "stats")]
pub fn get_stats(&self) -> SenderStats {
SenderStats {
batches: self.inner.stats.batches_count.load(Ordering::Relaxed),
bytes: self.inner.stats.bytes_count.load(Ordering::Relaxed),
dropped: self.inner.stats.dropped.load(Ordering::Relaxed),
}
}
pub fn stop(self) -> JoinHandle<W> {
let Self {
handle,
token,
inner: _,
} = self;
token.cancel();
handle
}
}
pub struct SenderBuilder<R>
where
R: AsyncWriteExt + Send + Sync + Unpin + 'static,
{
qos: QoS,
capacity: usize,
batch_size: BatchSize,
timeout: Duration,
timeout_progress: Duration,
timeout_batch: Duration,
writer: R,
}
impl<R> SenderBuilder<R>
where
R: AsyncWriteExt + Send + Sync + Unpin + 'static,
{
#[must_use]
pub fn qos(mut self, qos: QoS) -> Self {
self.qos = qos;
self
}
#[must_use]
pub fn capacity(mut self, capacity: usize) -> Self {
self.capacity = capacity;
self
}
#[must_use]
pub fn batch_size(mut self, batch_size: BatchSize) -> Self {
self.batch_size = batch_size;
self
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn timeout_batch(mut self, timeout_batch: Duration) -> Self {
self.timeout_batch = timeout_batch;
self
}
#[must_use]
pub fn build(self) -> (Sender, SenderTask<R>) {
let Self {
qos,
capacity,
batch_size,
timeout,
timeout_progress,
timeout_batch: max_batch_age,
writer,
} = self;
let (pipeline_in, pipeline_out) = crate::pipeline::tx::pipeline_tx(capacity, batch_size);
let token = CancellationToken::new();
let c_token = token.clone();
let inner = Arc::new(SenderTaskInner {
write_timeout: AtomicDuration::new(Duration::from_secs(10)),
max_batch_age: AtomicDuration::new(max_batch_age),
keep_alive: AtomicDuration::new(Duration::from_secs(3)),
#[cfg(feature = "stats")]
stats: SenderTaskStats::new(),
});
let c_inner = inner.clone();
let handle = tokio::spawn(write_task(pipeline_out, writer, c_inner, c_token));
let sender = Sender {
inner: pipeline_in,
config: PushConfig {
qos,
timeout,
timeout_batch: timeout_progress,
},
token: token.clone(),
};
let writer = SenderTask { handle, token, inner };
(sender, writer)
}
}
async fn write_task<W>(
mut pipeline_out: PipelineTxOut,
mut writer: W,
inner: Arc<SenderTaskInner>,
token: CancellationToken,
) -> W
where
W: AsyncWriteExt + Send + Sync + Unpin + 'static,
{
macro_rules! write_batch {
($batch:expr) => {{
let timeout = inner.write_timeout.load(Ordering::Relaxed);
tokio::time::timeout(timeout, async {
let mut written = 0;
let frame = $batch.frame.as_slice();
writer.write_all(frame).await?;
written += frame.len();
for s in $batch.fragment.slices() {
writer.write_all(s).await?;
written += s.len();
}
writer.flush().await?;
pipeline_out.refill($batch);
Ok::<usize, std::io::Error>(written)
})
.await
}};
}
loop {
let keep_alive = inner.keep_alive.load(Ordering::Relaxed);
let config = PullConfig {
max_batch_age: inner.max_batch_age.load(Ordering::Relaxed),
};
let res = select! {
res = tokio::time::timeout(keep_alive, pipeline_out.pull(&config)) => match res {
Ok(batch) => batch,
Err(_) => match writer.write_all(&BatchSize::MIN.to_le_bytes()).await {
Ok(_) => continue,
Err(_) => break,
},
},
_ = token.cancelled() => {
break;
}
};
let batch = match res {
Some(batch) => batch,
None => break,
};
let Ok(Ok(_written)) = write_batch!(batch) else {
break; };
#[cfg(feature = "stats")]
{
inner.stats.batches_count.fetch_add(1, Ordering::Release);
inner.stats.bytes_count.fetch_add(_written, Ordering::Release);
}
}
for batch in pipeline_out.drain().await.drain(..) {
let Ok(Ok(_)) = write_batch!(batch) else {
break; };
}
writer
}
pub fn sender<W>(writer: W) -> SenderBuilder<W>
where
W: AsyncWriteExt + Send + Sync + Unpin + 'static,
{
SenderBuilder {
qos: QoS::DEFAULT,
capacity: 16,
batch_size: BatchSize::MAX,
timeout: Duration::from_secs(60),
timeout_progress: Duration::from_secs(10),
timeout_batch: Duration::from_micros(100),
writer,
}
}