use std::collections::BTreeMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use chrono::{DateTime, Utc};
use uuid::Uuid;
use super::{Obligation, SlaError};
pub type StoreFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, SlaError>> + Send + 'a>>;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ObligationRecord {
pub obligation: Obligation,
pub generation: Uuid,
pub escalated_at: Option<DateTime<Utc>>,
pub due_at: Option<DateTime<Utc>>,
pub dispatch_token: Option<Uuid>,
}
pub(super) fn no_deadline() -> DateTime<Utc> {
DateTime::<Utc>::from_timestamp(253_402_300_799, 0).unwrap_or(DateTime::<Utc>::MAX_UTC)
}
pub(super) fn real_deadline(due: Option<DateTime<Utc>>) -> Option<DateTime<Utc>> {
due.filter(|due| *due < no_deadline())
}
impl ObligationRecord {
#[must_use]
pub const fn new(obligation: Obligation, generation: Uuid) -> Self {
Self {
obligation,
generation,
escalated_at: None,
due_at: None,
dispatch_token: None,
}
}
#[must_use]
pub const fn with_due_at(mut self, due_at: DateTime<Utc>) -> Self {
self.due_at = Some(due_at);
self
}
}
pub trait ObligationStore: Send + Sync + 'static {
fn insert(&self, record: ObligationRecord) -> StoreFuture<'_, (ObligationRecord, bool)>;
fn get<'a>(&'a self, key: &'a str) -> StoreFuture<'a, Option<ObligationRecord>>;
fn list(&self) -> StoreFuture<'_, Vec<ObligationRecord>>;
fn mark_met<'a>(
&'a self,
key: &'a str,
generation: Uuid,
at: DateTime<Utc>,
) -> StoreFuture<'a, bool>;
fn set_due<'a>(
&'a self,
key: &'a str,
generation: Uuid,
due_at: DateTime<Utc>,
) -> StoreFuture<'a, bool>;
fn begin_dispatch<'a>(
&'a self,
key: &'a str,
generation: Uuid,
due_at: DateTime<Utc>,
token: Uuid,
) -> StoreFuture<'a, bool>;
fn claim_escalation<'a>(
&'a self,
key: &'a str,
generation: Uuid,
due_at: DateTime<Utc>,
at: DateTime<Utc>,
) -> StoreFuture<'a, bool>;
fn release_escalation<'a>(
&'a self,
key: &'a str,
generation: Uuid,
claimed_at: DateTime<Utc>,
) -> StoreFuture<'a, ()>;
fn remove<'a>(&'a self, key: &'a str) -> StoreFuture<'a, bool>;
}
#[derive(Debug, Clone, Default)]
pub struct MemoryObligationStore {
records: Arc<Mutex<BTreeMap<String, ObligationRecord>>>,
}
impl MemoryObligationStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn with<T: Send + 'static>(
&self,
f: impl FnOnce(&mut BTreeMap<String, ObligationRecord>) -> T,
) -> StoreFuture<'_, T> {
let value = f(&mut self
.records
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner));
Box::pin(std::future::ready(Ok(value)))
}
fn with_instance<T: Send + 'static>(
&self,
key: &str,
generation: Uuid,
f: impl FnOnce(&mut ObligationRecord) -> T,
) -> StoreFuture<'_, Option<T>> {
self.with(|records| {
records
.get_mut(key)
.filter(|record| record.generation == generation)
.map(f)
})
}
}
impl ObligationStore for MemoryObligationStore {
fn insert(&self, record: ObligationRecord) -> StoreFuture<'_, (ObligationRecord, bool)> {
self.with(|records| match records.entry(record.obligation.key()) {
std::collections::btree_map::Entry::Occupied(entry) => (entry.get().clone(), false),
std::collections::btree_map::Entry::Vacant(entry) => {
(entry.insert(record).clone(), true)
}
})
}
fn get<'a>(&'a self, key: &'a str) -> StoreFuture<'a, Option<ObligationRecord>> {
self.with(|records| records.get(key).cloned())
}
fn list(&self) -> StoreFuture<'_, Vec<ObligationRecord>> {
self.with(|records| records.values().cloned().collect())
}
fn mark_met<'a>(
&'a self,
key: &'a str,
generation: Uuid,
at: DateTime<Utc>,
) -> StoreFuture<'a, bool> {
let changed = self.with_instance(key, generation, |record| {
let unset = record.obligation.met().is_none();
if unset {
record.obligation.set_met(at);
}
unset
});
Box::pin(async move { Ok(changed.await?.unwrap_or(false)) })
}
fn set_due<'a>(
&'a self,
key: &'a str,
generation: Uuid,
due_at: DateTime<Utc>,
) -> StoreFuture<'a, bool> {
let changed = self.with_instance(key, generation, |record| {
let open = record.escalated_at.is_none();
if open {
record.due_at = Some(due_at);
}
open
});
Box::pin(async move { Ok(changed.await?.unwrap_or(false)) })
}
fn begin_dispatch<'a>(
&'a self,
key: &'a str,
generation: Uuid,
due_at: DateTime<Utc>,
token: Uuid,
) -> StoreFuture<'a, bool> {
let owned = self.with_instance(key, generation, |record| {
if let Some(stored) = record.dispatch_token {
return stored == token;
}
let current = record.due_at.is_none_or(|due| due == due_at)
&& record.obligation.met().is_none_or(|met| met > due_at);
if current {
record.dispatch_token = Some(token);
}
current
});
Box::pin(async move { Ok(owned.await?.unwrap_or(false)) })
}
fn claim_escalation<'a>(
&'a self,
key: &'a str,
generation: Uuid,
due_at: DateTime<Utc>,
at: DateTime<Utc>,
) -> StoreFuture<'a, bool> {
let claimed = self.with_instance(key, generation, |record| {
let open = record.escalated_at.is_none()
&& record.due_at.is_none_or(|due| due == due_at)
&& record.obligation.met().is_none_or(|met| met > due_at);
if open {
record.escalated_at = Some(at);
}
open
});
Box::pin(async move { Ok(claimed.await?.unwrap_or(false)) })
}
fn release_escalation<'a>(
&'a self,
key: &'a str,
generation: Uuid,
claimed_at: DateTime<Utc>,
) -> StoreFuture<'a, ()> {
let released = self.with_instance(key, generation, |record| {
if record.escalated_at == Some(claimed_at) && record.dispatch_token.is_none() {
record.escalated_at = None;
}
});
Box::pin(async move { released.await.map(|_| ()) })
}
fn remove<'a>(&'a self, key: &'a str) -> StoreFuture<'a, bool> {
self.with(|records| records.remove(key).is_some())
}
}