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, FrameCodec, RecordRouter, RowEncoder, ShardQueues,
};
use crate::telemetry::RateLimit;
use bytes::{Bytes, BytesMut};
use std::collections::VecDeque;
use std::num::NonZeroU32;
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::clock::{Clock, SystemClock};
#[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,
finished_rows: Option<NonZeroU32>,
parked: u32,
}
struct CodecSlot(Box<dyn FrameCodec>);
impl std::fmt::Debug for CodecSlot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("FrameCodec")
}
}
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)>,
codec: Option<CodecSlot>,
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,
finished_rows: None,
parked: 0,
})
.collect();
let codec = queues.new_frame_codec().map(CodecSlot);
SinkHandoff {
router,
codec,
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;
}
let before = shard.buf.len();
if shard.finished_rows != NonZeroU32::new(shard.rows)
&& let Err(e) = shard.encoder.finish_chunk(&mut shard.buf)
{
shard.buf.truncate(before);
self.meter.0.fatal_error();
self.fatal.0.get_or_insert_with(|| FatalError {
component: self.component.to_string(),
reason: e.to_string(),
});
return;
}
let (frame, raw_len) = match &mut self.codec {
None => {
let frame = shard.buf.split().freeze();
shard.buf.reserve(self.cfg.target_bytes);
let raw_len = frame.len();
(frame, raw_len)
}
Some(CodecSlot(codec)) => match codec.encode_frame(&shard.buf) {
Ok(coded) => {
let frame = Bytes::copy_from_slice(coded);
let raw_len = shard.buf.len();
shard.buf.clear();
shard.finished_rows = None;
if shard.buf.capacity() > 2 * self.cfg.target_bytes {
shard.buf = BytesMut::with_capacity(self.cfg.target_bytes);
}
(frame, raw_len)
}
Err(e) => {
shard.finished_rows = NonZeroU32::new(shard.rows);
self.meter.0.fatal_error();
self.fatal.0.get_or_insert_with(|| FatalError {
component: self.component.to_string(),
reason: e.to_string(),
});
return;
}
},
};
self.budget.add(raw_len);
let chunk = EncodedChunk {
frame,
raw_len,
rows: shard.rows,
acks: std::mem::take(&mut shard.acks),
oldest_ingest: shard
.first_ingest
.take()
.unwrap_or_else(|| SystemClock.now()),
oldest_event_ms: shard.oldest_event_ms,
};
shard.rows = 0;
shard.last_batch = None;
shard.oldest_event_ms = i64::MAX;
if shard.parked > 0 {
shard.parked += 1;
self.parked.push_back((idx, chunk));
return;
}
match self.queues.try_send(idx, chunk) {
Ok(()) => {}
Err(ChunkSendError(chunk)) => {
shard.parked += 1;
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(()) => self.shards[idx].parked -= 1,
Err(ChunkSendError(chunk)) => {
self.parked.push_front((idx, chunk));
return false;
}
}
}
true
}
#[cfg(test)]
pub(crate) fn buffer_capacity(&self, idx: usize) -> usize {
self.shards[idx].buf.capacity()
}
}
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.raw_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(|| SystemClock.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.meter.0.fatal_error();
self.fatal.0.get_or_insert_with(|| 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
}
}
}