mod deliveries;
mod log;
mod seek;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use bytes::Bytes;
use ruststream::testing::Coordinator;
use ruststream::{BytesMut, OutgoingFor, OutgoingMessage, RawMessage, Str, Take};
pub(crate) use deliveries::{LogDeliveries, Release};
pub(crate) use seek::LogSeeker;
use crate::claim::StreamClaim;
use crate::error::KinesisError;
use crate::message::{PARTITION_KEY_HEADER, decode_envelope, encode_envelope};
use crate::publisher::{KinesisPublishOptions, resolve_partition_key};
use crate::stream::KinesisStream;
use crate::subscriber::KinesisSubscriber;
use log::StreamLog;
const MAX_STREAM_NAME: usize = 128;
const MAX_PARTITION_KEY: usize = 256;
const MAX_RECORD: usize = 10 * 1024 * 1024;
#[derive(Debug, Default)]
pub(crate) struct Bus {
log: StreamLog,
coordinator: OnceLock<Coordinator>,
closed: AtomicBool,
}
impl Bus {
pub(crate) fn new() -> Arc<Self> {
Arc::default()
}
pub(crate) fn log(&self) -> &StreamLog {
&self.log
}
pub(crate) fn install(&self, coordinator: Coordinator) {
let _ = self.coordinator.set(coordinator);
}
pub(crate) fn coordinator(&self) -> Option<&Coordinator> {
self.coordinator.get()
}
pub(crate) fn ensure_open(&self) -> Result<(), KinesisError> {
if self.closed.load(Ordering::Acquire) {
return Err(KinesisError::NotConnected);
}
Ok(())
}
pub(crate) fn close(&self) {
self.closed.store(true, Ordering::Release);
self.log.clear();
}
pub(crate) fn subscribe(
self: &Arc<Self>,
descriptor: &KinesisStream,
claim: StreamClaim,
) -> Result<KinesisSubscriber, KinesisError> {
self.ensure_open()?;
let stream = descriptor.stream();
check_stream(stream).map_err(|reason| KinesisError::Stream {
stream: stream.to_owned(),
source: Box::from(reason),
})?;
Ok(KinesisSubscriber::in_process(
stream.to_owned(),
LogDeliveries::open(self, stream),
claim,
))
}
pub(crate) fn put(
&self,
msg: OutgoingFor<'_, Take>,
options: Option<&KinesisPublishOptions>,
from_policy: Option<&str>,
) -> Result<(), KinesisError> {
self.ensure_open()?;
let partition_key = resolve_partition_key(options, msg.headers(), from_policy);
let (stream, payload, headers) = msg.into_parts();
let data = encode_envelope(&headers, payload);
self.append(stream, partition_key, data)
}
pub(crate) fn inject(&self, message: OutgoingMessage<'_>) -> Result<(), KinesisError> {
self.ensure_open()?;
let partition_key = resolve_partition_key(None, message.headers(), None);
let (stream, payload, headers) = message.into_parts();
let data = encode_envelope(&headers, BytesMut::from(payload));
self.append(stream, partition_key, data)
}
fn append(
&self,
stream: &str,
partition_key: String,
data: Vec<u8>,
) -> Result<(), KinesisError> {
check_record(stream, &partition_key, data.len()).map_err(|reason| {
KinesisError::Publish {
stream: stream.to_owned(),
source: Box::from(reason),
}
})?;
self.log.append(
stream,
Arc::from(partition_key),
Bytes::from(data),
self.coordinator(),
);
Ok(())
}
pub(crate) fn published(&self, stream: &str) -> Vec<RawMessage> {
self.log
.records(stream)
.into_iter()
.map(|record| {
let (mut headers, payload) = decode_envelope(&record.data);
headers.insert(
Str::from_static(PARTITION_KEY_HEADER),
record.partition_key.to_string(),
);
RawMessage::new(stream.to_owned(), payload).with_headers(headers)
})
.collect()
}
}
fn check_stream(stream: &str) -> Result<(), String> {
if stream.is_empty() || stream.len() > MAX_STREAM_NAME {
return Err(format!(
"a stream name is 1 to {MAX_STREAM_NAME} characters long, this one is {}",
stream.len()
));
}
if let Some(refused) = stream
.chars()
.find(|c| !(c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-')))
{
return Err(format!(
"a stream name holds letters, digits, '_', '.' and '-', and {refused:?} is none of \
them"
));
}
Ok(())
}
fn check_record(stream: &str, partition_key: &str, data: usize) -> Result<(), String> {
check_stream(stream)?;
let key_chars = partition_key.chars().count();
if key_chars == 0 || key_chars > MAX_PARTITION_KEY {
return Err(format!(
"a partition key is 1 to {MAX_PARTITION_KEY} characters long, this one is \
{key_chars}"
));
}
let size = data + partition_key.len();
if size > MAX_RECORD {
return Err(format!(
"a record carries at most {MAX_RECORD} bytes of data and partition key, this one \
carries {size}"
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_stream_name_takes_what_the_service_takes() {
assert!(check_stream("orders.dead-letter_v2").is_ok());
assert!(check_stream("").is_err());
assert!(check_stream(&"s".repeat(129)).is_err());
assert!(check_stream("orders/dead").is_err());
assert!(check_stream("arn:aws:kinesis:us-east-1:123456789012:stream/orders").is_err());
}
#[test]
fn a_record_takes_what_the_service_takes() {
assert!(check_record("orders", "k", MAX_RECORD - 1).is_ok());
assert!(check_record("orders", "", 1).is_err());
assert!(check_record("orders", &"k".repeat(257), 1).is_err());
assert!(check_record("orders", &"é".repeat(256), 1).is_ok());
assert!(check_record("orders", "k", MAX_RECORD).is_err());
}
}