use std::collections::HashSet;
use kafka_protocol::records::Record;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IsolationLevel {
ReadUncommitted,
ReadCommitted,
}
impl IsolationLevel {
#[must_use]
pub fn as_i8(self) -> i8 {
match self {
Self::ReadUncommitted => 0,
Self::ReadCommitted => 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AbortedTransaction {
pub producer_id: i64,
pub first_offset: i64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FetchPosition {
pub topic: String,
pub partition: i32,
pub next_offset: i64,
}
impl FetchPosition {
#[must_use]
pub fn new(topic: impl Into<String>, partition: i32, next_offset: i64) -> Self {
Self {
topic: topic.into(),
partition,
next_offset,
}
}
}
#[derive(Debug, Default)]
pub struct Fetched {
pub records: Vec<Record>,
pub next_offset: i64,
}
const CONTROL_ABORT: i16 = 0;
fn control_type(record: &Record) -> Option<i16> {
let key = record.key.as_ref()?;
if key.len() < 4 {
return None;
}
Some(i16::from_be_bytes([key[2], key[3]]))
}
#[must_use]
pub fn filter(
records: Vec<Record>,
aborted: &[AbortedTransaction],
last_stable_offset: i64,
isolation: IsolationLevel,
fetch_offset: i64,
) -> Fetched {
let mut sorted: Vec<AbortedTransaction> = aborted.to_vec();
sorted.sort_by_key(|a| a.first_offset);
let mut pending = sorted.into_iter().peekable();
let mut aborted_producers: HashSet<i64> = HashSet::new();
let read_committed = isolation == IsolationLevel::ReadCommitted;
if aborted.is_empty()
&& !records.iter().any(|r| {
r.control
|| r.offset < fetch_offset
|| (read_committed && r.offset >= last_stable_offset)
})
{
let next_offset = records.last().map_or(fetch_offset, |r| r.offset + 1);
return Fetched {
records,
next_offset,
};
}
let mut kept = Vec::with_capacity(records.len());
let mut next_offset = fetch_offset;
for record in records {
if read_committed && record.offset >= last_stable_offset {
break;
}
next_offset = record.offset + 1;
while pending
.peek()
.is_some_and(|a| a.first_offset <= record.offset)
{
let a = pending.next().expect("peeked");
aborted_producers.insert(a.producer_id);
}
if record.control {
if control_type(&record) == Some(CONTROL_ABORT) {
aborted_producers.remove(&record.producer_id);
}
continue;
}
if read_committed && record.transactional && aborted_producers.contains(&record.producer_id)
{
continue;
}
if record.offset < fetch_offset {
continue;
}
kept.push(record);
}
Fetched {
records: kept,
next_offset,
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use kafka_protocol::records::TimestampType;
fn record(offset: i64, producer_id: i64, transactional: bool) -> Record {
Record {
transactional,
control: false,
partition_leader_epoch: 0,
producer_id,
producer_epoch: 0,
timestamp_type: TimestampType::Creation,
offset,
sequence: offset as i32,
timestamp: 0,
key: Some(Bytes::from(format!("k{offset}"))),
value: Some(Bytes::from(format!("v{offset}"))),
headers: Default::default(),
}
}
fn marker(offset: i64, producer_id: i64, control_type: i16) -> Record {
let mut key = Vec::new();
key.extend_from_slice(&0i16.to_be_bytes());
key.extend_from_slice(&control_type.to_be_bytes());
Record {
transactional: true,
control: true,
partition_leader_epoch: 0,
producer_id,
producer_epoch: 0,
timestamp_type: TimestampType::Creation,
offset,
sequence: 0,
timestamp: 0,
key: Some(Bytes::from(key)),
value: None,
headers: Default::default(),
}
}
const ABORT: i16 = 0;
const COMMIT: i16 = 1;
fn offsets(f: &Fetched) -> Vec<i64> {
f.records.iter().map(|r| r.offset).collect()
}
#[test]
fn plain_records_pass_through() {
let recs = vec![record(0, -1, false), record(1, -1, false)];
let out = filter(recs, &[], 2, IsolationLevel::ReadCommitted, 0);
assert_eq!(offsets(&out), vec![0, 1]);
assert_eq!(out.next_offset, 2);
}
#[test]
fn aborted_records_are_dropped_under_read_committed() {
let recs = vec![record(0, 7, true), record(1, 7, true), marker(2, 7, ABORT)];
let aborted = [AbortedTransaction {
producer_id: 7,
first_offset: 0,
}];
let out = filter(recs, &aborted, 3, IsolationLevel::ReadCommitted, 0);
assert!(out.records.is_empty(), "aborted data reached the caller");
}
#[test]
fn an_entirely_aborted_fetch_still_makes_progress() {
let recs = vec![record(10, 7, true), marker(11, 7, ABORT)];
let aborted = [AbortedTransaction {
producer_id: 7,
first_offset: 10,
}];
let out = filter(recs, &aborted, 12, IsolationLevel::ReadCommitted, 10);
assert!(out.records.is_empty());
assert_eq!(
out.next_offset, 12,
"a fully-filtered fetch must advance the position"
);
}
#[test]
fn a_committed_transaction_after_an_aborted_one_survives() {
let recs = vec![
record(0, 7, true), record(1, 7, true), marker(2, 7, ABORT), record(3, 7, true), record(4, 7, true), marker(5, 7, COMMIT),
];
let aborted = [AbortedTransaction {
producer_id: 7,
first_offset: 0,
}];
let out = filter(recs, &aborted, 6, IsolationLevel::ReadCommitted, 0);
assert_eq!(
offsets(&out),
vec![3, 4],
"the committed transaction after an abort was discarded"
);
assert_eq!(out.next_offset, 6);
}
#[test]
fn only_the_aborted_producer_is_filtered() {
let recs = vec![
record(0, 7, true),
record(1, 8, true),
record(2, 7, true),
record(3, 8, true),
marker(4, 7, ABORT),
marker(5, 8, COMMIT),
];
let aborted = [AbortedTransaction {
producer_id: 7,
first_offset: 0,
}];
let out = filter(recs, &aborted, 6, IsolationLevel::ReadCommitted, 0);
assert_eq!(offsets(&out), vec![1, 3]);
}
#[test]
fn records_at_or_past_the_lso_are_withheld() {
let recs = vec![record(0, -1, false), record(1, 9, true), record(2, 9, true)];
let out = filter(recs, &[], 1, IsolationLevel::ReadCommitted, 0);
assert_eq!(offsets(&out), vec![0]);
assert_eq!(
out.next_offset, 1,
"the position must not advance past the LSO"
);
}
#[test]
fn control_records_are_never_returned() {
for isolation in [
IsolationLevel::ReadCommitted,
IsolationLevel::ReadUncommitted,
] {
let recs = vec![record(0, 7, true), marker(1, 7, COMMIT)];
let out = filter(recs, &[], 2, isolation, 0);
assert_eq!(offsets(&out), vec![0], "isolation {isolation:?}");
}
}
#[test]
fn read_uncommitted_sees_aborted_records() {
let recs = vec![record(0, 7, true), record(1, 7, true)];
let aborted = [AbortedTransaction {
producer_id: 7,
first_offset: 0,
}];
let out = filter(recs, &aborted, 0, IsolationLevel::ReadUncommitted, 0);
assert_eq!(offsets(&out), vec![0, 1]);
}
#[test]
fn the_aborted_list_need_not_be_sorted() {
let recs = vec![
record(0, 7, true),
marker(1, 7, ABORT),
record(2, 8, true),
marker(3, 8, ABORT),
];
let aborted = [
AbortedTransaction {
producer_id: 8,
first_offset: 2,
},
AbortedTransaction {
producer_id: 7,
first_offset: 0,
},
];
let out = filter(recs, &aborted, 4, IsolationLevel::ReadCommitted, 0);
assert!(out.records.is_empty());
}
#[test]
fn records_below_the_fetch_offset_are_dropped() {
let recs = vec![
record(0, -1, false),
record(1, -1, false),
record(2, -1, false),
];
let out = filter(recs, &[], 3, IsolationLevel::ReadCommitted, 2);
assert_eq!(offsets(&out), vec![2]);
assert_eq!(out.next_offset, 3);
}
#[test]
fn an_abort_beginning_before_the_fetch_offset_still_applies() {
let recs = vec![
record(0, 7, true),
record(1, 7, true),
record(2, 7, true),
marker(3, 7, ABORT),
];
let aborted = [AbortedTransaction {
producer_id: 7,
first_offset: 0,
}];
let out = filter(recs, &aborted, 4, IsolationLevel::ReadCommitted, 2);
assert!(
out.records.is_empty(),
"an abort that began before the fetch offset was forgotten"
);
}
#[test]
fn an_empty_fetch_does_not_move_the_position() {
let out = filter(Vec::new(), &[], 5, IsolationLevel::ReadCommitted, 5);
assert!(out.records.is_empty());
assert_eq!(out.next_offset, 5);
}
}