use crate::cancellation::CancellationToken;
use crate::foundation::{AuthorityId, Head};
use crate::performance::{OperationFailure, OperationReceipt, WorkBudget, WorkCounters, WorkError};
use std::collections::HashMap;
use std::future::Future;
use std::sync::Mutex;
use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NotificationPoll {
Unchanged,
Advanced(Head),
}
#[derive(Debug, Error)]
pub enum NotificationError {
#[error("notification operation was cancelled")]
Cancelled,
#[error("notification adapter state is unavailable")]
Unavailable,
#[error(transparent)]
Work(#[from] WorkError),
}
pub type NotificationResult<T> = Result<OperationReceipt<T>, OperationFailure<NotificationError>>;
pub trait NotificationStore {
fn publish(
&self,
authority_id: AuthorityId,
head: Head,
budget: WorkBudget,
) -> NotificationResult<()>;
fn poll_after(
&self,
authority_id: AuthorityId,
after: Head,
budget: WorkBudget,
) -> NotificationResult<NotificationPoll>;
}
pub trait AsyncNotificationStore {
fn publish_hint(
&self,
authority_id: AuthorityId,
head: Head,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> impl Future<Output = NotificationResult<()>>;
fn poll_hint_after(
&self,
authority_id: AuthorityId,
after: Head,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> impl Future<Output = NotificationResult<NotificationPoll>>;
}
pub trait ImmediateNotificationStore: NotificationStore {}
impl<T: ImmediateNotificationStore + ?Sized> AsyncNotificationStore for T {
async fn publish_hint(
&self,
authority_id: AuthorityId,
head: Head,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> NotificationResult<()> {
cancellation
.check()
.map_err(|_| OperationFailure::before_work(NotificationError::Cancelled))?;
NotificationStore::publish(self, authority_id, head, budget)
}
async fn poll_hint_after(
&self,
authority_id: AuthorityId,
after: Head,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> NotificationResult<NotificationPoll> {
cancellation
.check()
.map_err(|_| OperationFailure::before_work(NotificationError::Cancelled))?;
NotificationStore::poll_after(self, authority_id, after, budget)
}
}
#[derive(Default)]
pub struct MemoryNotificationStore {
heads: Mutex<HashMap<AuthorityId, Head>>,
}
impl MemoryNotificationStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl NotificationStore for MemoryNotificationStore {
fn publish(
&self,
authority_id: AuthorityId,
head: Head,
budget: WorkBudget,
) -> NotificationResult<()> {
let work = WorkCounters {
backend_write_operations: 1,
items_examined: 1,
..WorkCounters::default()
};
work.verify(budget)
.map_err(|error| OperationFailure::before_work(error.into()))?;
let mut heads = self
.heads
.lock()
.map_err(|_| OperationFailure::new(NotificationError::Unavailable, work))?;
match heads.get(&authority_id) {
Some(current) if !head_is_newer(head, *current) => {}
_ => {
heads.insert(authority_id, head);
}
}
Ok(OperationReceipt { value: (), work })
}
fn poll_after(
&self,
authority_id: AuthorityId,
after: Head,
budget: WorkBudget,
) -> NotificationResult<NotificationPoll> {
let work = WorkCounters {
backend_read_operations: 1,
items_examined: 1,
..WorkCounters::default()
};
work.verify(budget)
.map_err(|error| OperationFailure::before_work(error.into()))?;
let heads = self
.heads
.lock()
.map_err(|_| OperationFailure::new(NotificationError::Unavailable, work))?;
let value = heads
.get(&authority_id)
.copied()
.filter(|head| head_is_newer(*head, after))
.map_or(NotificationPoll::Unchanged, NotificationPoll::Advanced);
Ok(OperationReceipt { value, work })
}
}
impl ImmediateNotificationStore for MemoryNotificationStore {}
fn head_is_newer(candidate: Head, current: Head) -> bool {
candidate.epoch > current.epoch
|| (candidate.epoch == current.epoch && candidate.sequence > current.sequence)
}
#[cfg(test)]
#[path = "tests/notification.rs"]
mod tests;