#![cfg_attr(
not(feature = "testing"),
allow(clippy::infallible_destructuring_match)
)]
use std::sync::Arc;
use bytes::Bytes;
use ruststream::{AckError, BytesMut, HeaderMap, IncomingMessage, Partitioned, Positioned, Str};
#[cfg(feature = "testing")]
use crate::in_process::Release;
use crate::lease::{LeaseKey, LeaseStore};
use crate::subscriber::KinesisSeeker;
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"RSK2";
const TEXT_ENVELOPE_MAGIC: [u8; 4] = *b"RSK1";
const ENVELOPE_PREFIX: usize = 8;
const FIELD_LEN: usize = 4;
pub(crate) fn encode_envelope(headers: &HeaderMap, payload: BytesMut) -> Vec<u8> {
let user = || {
headers
.iter()
.filter(|(name, _)| *name != PARTITION_KEY_HEADER)
};
let block: usize = user()
.map(|(name, value)| 2 * FIELD_LEN + name.len() + value.len())
.sum();
if block == 0 {
return Vec::from(payload);
}
let mut out = Vec::with_capacity(ENVELOPE_PREFIX + block + payload.len());
out.extend_from_slice(&ENVELOPE_MAGIC);
out.extend_from_slice(&field_len(block));
for (name, value) in user() {
out.extend_from_slice(&field_len(name.len()));
out.extend_from_slice(name.as_bytes());
out.extend_from_slice(&field_len(value.len()));
out.extend_from_slice(value);
}
out.extend_from_slice(&payload);
out
}
fn field_len(len: usize) -> [u8; FIELD_LEN] {
u32::try_from(len).unwrap_or(0).to_be_bytes()
}
pub(crate) fn decode_envelope(data: &[u8]) -> (HeaderMap, Bytes) {
let parsed = match data.get(..4) {
Some(magic) if magic == ENVELOPE_MAGIC => envelope_parts(data, decode_fields),
Some(magic) if magic == TEXT_ENVELOPE_MAGIC => {
envelope_parts(data, |block| Some(decode_text(block)))
}
_ => None,
};
parsed.map_or_else(
|| (HeaderMap::new(), Bytes::copy_from_slice(data)),
|(headers, payload)| (headers, Bytes::copy_from_slice(payload)),
)
}
fn envelope_parts(
data: &[u8],
decode: fn(&[u8]) -> Option<HeaderMap>,
) -> Option<(HeaderMap, &[u8])> {
let (len, rest) = read_len(data.get(4..)?)?;
let block = rest.get(..len)?;
Some((decode(block)?, &rest[len..]))
}
fn read_len(data: &[u8]) -> Option<(usize, &[u8])> {
let (len, rest) = data.split_first_chunk::<FIELD_LEN>()?;
Some((usize::try_from(u32::from_be_bytes(*len)).ok()?, rest))
}
fn read_field(data: &[u8]) -> Option<(&[u8], &[u8])> {
let (len, rest) = read_len(data)?;
rest.split_at_checked(len)
}
fn decode_fields(mut block: &[u8]) -> Option<HeaderMap> {
let mut headers = HeaderMap::new();
while !block.is_empty() {
let (name, rest) = read_field(block)?;
let (value, rest) = read_field(rest)?;
headers.insert(
String::from_utf8(name.to_vec()).ok()?,
Bytes::copy_from_slice(value),
);
block = rest;
}
Some(headers)
}
fn decode_text(block: &[u8]) -> HeaderMap {
let mut headers = HeaderMap::new();
for line in String::from_utf8_lossy(block).lines() {
if let Some((name, value)) = line.split_once(':') {
headers.insert(name.trim().to_owned(), value.trim().to_owned());
}
}
headers
}
#[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) lease: Arc<LeaseKey>,
pub(crate) owner: Arc<str>,
pub(crate) epoch: u64,
pub(crate) gate: Arc<std::sync::atomic::AtomicU64>,
}
pub(crate) enum Settle {
Checkpoint(Settlement),
#[cfg(feature = "testing")]
InProcess(Release),
}
#[cfg(not(feature = "testing"))]
const _: () = assert!(size_of::<Settle>() == size_of::<Settlement>());
impl Settle {
fn shard(&self) -> &Arc<str> {
match self {
Self::Checkpoint(settlement) => settlement.lease.shard_shared(),
#[cfg(feature = "testing")]
Self::InProcess(release) => release.shard(),
}
}
}
pub struct KinesisMessage {
payload: Bytes,
headers: HeaderMap,
sequence: Arc<str>,
seeker: KinesisSeeker,
settlement: Settle,
}
impl std::fmt::Debug for KinesisMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug = f.debug_struct("KinesisMessage");
match &self.settlement {
Settle::Checkpoint(settlement) => {
debug.field("stream", &settlement.lease.stream());
}
#[cfg(feature = "testing")]
Settle::InProcess(_) => {}
}
debug
.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,
seeker: KinesisSeeker,
settlement: Settle,
) -> Self {
let (mut headers, payload) = decode_envelope(data);
headers.insert(
Str::from_static(PARTITION_KEY_HEADER),
partition_key.to_owned(),
);
headers.insert(Str::from_static(SEQUENCE_HEADER), sequence.to_owned());
headers.insert(
Str::from_static(SHARD_HEADER),
settlement.shard().to_string(),
);
Self {
payload,
headers,
sequence: Arc::from(sequence),
seeker,
settlement,
}
}
pub(crate) fn shard(&self) -> &Arc<str> {
self.settlement.shard()
}
pub(crate) fn sequence(&self) -> &Arc<str> {
&self.sequence
}
pub(crate) fn seeker(&self) -> &KinesisSeeker {
&self.seeker
}
async fn settle(self) -> Result<(), AckError> {
let settlement = match self.settlement {
Settle::Checkpoint(settlement) => settlement,
#[cfg(feature = "testing")]
Settle::InProcess(_) => return Ok(()),
};
let Settlement {
tracker,
index,
store,
lease,
owner,
epoch,
gate,
} = settlement;
if gate.load(std::sync::atomic::Ordering::Acquire) != epoch {
return Ok(());
}
let Some(sequence) = tracker.settle(index) else {
return Ok(()); };
match store.checkpoint(&lease, &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(), &*self.sequence)
}
}
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) -> &HeaderMap {
&self.headers
}
async fn ack(self) -> Result<(), AckError> {
self.settle().await
}
async fn nack(self, requeue: bool) -> Result<(), AckError> {
if requeue {
Err(AckError::Unsupported)
} else {
self.settle().await
}
}
fn partition_key(&self) -> Option<&[u8]> {
Partitioned::partition_key(self)
}
}
#[cfg(test)]
mod tests {
use ruststream::BytesMut;
use super::*;
#[test]
fn a_record_without_an_envelope_keeps_the_buffer_the_framework_wrote() {
let payload = BytesMut::from(&br#"{"id":1}"#[..]);
let written = payload.as_ptr();
let data = encode_envelope(&HeaderMap::new(), payload);
assert_eq!(
data.as_ptr(),
written,
"a publish carrying no header is the record's data as it stands, so the buffer is \
handed over rather than copied"
);
}
#[test]
fn the_envelope_applies_only_when_user_headers_exist() {
let mut headers = HeaderMap::new();
headers.insert(PARTITION_KEY_HEADER, "user-42");
assert_eq!(
encode_envelope(&headers, BytesMut::from(&b"raw"[..])),
b"raw"
);
headers.insert("x-tenant", "acme");
let enveloped = encode_envelope(&headers, BytesMut::from(&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 a_header_value_travels_byte_for_byte() {
let binary: &[u8] = &[0x00, 0x80, b'\r', b'\n', 0xfe, 0xff, 0xc3];
let mut headers = HeaderMap::new();
headers.insert("x-binary", Bytes::from_static(binary));
headers.insert("x-text", " a: b\nc ");
headers.insert("x-empty", "");
let enveloped = encode_envelope(&headers, BytesMut::from(&b"raw"[..]));
let (decoded, payload) = decode_envelope(&enveloped);
assert_eq!(decoded.get("x-binary"), Some(binary));
assert_eq!(decoded.get_str("x-text"), Some(" a: b\nc "));
assert_eq!(decoded.get("x-empty"), Some(&b""[..]));
assert_eq!(decoded.iter().count(), 3);
assert_eq!(payload.as_ref(), b"raw");
}
#[test]
fn a_record_in_the_earlier_text_envelope_reads_back() {
let block = b"x-tenant: acme\n";
let mut data = b"RSK1".to_vec();
data.extend_from_slice(&u32::try_from(block.len()).unwrap().to_be_bytes());
data.extend_from_slice(block);
data.extend_from_slice(b"raw");
let (decoded, payload) = decode_envelope(&data);
assert_eq!(decoded.get_str("x-tenant"), Some("acme"));
assert_eq!(payload.as_ref(), b"raw");
}
#[test]
fn a_blob_that_does_not_parse_as_an_envelope_reads_as_headerless() {
let mut headers = HeaderMap::new();
headers.insert("x-tenant", "acme");
let enveloped = encode_envelope(&headers, BytesMut::new());
for cut in [6, enveloped.len() - 1] {
let (decoded, payload) = decode_envelope(&enveloped[..cut]);
assert_eq!(decoded, HeaderMap::new(), "cut at {cut}");
assert_eq!(payload.as_ref(), &enveloped[..cut]);
}
let mut not_utf8_name = enveloped;
not_utf8_name[12] = 0xff;
assert_eq!(decode_envelope(¬_utf8_name).0, HeaderMap::new());
}
#[test]
fn plain_payloads_read_as_headerless() {
let (headers, payload) = decode_envelope(b"{\"id\":1}");
assert_eq!(headers, HeaderMap::new());
assert_eq!(payload.as_ref(), b"{\"id\":1}");
}
}