#![doc(html_logo_url = "https://raw.githubusercontent.com/emit-rs/emit/main/asset/logo.svg")]
#![deny(missing_docs)]
use crate::internal_metrics::InternalMetrics;
use std::{
any::Any,
cmp, error, fmt,
future::Future,
mem,
panic::{self, AssertUnwindSafe, UnwindSafe},
pin::Pin,
sync::{Arc, Mutex},
task::{Context, Poll},
time::Duration,
};
mod internal_metrics;
pub trait Channel {
type Item;
fn new() -> Self;
fn with_capacity(capacity_hint: usize) -> Self
where
Self: Sized,
{
let _ = capacity_hint;
Self::new()
}
fn push(&mut self, item: Self::Item);
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn clear(&mut self);
}
impl<T> Channel for Vec<T> {
type Item = T;
fn new() -> Self {
Vec::new()
}
fn with_capacity(capacity: usize) -> Self {
Vec::with_capacity(capacity)
}
fn push<'a>(&mut self, item: Self::Item) {
self.push(item);
}
fn len(&self) -> usize {
self.len()
}
fn is_empty(&self) -> bool {
self.is_empty()
}
fn clear(&mut self) {
self.clear()
}
}
pub fn bounded<T: Channel>(max_capacity: usize) -> (Sender<T>, Receiver<T>) {
let shared = Arc::new(Shared {
metrics: Default::default(),
state: Mutex::new(State {
next_batch: Batch::new(),
is_open: true,
is_in_batch: false,
}),
});
(
Sender {
max_capacity,
shared: shared.clone(),
},
Receiver {
idle_delay: Delay::new(Duration::from_millis(1), Duration::from_millis(500)),
retry: Retry::new(10),
retry_delay: Delay::new(Duration::from_millis(700), Duration::from_secs(10)),
capacity: Capacity::new(),
shared,
},
)
}
pub struct Sender<T> {
max_capacity: usize,
shared: Arc<Shared<T>>,
}
impl<T> Drop for Sender<T> {
fn drop(&mut self) {
self.shared.state.lock().unwrap().is_open = false;
}
}
impl<T: Channel> Sender<T> {
pub fn send<'a>(&self, msg: T::Item) {
let mut state = self.shared.state.lock().unwrap();
if state.next_batch.channel.len() >= self.max_capacity {
state.next_batch.channel.clear();
self.shared.metrics.queue_full_truncated.increment();
}
if !state.is_open {
return;
}
state.next_batch.channel.push(msg);
}
pub fn try_send<'a>(&self, msg: T::Item) -> Result<(), BatchError<T::Item>> {
let mut state = self.shared.state.lock().unwrap();
if !state.is_open {
return Err(BatchError::no_retry(TrySendError("the channel is closed")));
}
if state.next_batch.channel.len() < self.max_capacity {
state.next_batch.channel.push(msg);
Ok(())
} else {
Err(BatchError::retry(TrySendError("the channel is full"), msg))
}
}
async fn send_or_wait<'a, FWait: Future<Output = ()> + 'a>(
&'a self,
msg: T::Item,
timeout: Duration,
elapsed: impl Fn() -> Duration,
mut wait_until_empty: impl FnMut(&'a Self, Duration) -> FWait,
) -> Result<(), BatchError<T::Item>> {
match self.try_send(msg) {
Ok(()) => Ok(()),
Err(mut err) => {
self.shared.metrics.queue_full_blocked.increment();
loop {
let elapsed = elapsed();
if elapsed >= timeout {
return Err(err);
}
wait_until_empty(self, timeout.saturating_sub(elapsed)).await;
match self.try_send(err.try_into_retryable()?) {
Ok(()) => return Ok(()),
Err(retry) => {
err = retry;
continue;
}
}
}
}
}
}
pub fn when_empty(&self, f: impl FnOnce() + Send + 'static) {
let mut state = self.shared.state.lock().unwrap();
if state.next_batch.channel.is_empty() {
drop(state);
f();
} else {
state.next_batch.watchers.push_on_take(Box::new(f));
}
}
pub fn when_flushed(&self, f: impl FnOnce() + Send + 'static) {
let mut state = self.shared.state.lock().unwrap();
if !state.is_in_batch && (state.next_batch.channel.is_empty() || !state.is_open) {
drop(state);
f();
}
else {
state.next_batch.watchers.push_on_flush(Box::new(f));
}
}
pub fn metric_source(&self) -> ChannelMetrics<T> {
ChannelMetrics {
shared: self.shared.clone(),
}
}
}
pub struct Receiver<T> {
idle_delay: Delay,
retry: Retry,
retry_delay: Delay,
capacity: Capacity,
shared: Arc<Shared<T>>,
}
impl<T> Drop for Receiver<T> {
fn drop(&mut self) {
self.shared.state.lock().unwrap().is_open = false;
}
}
impl<T: Channel> Receiver<T> {
pub async fn exec<
FBatch: Future<Output = Result<(), BatchError<T>>>,
FWait: Future<Output = ()>,
>(
mut self,
mut wait: impl FnMut(Duration) -> FWait,
mut on_batch: impl FnMut(T) -> FBatch,
) {
let mut next_batch = Batch::new();
loop {
let (mut current_batch, is_open) = {
let mut state = self.shared.state.lock().unwrap();
if state.next_batch.channel.len() > 0 {
state.is_in_batch = true;
(
mem::replace(&mut state.next_batch, mem::take(&mut next_batch)),
state.is_open,
)
}
else {
state.is_in_batch = false;
let watchers = mem::take(&mut state.next_batch.watchers);
let open = state.is_open;
(
Batch {
channel: T::new(),
watchers,
},
open,
)
}
};
current_batch.watchers.notify_on_take();
if current_batch.channel.len() > 0 {
self.retry.reset();
self.retry_delay.reset();
self.idle_delay.reset();
next_batch = Batch {
channel: T::with_capacity(self.capacity.next(current_batch.channel.len())),
watchers: Watchers::new(),
};
loop {
match panic::catch_unwind(AssertUnwindSafe(|| on_batch(current_batch.channel)))
{
Ok(on_batch_future) => {
match CatchUnwind(AssertUnwindSafe(on_batch_future)).await {
Ok(Ok(())) => {
self.shared.metrics.queue_batch_processed.increment();
break;
}
Ok(Err(BatchError { retryable })) => {
self.shared.metrics.queue_batch_failed.increment();
if let Some(retryable) = retryable {
if retryable.len() > 0 && self.retry.next() {
wait(self.retry_delay.next()).await;
current_batch = Batch {
channel: retryable,
watchers: current_batch.watchers,
};
self.shared.metrics.queue_batch_retry.increment();
continue;
}
}
break;
}
Err(_) => {
self.shared.metrics.queue_batch_panicked.increment();
break;
}
}
}
Err(_) => {
self.shared.metrics.queue_batch_panicked.increment();
break;
}
}
}
current_batch.watchers.notify_on_flush();
}
else {
current_batch.watchers.notify_on_flush();
if !is_open {
return;
}
wait(self.idle_delay.next()).await;
}
}
}
pub fn metric_source(&self) -> ChannelMetrics<T> {
ChannelMetrics {
shared: self.shared.clone(),
}
}
}
#[derive(Debug)]
pub struct BatchError<T> {
retryable: Option<T>,
}
impl<T> BatchError<T> {
pub fn no_retry(_: impl std::error::Error + Send + Sync + 'static) -> Self {
BatchError { retryable: None }
}
pub fn retry(_: impl std::error::Error + Send + Sync + 'static, retryable: T) -> Self {
BatchError {
retryable: Some(retryable),
}
}
pub fn try_into_retryable(self) -> Result<T, BatchError<T>> {
self.retryable.ok_or_else(|| BatchError { retryable: None })
}
pub fn into_retryable(self) -> Option<T> {
self.retryable
}
pub fn map_retryable<U>(self, f: impl FnOnce(Option<T>) -> Option<U>) -> BatchError<U> {
BatchError {
retryable: f(self.retryable),
}
}
}
struct TrySendError(&'static str);
impl fmt::Debug for TrySendError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self.0, f)
}
}
impl fmt::Display for TrySendError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(self.0, f)
}
}
impl error::Error for TrySendError {}
struct CatchUnwind<F>(F);
impl<F: Future + UnwindSafe> Future for CatchUnwind<F> {
type Output = Result<F::Output, Box<dyn Any + Send>>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let f = unsafe { Pin::map_unchecked_mut(self, |x| &mut x.0) };
panic::catch_unwind(AssertUnwindSafe(|| f.poll(cx)))?.map(Ok)
}
}
struct Delay {
current: Duration,
step: Duration,
max: Duration,
}
impl Delay {
fn new(step: Duration, max: Duration) -> Delay {
Delay {
current: Duration::ZERO,
step,
max,
}
}
fn reset(&mut self) {
self.current = Duration::ZERO
}
fn next(&mut self) -> Duration {
self.current = cmp::min(self.current * 2 + self.step, self.max);
self.current
}
}
const CAPACITY_WINDOW: usize = 32;
struct Capacity {
rolling_values: [usize; CAPACITY_WINDOW],
idx: usize,
}
impl Capacity {
fn new() -> Self {
Capacity {
rolling_values: [0; CAPACITY_WINDOW],
idx: 0,
}
}
fn next(&mut self, last_len: usize) -> usize {
self.rolling_values[self.idx % CAPACITY_WINDOW] = last_len;
self.idx = self.idx.wrapping_add(1);
let max_len = self.rolling_values.iter().copied().max().unwrap();
max_len.saturating_add(cmp::max(1, max_len / 10))
}
}
struct Retry {
current: u32,
max: u32,
}
impl Retry {
fn new(max: u32) -> Self {
Retry { current: 0, max }
}
fn reset(&mut self) {
self.current = 0;
}
fn next(&mut self) -> bool {
self.current += 1;
self.current <= self.max
}
}
struct Shared<T> {
metrics: InternalMetrics,
state: Mutex<State<T>>,
}
pub struct ChannelMetrics<T> {
shared: Arc<Shared<T>>,
}
impl<T: Channel> emit::metric::Source for ChannelMetrics<T> {
fn sample_metrics<S: emit::metric::sampler::Sampler>(&self, sampler: S) {
let queue_length = { self.shared.state.lock().unwrap().next_batch.channel.len() };
let metrics = self
.shared
.metrics
.sample()
.chain(Some(emit::metric::Metric::new(
emit::pkg!(),
"queue_length",
emit::well_known::METRIC_AGG_LAST,
emit::empty::Empty,
queue_length,
emit::empty::Empty,
)));
for metric in metrics {
sampler.metric(metric);
}
}
}
struct State<T> {
next_batch: Batch<T>,
is_open: bool,
is_in_batch: bool,
}
struct Batch<T> {
channel: T,
watchers: Watchers,
}
impl<T: Channel> Batch<T> {
fn new() -> Self {
Batch {
channel: T::new(),
watchers: Watchers::new(),
}
}
}
impl<T: Channel> Default for Batch<T> {
fn default() -> Self {
Batch::new()
}
}
struct Watchers {
on_take: Vec<Watcher>,
on_flush: Vec<Watcher>,
}
type Watcher = Box<dyn FnOnce() + Send>;
impl Default for Watchers {
fn default() -> Self {
Watchers::new()
}
}
impl Watchers {
fn new() -> Self {
Watchers {
on_take: Vec::new(),
on_flush: Vec::new(),
}
}
fn push_on_flush(&mut self, watcher: Watcher) {
self.on_flush.push(watcher);
}
fn notify_on_flush(&mut self) {
for watcher in mem::take(&mut self.on_flush) {
let _ = panic::catch_unwind(AssertUnwindSafe(watcher));
}
}
fn push_on_take(&mut self, watcher: Watcher) {
self.on_take.push(watcher);
}
fn notify_on_take(&mut self) {
for watcher in mem::take(&mut self.on_take) {
let _ = panic::catch_unwind(AssertUnwindSafe(watcher));
}
}
}
pub mod sync;
#[cfg(all(
feature = "tokio",
not(all(
target_arch = "wasm32",
target_vendor = "unknown",
target_os = "unknown"
))
))]
pub mod tokio;
#[cfg(feature = "web")]
pub mod web;
#[cfg(all(
feature = "tokio",
not(all(
target_arch = "wasm32",
target_vendor = "unknown",
target_os = "unknown"
))
))]
pub use tokio::{blocking_flush, blocking_send};
#[cfg(not(all(
feature = "tokio",
not(all(
target_arch = "wasm32",
target_vendor = "unknown",
target_os = "unknown"
))
)))]
pub use sync::{blocking_flush, blocking_send};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capacity() {
let mut capacity = Capacity::new();
let next = capacity.next(10);
assert_eq!(11, next);
let next = capacity.next(5);
assert_eq!(11, next);
let next = capacity.next(100);
assert_eq!(110, next);
for _ in 0..CAPACITY_WINDOW {
let _ = capacity.next(0);
}
let next = capacity.next(0);
assert_eq!(1, next);
}
}