use std::sync::Arc;
use bytes::Bytes;
use ruststream::{AckError, Headers, IncomingMessage, Partitioned, Positioned};
use crate::lease::LeaseStore;
use crate::track::Watermark;
pub const PARTITION_KEY_HEADER: &str = "partition-key";
pub const SEQUENCE_HEADER: &str = "kinesis-sequence-number";
pub const SHARD_HEADER: &str = "kinesis-shard-id";
pub(crate) const KPL_MAGIC: [u8; 4] = [0xF3, 0x89, 0x9A, 0xC2];
pub(crate) const ENVELOPE_MAGIC: [u8; 4] = *b"RSK1";
pub(crate) fn encode_envelope(headers: &Headers, payload: &[u8]) -> Vec<u8> {
let mut lines = String::new();
for (name, value) in headers.iter() {
if name == PARTITION_KEY_HEADER {
continue;
}
lines.push_str(name);
lines.push_str(": ");
lines.push_str(&String::from_utf8_lossy(value));
lines.push('\n');
}
if lines.is_empty() {
return payload.to_vec();
}
let header_bytes = lines.as_bytes();
let mut out = Vec::with_capacity(8 + header_bytes.len() + payload.len());
out.extend_from_slice(&ENVELOPE_MAGIC);
out.extend_from_slice(&u32::try_from(header_bytes.len()).unwrap_or(0).to_be_bytes());
out.extend_from_slice(header_bytes);
out.extend_from_slice(payload);
out
}
pub(crate) fn decode_envelope(data: &[u8]) -> (Headers, Bytes) {
if data.len() >= 8 && data[0..4] == ENVELOPE_MAGIC {
let len = u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize;
if data.len() >= 8 + len {
let mut headers = Headers::new();
let text = String::from_utf8_lossy(&data[8..8 + len]);
for line in text.lines() {
if let Some((name, value)) = line.split_once(':') {
headers.insert(name.trim().to_owned(), value.trim().to_owned());
}
}
return (headers, Bytes::copy_from_slice(&data[8 + len..]));
}
}
(Headers::new(), Bytes::copy_from_slice(data))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KinesisPosition {
Horizon,
Latest,
Timestamp(u64),
Sequence {
shard: String,
sequence: String,
},
}
impl KinesisPosition {
#[must_use]
pub const fn horizon() -> Self {
Self::Horizon
}
#[must_use]
pub const fn latest() -> Self {
Self::Latest
}
#[must_use]
pub const fn timestamp(millis: u64) -> Self {
Self::Timestamp(millis)
}
#[must_use]
pub fn sequence(shard: impl Into<String>, sequence: impl Into<String>) -> Self {
Self::Sequence {
shard: shard.into(),
sequence: sequence.into(),
}
}
}
pub(crate) struct Settlement {
pub(crate) tracker: Arc<Watermark>,
pub(crate) index: u64,
pub(crate) store: Arc<dyn LeaseStore>,
pub(crate) shard: String,
pub(crate) owner: String,
pub(crate) epoch: u64,
pub(crate) gate: Arc<std::sync::atomic::AtomicU64>,
}
pub struct KinesisMessage {
payload: Bytes,
headers: Headers,
sequence: String,
settlement: Settlement,
}
impl std::fmt::Debug for KinesisMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KinesisMessage")
.field("shard", &self.settlement.shard)
.field("payload_len", &self.payload.len())
.finish_non_exhaustive()
}
}
impl KinesisMessage {
pub(crate) fn new(
data: &[u8],
partition_key: &str,
sequence: &str,
settlement: Settlement,
) -> Self {
let (mut headers, payload) = decode_envelope(data);
headers.insert(PARTITION_KEY_HEADER, partition_key.to_owned());
headers.insert(SEQUENCE_HEADER, sequence.to_owned());
headers.insert(SHARD_HEADER, settlement.shard.clone());
Self {
payload,
headers,
sequence: sequence.to_owned(),
settlement,
}
}
async fn settle(self) -> Result<(), AckError> {
let Settlement {
tracker,
index,
store,
shard,
owner,
epoch,
gate,
} = self.settlement;
if gate.load(std::sync::atomic::Ordering::Acquire) != epoch {
return Ok(());
}
let Some(sequence) = tracker.settle(index) else {
return Ok(()); };
match store.checkpoint(&shard, &owner, &sequence).await {
Ok(_) => Ok(()),
Err(err) => Err(AckError::Broker(err)),
}
}
}
impl Positioned for KinesisMessage {
type Position = KinesisPosition;
fn position(&self) -> KinesisPosition {
KinesisPosition::sequence(self.settlement.shard.clone(), self.sequence.clone())
}
}
impl Partitioned for KinesisMessage {
fn partition_key(&self) -> Option<&[u8]> {
self.headers.get(PARTITION_KEY_HEADER)
}
}
impl IncomingMessage for KinesisMessage {
fn payload(&self) -> &[u8] {
&self.payload
}
fn headers(&self) -> &Headers {
&self.headers
}
async fn ack(self) -> Result<(), AckError> {
self.settle().await
}
async fn nack(self, requeue: bool) -> Result<(), AckError> {
if requeue {
Ok(())
} else {
self.settle().await
}
}
fn partition_key(&self) -> Option<&[u8]> {
Partitioned::partition_key(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_envelope_applies_only_when_user_headers_exist() {
let mut headers = Headers::new();
headers.insert(PARTITION_KEY_HEADER, "user-42");
assert_eq!(encode_envelope(&headers, b"raw"), b"raw");
headers.insert("x-tenant", "acme");
let enveloped = encode_envelope(&headers, b"raw");
assert_eq!(enveloped[0..4], ENVELOPE_MAGIC);
let (decoded, payload) = decode_envelope(&enveloped);
assert_eq!(decoded.get_str("x-tenant"), Some("acme"));
assert!(decoded.get(PARTITION_KEY_HEADER).is_none());
assert_eq!(payload.as_ref(), b"raw");
}
#[test]
fn plain_payloads_read_as_headerless() {
let (headers, payload) = decode_envelope(b"{\"id\":1}");
assert!(headers.is_empty());
assert_eq!(payload.as_ref(), b"{\"id\":1}");
}
}