use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use async_trait::async_trait;
use ciborium::Value as CborValue;
use fake::Fake;
use tokio::sync::broadcast;
use tokio::time::{Instant, interval};
use vantage_diorama::ChangeEvent;
use vantage_types::Record;
use vantage_vista::mocks::MockShell;
use crate::FakerColumn;
use crate::value_gen::ValueGen;
pub struct FakerCtx {
shell: MockShell,
events: broadcast::Sender<ChangeEvent>,
columns: Vec<FakerColumn>,
id_column: String,
values: ValueGen,
seq: AtomicU64,
}
impl FakerCtx {
pub fn new(
shell: MockShell,
events: broadcast::Sender<ChangeEvent>,
columns: Vec<FakerColumn>,
id_column: String,
) -> Self {
Self {
shell,
events,
columns,
id_column,
values: ValueGen::new(),
seq: AtomicU64::new(0),
}
}
fn generate(&self, id: &str) -> Record<CborValue> {
self.values.record_for(&self.columns, &self.id_column, id)
}
fn next_fifo_id(&self) -> String {
let seq = self.seq.fetch_add(1, Ordering::SeqCst);
format!("{:020}", u64::MAX - seq)
}
fn next_seed_id(&self) -> String {
let seq = self.seq.fetch_add(1, Ordering::SeqCst);
format!("{seq:020}")
}
pub fn seed_one(&self) -> String {
let id = self.next_seed_id();
self.shell.set_record(&id, self.generate(&id));
id
}
pub fn push(&self) -> String {
let id = self.next_fifo_id();
let record = self.generate(&id);
self.shell.set_record(&id, record.clone());
let _ = self.events.send(ChangeEvent::Inserted {
id: id.clone(),
new: Some(record),
});
id
}
pub fn expire(&self, id: &str) {
self.shell.remove_record(id);
let _ = self
.events
.send(ChangeEvent::Deleted { id: id.to_string() });
}
}
#[async_trait]
pub trait FakerEffect: Send + Sync {
fn seed(&self, ctx: &FakerCtx);
async fn run(&self, _ctx: Arc<FakerCtx>) {}
fn is_live(&self) -> bool {
false
}
}
pub struct StaticEffect {
pub count: usize,
}
#[async_trait]
impl FakerEffect for StaticEffect {
fn seed(&self, ctx: &FakerCtx) {
for _ in 0..self.count {
ctx.seed_one();
}
}
}
pub struct FifoEffect {
pub interval: Duration,
pub retention_lo: Duration,
pub retention_hi: Duration,
}
impl FifoEffect {
fn random_ttl(&self) -> Duration {
let lo = self.retention_lo.as_millis() as u64;
let hi = (self.retention_hi.as_millis() as u64).max(lo + 1);
Duration::from_millis((lo..hi).fake())
}
}
#[async_trait]
impl FakerEffect for FifoEffect {
fn seed(&self, _ctx: &FakerCtx) {
}
fn is_live(&self) -> bool {
true
}
async fn run(&self, ctx: Arc<FakerCtx>) {
let mut ticker = interval(self.interval);
let mut pending: VecDeque<(Instant, String)> = VecDeque::new();
loop {
ticker.tick().await;
let id = ctx.push();
pending.push_back((Instant::now() + self.random_ttl(), id));
let now = Instant::now();
let mut i = 0;
while i < pending.len() {
if pending[i].0 <= now {
let (_, expired) = pending.remove(i).expect("index in range");
ctx.expire(&expired);
} else {
i += 1;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::sync::broadcast::error::TryRecvError;
fn columns() -> Vec<FakerColumn> {
vec![
FakerColumn {
name: "id".into(),
ty: "string".into(),
flags: vec!["id".into()],
},
FakerColumn {
name: "email".into(),
ty: "string".into(),
flags: vec![],
},
]
}
fn ctx() -> (Arc<FakerCtx>, broadcast::Receiver<ChangeEvent>) {
let shell = MockShell::new();
let (tx, rx) = broadcast::channel(64);
let ctx = Arc::new(FakerCtx::new(shell, tx, columns(), "id".into()));
(ctx, rx)
}
#[test]
fn static_effect_seeds_exactly_count_rows_without_events() {
let (ctx, mut rx) = ctx();
StaticEffect { count: 20 }.seed(&ctx);
assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty)));
assert_eq!(count_store(&ctx), 20);
}
#[tokio::test]
async fn push_stores_and_broadcasts_inserted() {
let (ctx, mut rx) = ctx();
let id = ctx.push();
match rx.try_recv().unwrap() {
ChangeEvent::Inserted { id: got, new } => {
assert_eq!(got, id);
assert!(new.is_some());
}
other => panic!("expected Inserted, got {other:?}"),
}
assert_eq!(count_store(&ctx), 1);
}
#[tokio::test]
async fn expire_removes_and_broadcasts_deleted() {
let (ctx, mut rx) = ctx();
let id = ctx.push();
let _ = rx.try_recv(); ctx.expire(&id);
assert!(matches!(rx.try_recv().unwrap(), ChangeEvent::Deleted { id: got } if got == id));
assert_eq!(count_store(&ctx), 0);
}
#[test]
fn fifo_ids_descend_so_ascending_order_is_newest_first() {
let (ctx, _rx) = ctx();
let a = ctx.push();
let b = ctx.push();
assert!(a > b, "expected newer id {b} to sort before older {a}");
}
fn count_store(ctx: &FakerCtx) -> usize {
ctx.shell.len()
}
}