use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, SystemTime};
use async_trait::async_trait;
use super::{
Appended, Capacity, CapacityPolicy, Claim, ConsumerId, Delivery, DeliveryId, DeliveryMode,
IdempotencyKey, JournalError, JournalStats, OrderingKey, PoisonReason, UsageEvent,
UsageJournal,
};
#[derive(Debug, Clone)]
struct Entry {
position: u64,
event: UsageEvent,
}
#[derive(Debug, Default)]
struct ConsumerState {
acked: BTreeSet<u64>,
quarantined: BTreeMap<u64, PoisonReason>,
attempts: BTreeMap<u64, u32>,
leases: BTreeMap<u64, SystemTime>,
}
#[derive(Debug, Default)]
struct Storage {
entries: Vec<Entry>,
positions: BTreeMap<IdempotencyKey, u64>,
consumers: BTreeMap<ConsumerId, ConsumerState>,
next_position: u64,
dropped: u64,
}
impl Storage {
fn entry(&self, position: u64) -> Option<&Entry> {
self.entries.iter().find(|entry| entry.position == position)
}
fn delivered(&self) -> Vec<u64> {
if self.consumers.is_empty() {
return Vec::new();
}
self.entries
.iter()
.map(|entry| entry.position)
.filter(|position| !self.is_quarantined(*position))
.filter(|position| {
self.consumers
.values()
.all(|state| state.acked.contains(position))
})
.collect()
}
fn forget(&mut self, position: u64) {
let Some(index) = self
.entries
.iter()
.position(|entry| entry.position == position)
else {
return;
};
let removed = self.entries.remove(index);
self.positions.remove(removed.event.idempotency_key());
for state in self.consumers.values_mut() {
state.acked.remove(&position);
state.attempts.remove(&position);
state.leases.remove(&position);
}
}
fn reclaim_delivered(&mut self, max_events: u64) -> usize {
let mut reclaimed = 0;
for position in self.delivered() {
if (self.entries.len() as u64) < max_events {
break;
}
self.forget(position);
reclaimed += 1;
}
reclaimed
}
fn is_quarantined(&self, position: u64) -> bool {
self.consumers
.values()
.any(|state| state.quarantined.contains_key(&position))
}
fn prune_acknowledged(&mut self, retain: Duration, now: SystemTime) {
let expired: Vec<u64> = self
.delivered()
.into_iter()
.filter(|position| {
self.entry(*position)
.is_some_and(|entry| entry.event.observed_at() + retain <= now)
})
.collect();
for position in expired {
self.forget(position);
}
}
}
pub(crate) struct InMemoryUsageJournal {
storage: Arc<Mutex<Storage>>,
capacity: Capacity,
}
impl InMemoryUsageJournal {
pub(crate) fn new() -> Self {
Self::with_capacity(Capacity::BILLING_GRADE)
}
pub(crate) fn with_capacity(capacity: Capacity) -> Self {
Self {
storage: Arc::new(Mutex::new(Storage::default())),
capacity,
}
}
pub(crate) fn restart(&self) -> Self {
Self {
storage: Arc::clone(&self.storage),
capacity: self.capacity,
}
}
fn locked(&self) -> MutexGuard<'_, Storage> {
self.storage.lock().expect("journal mutex is not poisoned")
}
pub(crate) fn stored_events(&self) -> usize {
self.locked().entries.len()
}
}
#[async_trait]
impl UsageJournal for InMemoryUsageJournal {
fn name(&self) -> &'static str {
"memory"
}
fn capacity(&self) -> Capacity {
self.capacity
}
fn mode(&self) -> DeliveryMode {
DeliveryMode::TelemetryGrade
}
async fn append(&self, event: &UsageEvent) -> Result<Appended, JournalError> {
let mut storage = self.locked();
storage.prune_acknowledged(self.capacity.retain_acknowledged, SystemTime::now());
if let Some(position) = storage.positions.get(event.idempotency_key()).copied() {
let stored = storage.entry(position).expect("indexed position exists");
if stored.event.is_same_fact_as(event) {
return Ok(Appended::AlreadyPresent { position });
}
return Err(JournalError::Conflict {
key: event.idempotency_key().clone(),
});
}
if storage.entries.len() as u64 >= self.capacity.max_events {
storage.reclaim_delivered(self.capacity.max_events);
}
if storage.entries.len() as u64 >= self.capacity.max_events {
let retained = storage.entries.len() as u64;
match self.capacity.policy {
CapacityPolicy::Refuse => {
return Err(JournalError::AtCapacity {
pending: retained,
capacity: self.capacity,
});
}
CapacityPolicy::DropOldest => {
let Some(oldest) = storage
.entries
.iter()
.map(|entry| entry.position)
.find(|position| !storage.is_quarantined(*position))
else {
return Err(JournalError::AtCapacity {
pending: retained,
capacity: self.capacity,
});
};
storage.forget(oldest);
storage.dropped += 1;
}
}
}
let position = storage.next_position;
storage.next_position += 1;
storage
.positions
.insert(event.idempotency_key().clone(), position);
storage.entries.push(Entry {
position,
event: event.clone(),
});
Ok(Appended::Accepted { position })
}
async fn claim(
&self,
consumer: &ConsumerId,
claim: Claim,
) -> Result<Vec<Delivery>, JournalError> {
let mut storage = self.locked();
let capacity = self.capacity;
let mut poisoned: Vec<(u64, PoisonReason)> = Vec::new();
let mut claimed: Vec<Delivery> = Vec::new();
let mut busy: HashSet<OrderingKey> = HashSet::new();
let entries: Vec<Entry> = storage.entries.clone();
let state = storage.consumers.entry(consumer.clone()).or_default();
for entry in &entries {
if state.acked.contains(&entry.position)
|| state.quarantined.contains_key(&entry.position)
{
continue;
}
let key = entry.event.ordering_key().clone();
if let Some(expiry) = state.leases.get(&entry.position).copied() {
if expiry > claim.now {
busy.insert(key);
continue;
}
state.leases.remove(&entry.position);
}
if busy.contains(&key) {
continue;
}
let attempt = state.attempts.get(&entry.position).copied().unwrap_or(0) + 1;
if attempt > capacity.max_delivery_attempts {
poisoned.push((entry.position, PoisonReason::AttemptsExhausted));
continue;
}
state.attempts.insert(entry.position, attempt);
let expiry = claim.now + claim.lease;
state.leases.insert(entry.position, expiry);
busy.insert(key);
claimed.push(Delivery {
id: DeliveryId {
consumer: consumer.clone(),
event: entry.event.id(),
attempt,
},
event: entry.event.clone(),
lease_expires_at: expiry,
});
if claimed.len() >= claim.max_events {
break;
}
}
for (position, reason) in poisoned {
state.quarantined.insert(position, reason);
state.leases.remove(&position);
}
Ok(claimed)
}
async fn ack(&self, delivery: &DeliveryId) -> Result<(), JournalError> {
let mut storage = self.locked();
let Some(position) = storage
.positions
.get(&IdempotencyKey::from(delivery.event))
.copied()
else {
return Ok(());
};
let Some(state) = storage.consumers.get_mut(&delivery.consumer) else {
return Err(JournalError::NotOutstanding {
delivery: delivery.clone(),
});
};
if state.acked.contains(&position) {
return Ok(());
}
if state.quarantined.contains_key(&position) {
return Err(JournalError::Quarantined {
delivery: delivery.clone(),
});
}
if !state.attempts.contains_key(&position) {
return Err(JournalError::NotOutstanding {
delivery: delivery.clone(),
});
}
state.acked.insert(position);
state.leases.remove(&position);
Ok(())
}
async fn relinquish(&self, delivery: &DeliveryId) -> Result<(), JournalError> {
let mut storage = self.locked();
let Some(position) = storage
.positions
.get(&IdempotencyKey::from(delivery.event))
.copied()
else {
return Ok(());
};
let Some(state) = storage.consumers.get_mut(&delivery.consumer) else {
return Err(JournalError::NotOutstanding {
delivery: delivery.clone(),
});
};
if state.acked.contains(&position) || state.quarantined.contains_key(&position) {
return Ok(());
}
let Some(attempts) = state.attempts.get_mut(&position) else {
return Err(JournalError::NotOutstanding {
delivery: delivery.clone(),
});
};
if *attempts == delivery.attempt {
*attempts = attempts.saturating_sub(1);
}
Ok(())
}
async fn quarantine(
&self,
delivery: &DeliveryId,
reason: PoisonReason,
) -> Result<(), JournalError> {
let mut storage = self.locked();
let Some(position) = storage
.positions
.get(&IdempotencyKey::from(delivery.event))
.copied()
else {
return Err(JournalError::NotOutstanding {
delivery: delivery.clone(),
});
};
let Some(state) = storage.consumers.get_mut(&delivery.consumer) else {
return Err(JournalError::NotOutstanding {
delivery: delivery.clone(),
});
};
if state.quarantined.contains_key(&position) {
return Ok(());
}
if state.acked.contains(&position) {
return Err(JournalError::AlreadyAcknowledged {
delivery: delivery.clone(),
});
}
if !state.attempts.contains_key(&position) {
return Err(JournalError::NotOutstanding {
delivery: delivery.clone(),
});
}
state.quarantined.insert(position, reason);
state.leases.remove(&position);
Ok(())
}
async fn stats(&self, consumer: &ConsumerId) -> Result<JournalStats, JournalError> {
let now = SystemTime::now();
let storage = self.locked();
let state = storage.consumers.get(consumer);
let mut stats = JournalStats {
pending: 0,
in_flight: 0,
quarantined: 0,
oldest_pending_age: None,
dropped: storage.dropped,
capacity: self.capacity,
};
let mut oldest: Option<SystemTime> = None;
for entry in &storage.entries {
let (acked, quarantined, leased) = state.map_or((false, false, None), |state| {
(
state.acked.contains(&entry.position),
state.quarantined.contains_key(&entry.position),
state.leases.get(&entry.position).copied(),
)
});
if quarantined {
stats.quarantined += 1;
continue;
}
if acked {
continue;
}
if leased.is_some_and(|expiry| expiry > now) {
stats.in_flight += 1;
continue;
}
stats.pending += 1;
let observed = entry.event.observed_at();
if oldest.is_none_or(|current| observed < current) {
oldest = Some(observed);
}
}
stats.oldest_pending_age =
oldest.map(|observed| now.duration_since(observed).unwrap_or(Duration::ZERO));
Ok(stats)
}
}