use super::Collector;
use super::chain::{FatalSlot, OpMeterSlot, StageLifecycle};
use crate::backpressure::InflightBudget;
use crate::checkpoint::{AckSet, BatchId};
use crate::deser::RecFamily;
use crate::error::{ErrorClass, ErrorPolicy, FatalError, SinkError};
use crate::record::{Flow, Record};
use crate::sink::{ChunkSendError, EncodedChunk, RecordRouter, RowEncoder, ShardQueues};
use crate::telemetry::RateLimit;
use bytes::BytesMut;
use std::collections::VecDeque;
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug)]
pub struct ChunkConfig {
pub target_bytes: usize,
pub encode_policy: ErrorPolicy,
}
impl Default for ChunkConfig {
fn default() -> Self {
ChunkConfig {
target_bytes: 64 * 1024,
encode_policy: ErrorPolicy::Skip,
}
}
}
#[derive(Debug)]
struct ShardBuf<E> {
encoder: E,
buf: BytesMut,
rows: u32,
acks: AckSet,
last_batch: Option<BatchId>,
first_ingest: Option<Instant>,
oldest_event_ms: i64,
}
static ENCODE_SKIP_WARN: RateLimit = RateLimit::new(5, Duration::from_secs(10));
#[derive(Debug)]
pub struct SinkHandoff<F: RecFamily, E, R> {
router: R,
queues: ShardQueues,
budget: Arc<InflightBudget>,
cfg: ChunkConfig,
shards: Vec<ShardBuf<E>>,
parked: VecDeque<(usize, EncodedChunk)>,
pub(crate) meter: OpMeterSlot,
pub(crate) fatal: FatalSlot,
component: Arc<str>,
_family: std::marker::PhantomData<fn() -> F>,
}
impl<F: RecFamily, E, R> SinkHandoff<F, E, R>
where
E: RowEncoder<F> + Clone,
{
pub(crate) fn new(
encoder: E,
router: R,
queues: ShardQueues,
budget: Arc<InflightBudget>,
cfg: ChunkConfig,
meter: OpMeterSlot,
component: Arc<str>,
) -> Self {
assert!(cfg.target_bytes > 0, "chunk target must be non-zero");
let shards = (0..queues.num_shards())
.map(|_| ShardBuf {
encoder: encoder.clone(),
buf: BytesMut::with_capacity(cfg.target_bytes),
rows: 0,
acks: AckSet::new(),
last_batch: None,
first_ingest: None,
oldest_event_ms: i64::MAX,
})
.collect();
SinkHandoff {
router,
queues,
budget,
cfg,
shards,
parked: VecDeque::new(),
meter,
fatal: FatalSlot(None),
component,
_family: std::marker::PhantomData,
}
}
fn seal_and_send(&mut self, idx: usize) {
let shard = &mut self.shards[idx];
if shard.rows == 0 {
return;
}
if let Err(e) = shard.encoder.finish_chunk(&mut shard.buf) {
self.fatal.0 = Some(FatalError {
component: self.component.to_string(),
reason: e.to_string(),
});
return;
}
let frame = shard.buf.split().freeze();
shard.buf.reserve(self.cfg.target_bytes);
self.budget.add(frame.len());
let chunk = EncodedChunk {
frame,
rows: shard.rows,
acks: std::mem::take(&mut shard.acks),
oldest_ingest: shard.first_ingest.take().unwrap_or_else(Instant::now),
oldest_event_ms: shard.oldest_event_ms,
};
shard.rows = 0;
shard.last_batch = None;
shard.oldest_event_ms = i64::MAX;
match self.queues.try_send(idx, chunk) {
Ok(()) => {}
Err(ChunkSendError(chunk)) => self.parked.push_back((idx, chunk)),
}
}
fn drain_parked(&mut self) -> bool {
while let Some((idx, chunk)) = self.parked.pop_front() {
match self.queues.try_send(idx, chunk) {
Ok(()) => {}
Err(ChunkSendError(chunk)) => {
self.parked.push_front((idx, chunk));
return false;
}
}
}
true
}
}
impl<F: RecFamily, E, R> Drop for SinkHandoff<F, E, R> {
fn drop(&mut self) {
for (_, chunk) in self.parked.drain(..) {
self.budget.sub(chunk.frame.len());
}
}
}
impl<'buf, F, E, R> Collector<<F as RecFamily>::Rec<'buf>> for SinkHandoff<F, E, R>
where
F: RecFamily,
E: RowEncoder<F> + Clone,
R: RecordRouter<F>,
{
fn push(&mut self, rec: Record<F::Rec<'buf>>) -> Flow {
self.meter.0.seen();
if self.fatal.0.is_some() {
return Flow::Continue;
}
let idx = self.router.route_record(&rec, self.shards.len());
let shard = &mut self.shards[idx];
let before = shard.buf.len();
match shard.encoder.encode(&rec, &mut shard.buf) {
Ok(()) => {
shard.rows += 1;
self.meter.0.out();
shard.first_ingest.get_or_insert_with(Instant::now);
shard.oldest_event_ms = shard.oldest_event_ms.min(rec.meta.event_time_ms);
let bid = rec.ack.batch_id();
if shard.last_batch != Some(bid) {
shard.acks.push(rec.ack.clone());
shard.last_batch = Some(bid);
}
if shard.buf.len() + shard.encoder.buffered_bytes() >= self.cfg.target_bytes {
self.seal_and_send(idx);
}
Flow::Continue
}
Err(e) => {
shard.buf.truncate(before);
let fatal_class = matches!(
e,
SinkError::Client {
class: ErrorClass::Fatal,
..
}
);
match self.cfg.encode_policy {
ErrorPolicy::Skip if !fatal_class => {
self.meter.0.skipped();
self.meter.0.record_error();
crate::rate_limited_warn!(
ENCODE_SKIP_WARN,
component = &*self.component,
error = %e,
"record skipped by sink encoder error policy"
);
}
_ => {
self.fatal.0 = Some(FatalError {
component: self.component.to_string(),
reason: e.to_string(),
});
}
}
Flow::Continue
}
}
}
}
impl<F: RecFamily, E, R> StageLifecycle for SinkHandoff<F, E, R>
where
E: RowEncoder<F> + Clone,
{
fn on_batch_end(&mut self, elapsed: Duration) {
self.meter.0.flush(elapsed);
}
fn take_fatal(&mut self) -> Option<FatalError> {
self.fatal.0.take()
}
fn relieve(&mut self) -> Flow {
if self.parked.is_empty() || self.drain_parked() {
Flow::Continue
} else {
Flow::Blocked
}
}
fn flush_terminal(&mut self) -> Flow {
for idx in 0..self.shards.len() {
self.seal_and_send(idx);
}
if self.drain_parked() {
Flow::Continue
} else {
Flow::Blocked
}
}
}