use std::{error::Error, fmt, sync::Arc, time::Duration};
use chrono::{DateTime, Utc};
use futures_util::future;
use tokio::sync::watch;
use crate::{Categorized, Category, ProjectionError, Revision, Subject};
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct ProjectionOptions {
pub name: Option<String>,
}
impl ProjectionOptions {
#[must_use]
pub fn named(name: impl Into<String>) -> Self {
Self {
name: Some(name.into()),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Phase {
CatchingUp,
Live,
Reconnecting,
Stopped,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ProjectionStatus {
pub phase: Phase,
pub since: DateTime<Utc>,
pub error: Option<Arc<ProjectionError>>,
pub attempts: usize,
pub revision: Option<Revision>,
pub has_caught_up: bool,
}
#[derive(Clone, Debug)]
pub struct ProjectionRun {
inner: Arc<Inner>,
}
#[derive(Debug)]
struct Inner {
name: Option<String>,
subject: Subject,
status: watch::Sender<ProjectionStatus>,
}
impl ProjectionRun {
pub(crate) fn new(subject: Subject, options: ProjectionOptions) -> Self {
assert!(
options.name.as_deref() != Some(""),
"architecturekit: a projection needs a name that is not empty, or none"
);
let (status, _) = watch::channel(ProjectionStatus {
phase: Phase::CatchingUp,
since: Utc::now(),
error: None,
attempts: 0,
revision: None,
has_caught_up: false,
});
Self {
inner: Arc::new(Inner {
name: options.name,
subject,
status,
}),
}
}
#[must_use]
pub fn name(&self) -> Option<&str> {
self.inner.name.as_deref()
}
#[must_use]
pub fn status(&self) -> ProjectionStatus {
self.inner.status.borrow().clone()
}
#[must_use]
pub fn error(&self) -> Option<Arc<ProjectionError>> {
let status = self.inner.status.borrow();
if status.phase == Phase::Stopped {
return status.error.clone();
}
None
}
pub async fn caught_up(&self) {
self.wait_for(|status| status.has_caught_up).await;
}
pub async fn done(&self) {
self.wait_for(|status| status.phase == Phase::Stopped).await;
}
async fn wait_for(&self, holds: impl FnMut(&ProjectionStatus) -> bool) {
let mut status = self.inner.status.subscribe();
let _ = status.wait_for(holds).await;
}
pub(crate) fn caught_up_now(&self) {
self.inner.status.send_modify(|status| {
status.phase = Phase::Live;
status.since = Utc::now();
status.error = None;
status.attempts = 0;
status.has_caught_up = true;
});
}
pub(crate) fn disrupted(&self, failure: Arc<ProjectionError>) {
self.inner.status.send_modify(|status| {
if status.phase != Phase::Reconnecting {
status.phase = Phase::Reconnecting;
status.since = Utc::now();
status.attempts = 0;
}
status.error = Some(failure);
status.attempts += 1;
});
}
pub(crate) fn committed(&self, event_id: Revision) {
self.inner.status.send_modify(|status| {
status.revision = Some(event_id);
});
}
pub(crate) fn stop(&self, failure: Option<ProjectionError>) {
self.inner.status.send_modify(|status| {
status.phase = Phase::Stopped;
status.since = Utc::now();
status.error = failure.map(Arc::new);
status.attempts = 0;
});
}
}
pub async fn wait_caught_up(runs: &[&ProjectionRun]) -> Result<(), CaughtUpError> {
let mut statuses: Vec<_> = runs
.iter()
.map(|run| run.inner.status.subscribe())
.collect();
loop {
if let Some(verdict) = caught_up_verdict(runs) {
return verdict;
}
let changes = statuses.iter_mut().map(|status| Box::pin(status.changed()));
let _ = future::select_all(changes).await;
}
}
fn caught_up_verdict(runs: &[&ProjectionRun]) -> Option<Result<(), CaughtUpError>> {
let mut has_every_run_caught_up = true;
let mut stopped = None;
for run in runs {
let status = run.status();
if status.has_caught_up {
continue;
}
has_every_run_caught_up = false;
if status.phase != Phase::Stopped {
continue;
}
if let Some(error) = status.error {
return Some(Err(CaughtUpError::Failed {
projection: run.name().map(ToOwned::to_owned),
subject: run.inner.subject.clone(),
error,
}));
}
if stopped.is_none() {
stopped = Some(*run);
}
}
if has_every_run_caught_up {
return Some(Ok(()));
}
stopped.map(|run| {
Err(CaughtUpError::Stopped {
projection: run.name().map(ToOwned::to_owned),
subject: run.inner.subject.clone(),
})
})
}
#[derive(Debug)]
#[non_exhaustive]
pub enum CaughtUpError {
#[non_exhaustive]
Failed {
projection: Option<String>,
subject: Subject,
error: Arc<ProjectionError>,
},
#[non_exhaustive]
Stopped {
projection: Option<String>,
subject: Subject,
},
}
impl fmt::Display for CaughtUpError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Failed {
projection: Some(name),
error,
..
} => write!(formatter, "projection {name:?}: {error}"),
Self::Failed {
projection: None,
subject,
error,
} => write!(formatter, "projection of {:?}: {error}", subject.as_str()),
Self::Stopped {
projection: Some(name),
..
} => write!(
formatter,
"the projection {name:?} stopped before it caught up"
),
Self::Stopped {
projection: None,
subject,
} => write!(
formatter,
"the projection of {:?} stopped before it caught up",
subject.as_str()
),
}
}
}
impl Error for CaughtUpError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Failed { error, .. } => error.source(),
Self::Stopped { .. } => None,
}
}
}
impl Categorized for CaughtUpError {
fn category(&self) -> Option<Category> {
match self {
Self::Failed { error, .. } => error.category(),
Self::Stopped { .. } => None,
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Reconnect {
pub projection: Option<String>,
pub subject: Subject,
pub error: Arc<ProjectionError>,
pub delay: Duration,
pub attempt: usize,
}