mod adaptive;
mod config;
pub use adaptive::{AdaptiveLimiter, Outcome, Permit};
pub use config::{AdaptiveConfig, SinkStackConfig};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use backon::{ExponentialBuilder, Retryable};
use tower::util::BoxCloneService;
use tower::{BoxError, Layer, Service, ServiceBuilder, ServiceExt};
use crate::governor::RateLimiter;
use crate::transport::{
CommitToken, Record, SendResult, TransportError, TransportSender, WorkBatch,
};
#[derive(Debug, Clone)]
pub struct SinkBatch(Arc<Vec<Record>>);
impl SinkBatch {
#[must_use]
pub fn new(records: Vec<Record>) -> Self {
Self(Arc::new(records))
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[derive(Debug, thiserror::Error)]
pub enum SinkError {
#[error("sink transiently unavailable (backpressured)")]
Transient,
#[error("sink fatal error: {0}")]
Fatal(TransportError),
}
pub struct SinkService<S> {
sender: Arc<S>,
}
impl<S> SinkService<S> {
#[must_use]
pub fn new(sender: Arc<S>) -> Self {
Self { sender }
}
}
impl<S> Clone for SinkService<S> {
fn clone(&self) -> Self {
Self {
sender: Arc::clone(&self.sender),
}
}
}
impl<S> std::fmt::Debug for SinkService<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SinkService").finish_non_exhaustive()
}
}
impl<S> Service<SinkBatch> for SinkService<S>
where
S: TransportSender + 'static,
{
type Response = ();
type Error = SinkError;
type Future = Pin<Box<dyn Future<Output = Result<(), SinkError>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: SinkBatch) -> Self::Future {
let sender = Arc::clone(&self.sender);
Box::pin(async move {
match sender.send_batch(&req.0).await {
SendResult::Ok | SendResult::FilteredDlq => Ok(()),
SendResult::Backpressured => Err(SinkError::Transient),
SendResult::Fatal(e) => Err(SinkError::Fatal(e)),
}
})
}
}
#[derive(Debug, Clone)]
pub struct RateLimitLayer {
limiter: RateLimiter,
}
impl RateLimitLayer {
#[must_use]
pub fn new(limiter: RateLimiter) -> Self {
Self { limiter }
}
}
impl<S> Layer<S> for RateLimitLayer {
type Service = RateLimitService<S>;
fn layer(&self, inner: S) -> Self::Service {
RateLimitService {
inner,
limiter: self.limiter.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct RateLimitService<S> {
inner: S,
limiter: RateLimiter,
}
impl<S, Req> Service<Req> for RateLimitService<S>
where
S: Service<Req> + Clone + Send + 'static,
S::Future: Send,
Req: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<S::Response, S::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Req) -> Self::Future {
let limiter = self.limiter.clone();
let clone = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, clone);
Box::pin(async move {
limiter.acquire().await;
inner.call(req).await
})
}
}
const UNLIMITED_CONCURRENCY: usize = 1 << 20;
#[derive(Clone)]
pub struct SinkStack {
svc: BoxCloneService<SinkBatch, (), BoxError>,
backoff: ExponentialBuilder,
max_retries: usize,
}
impl SinkStack {
#[must_use]
pub fn new<S>(sender: Arc<S>, cfg: &SinkStackConfig) -> Self
where
S: TransportSender + 'static,
{
let limiter = RateLimiter::new(cfg.rate_limit, "sink");
let core: BoxCloneService<SinkBatch, (), BoxError> = if let Some(ac) = cfg.adaptive {
ServiceBuilder::new()
.layer(RateLimitLayer::new(limiter))
.layer(AdaptiveConcurrencyLayer::new(
ac.build_limiter(),
cfg.attempt_timeout(),
))
.timeout(cfg.attempt_timeout())
.service(SinkService::new(sender))
.boxed_clone()
} else {
let concurrency = if cfg.max_concurrency == 0 {
UNLIMITED_CONCURRENCY
} else {
cfg.max_concurrency
};
ServiceBuilder::new()
.layer(RateLimitLayer::new(limiter))
.timeout(cfg.attempt_timeout())
.concurrency_limit(concurrency)
.service(SinkService::new(sender))
.boxed_clone()
};
let svc = if cfg.load_shed {
ServiceBuilder::new()
.load_shed()
.service(core)
.boxed_clone()
} else {
core
};
Self {
svc,
backoff: cfg.backoff(),
max_retries: cfg.max_retries,
}
}
pub async fn send_batch(&self, records: Vec<Record>) -> SendResult {
if records.is_empty() {
return SendResult::Ok;
}
let batch = SinkBatch::new(records);
let svc = self.svc.clone();
let attempt = || {
let mut svc = svc.clone();
let batch = batch.clone();
async move { svc.ready().await?.call(batch).await }
};
let result = attempt
.retry(self.backoff.with_max_times(self.max_retries))
.when(is_transient)
.sleep(tokio::time::sleep)
.notify(|_e: &BoxError, _d: Duration| record_retry())
.await;
match result {
Ok(()) => SendResult::Ok,
Err(e) => classify_final(e),
}
}
pub async fn send_workbatch<T: CommitToken>(&self, batch: &WorkBatch<T>) -> SendResult {
self.send_batch(batch.records.clone()).await
}
}
impl std::fmt::Debug for SinkStack {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SinkStack")
.field("max_retries", &self.max_retries)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
struct AdaptiveConcurrencyLayer {
limiter: Arc<AdaptiveLimiter>,
acquire_timeout: Duration,
}
impl AdaptiveConcurrencyLayer {
fn new(limiter: Arc<AdaptiveLimiter>, acquire_timeout: Duration) -> Self {
Self {
limiter,
acquire_timeout,
}
}
}
impl<S> Layer<S> for AdaptiveConcurrencyLayer {
type Service = AdaptiveConcurrencyService<S>;
fn layer(&self, inner: S) -> Self::Service {
AdaptiveConcurrencyService {
inner,
limiter: Arc::clone(&self.limiter),
acquire_timeout: self.acquire_timeout,
}
}
}
#[derive(Clone)]
struct AdaptiveConcurrencyService<S> {
inner: S,
limiter: Arc<AdaptiveLimiter>,
acquire_timeout: Duration,
}
impl<S, Req> Service<Req> for AdaptiveConcurrencyService<S>
where
S: Service<Req, Error = BoxError> + Clone + Send + 'static,
S::Response: Send + 'static,
S::Future: Send,
Req: Send + 'static,
{
type Response = S::Response;
type Error = BoxError;
type Future = Pin<Box<dyn Future<Output = Result<S::Response, BoxError>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Req) -> Self::Future {
let limiter = Arc::clone(&self.limiter);
let acquire_timeout = self.acquire_timeout;
let clone = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, clone);
Box::pin(async move {
let Some(permit) = limiter.acquire_timeout(acquire_timeout).await else {
return Err(Box::new(SinkError::Transient) as BoxError);
};
let result = inner.call(req).await;
let outcome = match &result {
Ok(_) => Outcome::Success,
Err(e) => {
if matches!(e.downcast_ref::<SinkError>(), Some(SinkError::Fatal(_))) {
Outcome::Success
} else {
Outcome::Overload
}
}
};
limiter.record(outcome);
drop(permit);
result
})
}
}
fn is_transient(e: &BoxError) -> bool {
!matches!(e.downcast_ref::<SinkError>(), Some(SinkError::Fatal(_)))
}
fn classify_final(e: BoxError) -> SendResult {
match e.downcast::<SinkError>() {
Ok(boxed) => match *boxed {
SinkError::Fatal(te) => SendResult::Fatal(te),
SinkError::Transient => SendResult::Backpressured,
},
Err(_) => SendResult::Backpressured,
}
}
fn record_retry() {
#[cfg(feature = "metrics")]
::metrics::counter!("sink_stack_retries_total").increment(1);
}
#[cfg(test)]
mod tests;