use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{SystemTime, UNIX_EPOCH};
use bytes::Bytes;
use ruststream::testing::Coordinator;
use tokio::sync::mpsc;
use super::seek::{IN_PROCESS_SHARD, SeekControl, index_of};
use crate::error::KinesisError;
use crate::message::KinesisPosition;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct SubscriptionId(u64);
#[derive(Debug, Clone)]
pub(crate) struct Record {
pub(crate) partition_key: Arc<str>,
pub(crate) data: Bytes,
at_millis: u64,
}
impl Record {
fn delivery(&self, index: usize) -> Delivery {
Delivery {
record: self.clone(),
index,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Delivery {
pub(crate) record: Record,
pub(crate) index: usize,
}
pub(crate) type DeliverySender = mpsc::UnboundedSender<Delivery>;
pub(crate) type DeliveryReceiver = mpsc::UnboundedReceiver<Delivery>;
struct Subscription {
stream: Arc<str>,
sender: DeliverySender,
}
#[derive(Default)]
struct LogState {
subscriptions: HashMap<SubscriptionId, Subscription>,
records: HashMap<String, Vec<Record>>,
}
#[derive(Default)]
pub(crate) struct StreamLog {
state: Mutex<LogState>,
next_id: AtomicU64,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Plan {
pub(crate) target: usize,
pub(crate) count: usize,
}
pub(crate) struct Registration {
pub(crate) id: SubscriptionId,
pub(crate) replay: DeliverySender,
pub(crate) deliveries: DeliveryReceiver,
pub(crate) control: Arc<SeekControl>,
}
fn now_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |since| {
u64::try_from(since.as_millis()).unwrap_or(u64::MAX)
})
}
impl StreamLog {
fn lock(&self) -> MutexGuard<'_, LogState> {
self.state
.lock()
.expect("kinesis in-process log mutex poisoned")
}
pub(crate) fn subscribe(&self, stream: Arc<str>) -> Registration {
let (tx, rx) = mpsc::unbounded_channel();
let id = SubscriptionId(self.next_id.fetch_add(1, Ordering::Relaxed));
self.lock().subscriptions.insert(
id,
Subscription {
stream,
sender: tx.clone(),
},
);
Registration {
id,
replay: tx,
deliveries: rx,
control: Arc::default(),
}
}
pub(crate) fn unsubscribe(&self, id: SubscriptionId) {
self.lock().subscriptions.remove(&id);
}
pub(crate) fn append(
&self,
stream: &str,
partition_key: Arc<str>,
data: Bytes,
coordinator: Option<&Coordinator>,
) {
let record = Record {
partition_key,
data,
at_millis: now_millis(),
};
let mut state = self.lock();
let log = state.records.entry(stream.to_owned()).or_default();
let delivery = record.delivery(log.len());
log.push(record);
for subscription in state.subscriptions.values() {
if *subscription.stream == *stream
&& subscription.sender.send(delivery.clone()).is_ok()
&& let Some(coordinator) = coordinator
{
coordinator.enqueued();
}
}
}
#[allow(clippy::significant_drop_tightening)]
pub(crate) fn reposition(
&self,
stream: &str,
plan: Plan,
queue: &mut DeliveryReceiver,
replay: &DeliverySender,
coordinator: Option<&Coordinator>,
) {
let state = self.lock();
let suffix: Vec<Delivery> = state.records.get(stream).map_or_else(Vec::new, |log| {
log.iter()
.enumerate()
.skip(plan.target)
.map(|(index, record)| record.delivery(index))
.collect()
});
if let Some(coordinator) = coordinator {
for _ in plan.count..suffix.len() {
coordinator.enqueued();
}
}
while queue.try_recv().is_ok() {
if let Some(coordinator) = coordinator {
coordinator.consumed();
}
}
for delivery in suffix {
let _ = replay.send(delivery);
}
}
pub(crate) fn plan(&self, stream: &str, to: &KinesisPosition) -> Result<Plan, KinesisError> {
let state = self.lock();
let plan = Self::plan_over(
state.records.get(stream).map_or(&[][..], Vec::as_slice),
stream,
to,
);
drop(state);
plan
}
fn plan_over(log: &[Record], stream: &str, to: &KinesisPosition) -> Result<Plan, KinesisError> {
let target = match to {
KinesisPosition::Horizon => 0,
KinesisPosition::Latest => log.len(),
KinesisPosition::Timestamp(millis) => {
log.partition_point(|record| record.at_millis < *millis)
}
KinesisPosition::Sequence { shard, sequence } => {
if shard != IN_PROCESS_SHARD {
return Err(KinesisError::Read {
stream: stream.to_owned(),
shard: shard.clone(),
source: Box::from("no live reader for this shard (not owned, or finished)"),
});
}
let index = index_of(sequence).ok_or_else(|| KinesisError::Read {
stream: stream.to_owned(),
shard: shard.clone(),
source: Box::from("the sequence number is not one this stream issued"),
})?;
index.min(log.len())
}
};
Ok(Plan {
target,
count: log.len() - target,
})
}
pub(crate) fn records(&self, stream: &str) -> Vec<Record> {
self.lock().records.get(stream).cloned().unwrap_or_default()
}
pub(crate) fn clear(&self) {
let mut state = self.lock();
state.subscriptions.clear();
state.records.clear();
}
}
impl std::fmt::Debug for StreamLog {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let state = self.lock();
f.debug_struct("StreamLog")
.field("subscriptions", &state.subscriptions.len())
.field("streams", &state.records.len())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::in_process::seek::sequence_of;
fn record(at_millis: u64) -> Record {
Record {
partition_key: Arc::from("key"),
data: Bytes::from_static(b"record"),
at_millis,
}
}
fn target_of(log: &[Record], to: &KinesisPosition) -> Result<usize, KinesisError> {
StreamLog::plan_over(log, "jobs", to).map(|plan| plan.target)
}
#[test]
fn a_timestamp_opens_at_the_first_record_from_that_instant() {
let log = [record(10), record(20), record(30)];
let at = |millis| {
target_of(&log, &KinesisPosition::timestamp(millis)).expect("a timestamp resolves")
};
assert_eq!(at(0), 0);
assert_eq!(at(20), 1);
assert_eq!(at(25), 2);
assert_eq!(at(40), 3);
}
#[test]
fn a_position_this_stream_never_issued_is_refused() {
let log = [record(10)];
let elsewhere = KinesisPosition::sequence("shardId-000000000007", sequence_of(0));
assert!(target_of(&log, &elsewhere).is_err());
let unissued = KinesisPosition::sequence(IN_PROCESS_SHARD, "not-a-sequence-number");
assert!(target_of(&log, &unissued).is_err());
}
#[test]
fn a_sequence_past_the_tip_clamps_to_it() {
let log = [record(10)];
let ahead = KinesisPosition::sequence(IN_PROCESS_SHARD, sequence_of(9));
assert_eq!(target_of(&log, &ahead).expect("a sequence resolves"), 1);
}
}