use crate::store::error::{StorageError, StorageResult};
use error_stack::{AttachmentKind, FrameKind, Report};
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
pub struct Durable<'a, T>(pub(crate) &'a T);
#[derive(Default)]
pub struct CommitSignal {
begun: AtomicU64,
settled: Mutex<Settled>,
waiters: Mutex<Vec<Waker>>,
}
#[derive(Default)]
struct Settled {
landed: u64,
failed: u64,
given_up: Option<(u64, Arc<Report<StorageError>>)>,
}
impl CommitSignal {
pub(crate) fn begin(&self) -> u64 {
self.begun.fetch_add(1, Ordering::AcqRel) + 1
}
pub(crate) fn settle(&self, flush: u64, ended: &StorageResult<()>) {
let mut settled = self.settled.lock().unwrap();
match ended {
Ok(()) => {
settled.landed = settled.landed.max(flush);
drop(settled);
self.wake();
}
Err(_) => settled.failed = settled.failed.max(flush),
}
}
pub(crate) fn gave_up(&self, why: Arc<Report<StorageError>>) {
{
let mut settled = self.settled.lock().unwrap();
let through = settled.failed;
settled.given_up = Some((through, why));
}
self.wake();
}
fn begun(&self) -> u64 {
self.begun.load(Ordering::Acquire)
}
fn answer(&self, after: u64) -> Option<StorageResult<()>> {
let settled = self.settled.lock().unwrap();
if settled.landed > after {
return Some(Ok(()));
}
match &settled.given_up {
Some((through, why)) if *through > after => Some(Err(commit_failed(why))),
_ => None,
}
}
fn park(&self, cx: &Context<'_>) {
self.waiters.lock().unwrap().push(cx.waker().clone());
}
fn wake(&self) {
let woken = std::mem::take(&mut *self.waiters.lock().unwrap());
for waker in woken {
waker.wake();
}
}
}
#[derive(Default)]
pub struct PersistHealth {
given_up: Mutex<Option<Arc<Report<StorageError>>>>,
}
impl PersistHealth {
pub fn failure(&self) -> Option<Arc<Report<StorageError>>> {
self.given_up.lock().unwrap().clone()
}
pub(crate) fn give_up(&self, reason: Arc<Report<StorageError>>) {
*self.given_up.lock().unwrap() = Some(reason);
}
pub(crate) fn landed(&self) {
*self.given_up.lock().unwrap() = None;
}
}
pub struct Commit(Awaiting);
enum Awaiting {
Flush {
signal: Arc<CommitSignal>,
after: u64,
},
Gone,
}
impl Commit {
pub(crate) fn awaiting(signal: Arc<CommitSignal>) -> Self {
let after = signal.begun();
Self(Awaiting::Flush { signal, after })
}
pub(crate) fn gone() -> Self {
Self(Awaiting::Gone)
}
}
impl Future for Commit {
type Output = StorageResult<()>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let Awaiting::Flush { signal, after } = &self.0 else {
return Poll::Ready(Err(Report::new(StorageError::Closed)));
};
if let Some(answer) = signal.answer(*after) {
return Poll::Ready(answer);
}
signal.park(cx);
match signal.answer(*after) {
Some(answer) => Poll::Ready(answer),
None => Poll::Pending,
}
}
}
fn commit_failed(why: &Report<StorageError>) -> Report<StorageError> {
why.frames().fold(
Report::new(StorageError::CommitFailed),
|told, frame| match frame.kind() {
FrameKind::Context(context) => told.attach(context.to_string()),
FrameKind::Attachment(AttachmentKind::Printable(said)) => told.attach(said.to_string()),
FrameKind::Attachment(_) => told,
},
)
}
#[cfg(test)]
mod tests {
use super::*;
fn poll(commit: &mut Commit) -> Poll<StorageResult<()>> {
Pin::new(commit).poll(&mut Context::from_waker(Waker::noop()))
}
fn said(why: &Report<StorageError>) -> String {
let attached: Vec<&str> = why
.frames()
.filter_map(|frame| frame.downcast_ref::<String>().map(String::as_str))
.collect();
format!("{}\n{}", why.current_context(), attached.join("\n"))
}
#[test]
fn a_flush_already_under_way_does_not_answer_a_waiter_that_came_after_it() {
let signal = Arc::new(CommitSignal::default());
let under_way = signal.begin();
let mut commit = Commit::awaiting(signal.clone());
signal.settle(under_way, &Ok(()));
assert!(
poll(&mut commit).is_pending(),
"that flush gathered its writes before this waiter's write was there"
);
let next = signal.begin();
signal.settle(next, &Ok(()));
assert!(matches!(poll(&mut commit), Poll::Ready(Ok(()))));
}
#[test]
fn a_waiter_hears_why_the_flush_it_waited_on_gave_up() {
let signal = Arc::new(CommitSignal::default());
let mut commit = Commit::awaiting(signal.clone());
let attempt = signal.begin();
signal.settle(
attempt,
&Err(Report::new(StorageError::Flush).attach("the disk is full")),
);
assert!(
poll(&mut commit).is_pending(),
"an attempt inside the retry budget answers nobody"
);
signal.gave_up(Arc::new(
Report::new(StorageError::Flush).attach("the disk is full"),
));
let Poll::Ready(Err(why)) = poll(&mut commit) else {
panic!("a waiter was not told the flush gave up");
};
insta::assert_snapshot!(said(&why));
}
#[test]
fn a_landing_after_the_give_up_answers_a_waiter_that_had_not_heard_yet() {
let signal = Arc::new(CommitSignal::default());
let mut commit = Commit::awaiting(signal.clone());
let failed = signal.begin();
signal.settle(failed, &Err(Report::new(StorageError::Flush)));
signal.gave_up(Arc::new(Report::new(StorageError::Flush)));
let landed = signal.begin();
signal.settle(landed, &Ok(()));
assert!(matches!(poll(&mut commit), Poll::Ready(Ok(()))));
}
#[test]
fn a_commit_for_a_store_that_is_gone_says_it_was_closed() {
let Poll::Ready(Err(why)) = poll(&mut Commit::gone()) else {
panic!("a commit for a store that is gone did not answer at once");
};
assert_eq!(*why.current_context(), StorageError::Closed);
}
}