use crate::context::SourceContext;
use rdkafka::Message;
use rdkafka::consumer::base_consumer::PartitionQueue;
use rdkafka::message::BorrowedMessage;
use spate_core::checkpoint::{AckIssuer, AckRef};
use spate_core::error::SourceError;
use spate_core::record::{PartitionId, RawPayload};
use spate_core::source::{LaneId, PayloadBatch, SourceLane};
use std::time::Duration;
pub struct KafkaLane {
id: LaneId,
partition: PartitionId,
held: Vec<BorrowedMessage<'static>>,
queue: PartitionQueue<SourceContext>,
issuer: AckIssuer,
}
impl std::fmt::Debug for KafkaLane {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KafkaLane")
.field("id", &self.id)
.field("partition", &self.partition)
.field("held", &self.held.len())
.finish_non_exhaustive()
}
}
impl KafkaLane {
pub(crate) fn new(
id: LaneId,
partition: PartitionId,
queue: PartitionQueue<SourceContext>,
issuer: AckIssuer,
) -> Self {
KafkaLane {
id,
partition,
held: Vec::new(),
queue,
issuer,
}
}
}
unsafe fn erase_lifetime(msg: BorrowedMessage<'_>) -> BorrowedMessage<'static> {
unsafe { std::mem::transmute::<BorrowedMessage<'_>, BorrowedMessage<'static>>(msg) }
}
impl SourceLane for KafkaLane {
type Batch<'a> = KafkaBatch<'a>;
fn id(&self) -> LaneId {
self.id
}
fn partition(&self) -> PartitionId {
self.partition
}
fn poll(
&mut self,
max_records: usize,
timeout: Duration,
) -> Result<Option<Self::Batch<'_>>, SourceError> {
self.held.clear();
match self.queue.poll(timeout) {
None => return Ok(None),
Some(Err(e)) => {
return Err(SourceError::Client {
class: crate::error::classify_poll_error(&e, true),
reason: format!("partition {} poll: {e}", self.partition.0),
});
}
Some(Ok(msg)) => {
self.held.push(unsafe { erase_lifetime(msg) });
}
}
while self.held.len() < max_records {
match self.queue.poll(Duration::ZERO) {
Some(Ok(msg)) => {
self.held.push(unsafe { erase_lifetime(msg) });
}
Some(Err(e)) => {
tracing::debug!(partition = self.partition.0, error = %e,
"queue error while batching; delivering partial batch");
break;
}
None => break,
}
}
let last_offset = self
.held
.last()
.expect("batch has at least the first message")
.offset();
let ack = self.issuer.issue(self.partition, last_offset);
Ok(Some(KafkaBatch {
msgs: &self.held,
idx: 0,
ack,
partition: self.partition,
}))
}
}
#[derive(Debug)]
pub struct KafkaBatch<'a> {
msgs: &'a [BorrowedMessage<'static>],
idx: usize,
ack: AckRef,
partition: PartitionId,
}
impl<'a> PayloadBatch<'a> for KafkaBatch<'a> {
fn next_payload(&mut self) -> Option<RawPayload<'a>> {
let msg = self.msgs.get(self.idx)?;
self.idx += 1;
Some(RawPayload {
bytes: msg.payload().unwrap_or(&[]),
key: msg.key(),
partition: self.partition,
offset: msg.offset(),
timestamp_ms: msg.timestamp().to_millis().unwrap_or(0),
})
}
fn ack(&self) -> &AckRef {
&self.ack
}
}