use std::{
future::{self, Future},
panic::AssertUnwindSafe,
};
use eventsourcingdb::{
error::ClientError,
request_options::{Bound, BoundType, ReadEventsOptions},
};
use futures_util::{FutureExt, Stream, StreamExt};
use super::{
Store,
failure::{Sorted, read_failure},
};
use crate::{
Permanent, Projected, Projection, ProjectionError, ResumableProjection, Subjects, Transaction,
TransactionalProjection,
};
#[cfg(test)]
mod tests;
pub(crate) trait Writer: Send {
fn checkpoint(
&mut self,
) -> impl Future<Output = Result<Option<String>, ProjectionError>> + Send;
fn begin(&mut self) -> impl Future<Output = Result<(), ProjectionError>> + Send;
fn apply(
&mut self,
event: &eventsourcingdb::Event,
) -> impl Future<Output = Result<(), ProjectionError>> + Send;
fn commit(
&mut self,
last_event_id: &str,
) -> impl Future<Output = Result<(), ProjectionError>> + Send;
fn rollback(&mut self) -> impl Future<Output = Result<(), ProjectionError>> + Send;
fn batch_size(&self) -> usize;
}
async fn project<E: Projected, F: Future<Output = Result<(), ProjectionError>>>(
event: &eventsourcingdb::Event,
upcasters: Option<&crate::Upcasters>,
mut apply: impl FnMut(crate::Envelope<E>) -> F,
) -> Result<(), ProjectionError> {
for envelope in E::project(event, upcasters)? {
apply(envelope).await?;
}
Ok(())
}
pub(crate) struct RebuildWriter<'p, P> {
projection: &'p P,
last_applied: Option<String>,
}
impl<'p, P> RebuildWriter<'p, P> {
pub(crate) fn new(projection: &'p P) -> Self {
Self {
projection,
last_applied: None,
}
}
}
impl<P: Projection> Writer for RebuildWriter<'_, P> {
fn checkpoint(
&mut self,
) -> impl Future<Output = Result<Option<String>, ProjectionError>> + Send {
future::ready(Ok(self.last_applied.clone()))
}
fn begin(&mut self) -> impl Future<Output = Result<(), ProjectionError>> + Send {
future::ready(Ok(()))
}
async fn apply(&mut self, event: &eventsourcingdb::Event) -> Result<(), ProjectionError> {
let projection = self.projection;
project(event, P::upcasters(), |envelope| async move {
projection
.apply(envelope)
.await
.map_err(ProjectionError::of)
})
.await?;
self.last_applied = Some(event.id().to_owned());
Ok(())
}
fn commit(&mut self, _: &str) -> impl Future<Output = Result<(), ProjectionError>> + Send {
future::ready(Ok(()))
}
fn rollback(&mut self) -> impl Future<Output = Result<(), ProjectionError>> + Send {
future::ready(Ok(()))
}
fn batch_size(&self) -> usize {
1
}
}
pub(crate) struct ResumableWriter<'p, P> {
projection: &'p P,
last_applied: Option<String>,
}
impl<'p, P> ResumableWriter<'p, P> {
pub(crate) fn new(projection: &'p P) -> Self {
Self {
projection,
last_applied: None,
}
}
}
impl<P: ResumableProjection> Writer for ResumableWriter<'_, P> {
async fn checkpoint(&mut self) -> Result<Option<String>, ProjectionError> {
if self.last_applied.is_some() {
return Ok(self.last_applied.clone());
}
self.projection
.checkpoint()
.await
.map_err(ProjectionError::of)
}
fn begin(&mut self) -> impl Future<Output = Result<(), ProjectionError>> + Send {
future::ready(Ok(()))
}
async fn apply(&mut self, event: &eventsourcingdb::Event) -> Result<(), ProjectionError> {
let projection = self.projection;
project(event, P::upcasters(), |envelope| async move {
projection
.apply(envelope)
.await
.map_err(ProjectionError::of)
})
.await?;
self.last_applied = Some(event.id().to_owned());
Ok(())
}
async fn commit(&mut self, last_event_id: &str) -> Result<(), ProjectionError> {
self.projection
.save_checkpoint(last_event_id)
.await
.map_err(ProjectionError::of)
}
fn rollback(&mut self) -> impl Future<Output = Result<(), ProjectionError>> + Send {
future::ready(Ok(()))
}
fn batch_size(&self) -> usize {
self.projection.catch_up_batch_size().max(1)
}
}
pub(crate) struct TransactionalWriter<'p, P: TransactionalProjection> {
projection: &'p P,
transaction: Option<P::Transaction>,
}
impl<'p, P: TransactionalProjection> TransactionalWriter<'p, P> {
pub(crate) fn new(projection: &'p P) -> Self {
Self {
projection,
transaction: None,
}
}
}
impl<P: TransactionalProjection> Writer for TransactionalWriter<'_, P> {
async fn checkpoint(&mut self) -> Result<Option<String>, ProjectionError> {
self.projection
.checkpoint()
.await
.map_err(ProjectionError::of)
}
async fn begin(&mut self) -> Result<(), ProjectionError> {
self.transaction = Some(self.projection.begin().await.map_err(ProjectionError::of)?);
Ok(())
}
async fn apply(&mut self, event: &eventsourcingdb::Event) -> Result<(), ProjectionError> {
let transaction = self
.transaction
.as_mut()
.expect("a transaction has begun before an event is applied");
for envelope in P::Event::project(event, P::upcasters())? {
AssertUnwindSafe(transaction.apply(envelope))
.catch_unwind()
.await
.map_err(|payload| ProjectionError::panicked(&*payload))?
.map_err(ProjectionError::of)?;
}
Ok(())
}
async fn commit(&mut self, last_event_id: &str) -> Result<(), ProjectionError> {
let transaction = self
.transaction
.take()
.expect("a transaction has begun before it is committed");
transaction
.commit(last_event_id)
.await
.map_err(ProjectionError::of)
}
async fn rollback(&mut self) -> Result<(), ProjectionError> {
let Some(transaction) = self.transaction.take() else {
return Ok(());
};
AssertUnwindSafe(transaction.rollback())
.catch_unwind()
.await
.map_err(|payload| ProjectionError::panicked(&*payload))?
.map_err(ProjectionError::of)
}
fn batch_size(&self) -> usize {
self.projection.catch_up_batch_size().max(1)
}
}
fn read_failed(error: ClientError) -> ProjectionError {
match read_failure(error, "reading events") {
Sorted::Transient(failure) => ProjectionError::Transient(failure),
Sorted::Permanent(failure) => ProjectionError::Permanent(failure),
}
}
async fn roll_back<W: Writer>(writer: &mut W, cause: ProjectionError) -> ProjectionError {
match writer.rollback().await {
Ok(()) => cause,
Err(rollback) => ProjectionError::RolledBack {
cause: Box::new(cause),
rollback: Box::new(rollback),
},
}
}
pub(crate) async fn drive<W: Writer>(
writer: &mut W,
events: impl Stream<Item = Result<eventsourcingdb::Event, ClientError>>,
verify: impl Fn(&eventsourcingdb::Event) -> Result<(), Permanent>,
batch_size: usize,
mut committed: impl FnMut(&str),
) -> (Option<String>, Result<(), ProjectionError>) {
let mut events = std::pin::pin!(events);
let mut last_event_id = None;
let mut in_batch = 0;
let mut is_open = false;
while let Some(event) = events.next().await {
let checked = event
.map_err(read_failed)
.and_then(|event| verify(&event).map(|()| event).map_err(Into::into));
let event = match checked {
Ok(event) => event,
Err(failure) if is_open => {
return (last_event_id, Err(roll_back(writer, failure).await));
}
Err(failure) => return (last_event_id, Err(failure)),
};
if !is_open {
if let Err(failure) = writer.begin().await {
return (last_event_id, Err(failure));
}
is_open = true;
}
if let Err(failure) = writer.apply(&event).await {
return (last_event_id, Err(roll_back(writer, failure).await));
}
last_event_id = Some(event.id().to_owned());
in_batch += 1;
if in_batch < batch_size {
continue;
}
let id = event.id();
if let Err(failure) = writer.commit(id).await {
return (last_event_id, Err(failure));
}
committed(id);
is_open = false;
in_batch = 0;
}
if is_open && let Some(id) = &last_event_id {
if let Err(failure) = writer.commit(id).await {
return (last_event_id, Err(failure));
}
committed(id);
}
(last_event_id, Ok(()))
}
pub(crate) fn bound_after(checkpoint: Option<&str>) -> Option<Bound<'_>> {
checkpoint.map(|id| Bound {
bound_type: BoundType::Exclusive,
id,
})
}
impl Store {
pub(crate) async fn catch_up<W: Writer>(
&self,
subjects: &Subjects,
writer: &mut W,
batch_size: usize,
committed: impl FnMut(&str),
) -> (Option<String>, Result<(), ProjectionError>) {
let checkpoint = match writer.checkpoint().await {
Ok(checkpoint) => checkpoint,
Err(failure) => return (None, Err(failure)),
};
let options = ReadEventsOptions {
lower_bound: bound_after(checkpoint.as_deref()),
recursive: subjects.is_tree(),
..ReadEventsOptions::default()
};
let events = match self
.client
.read_events(subjects.subject(), Some(options))
.await
{
Ok(events) => events,
Err(error) => return (checkpoint.clone(), Err(read_failed(error))),
};
let (last_event_id, result) = drive(
writer,
events,
|event| self.verify(event),
batch_size,
committed,
)
.await;
(last_event_id.or(checkpoint), result)
}
async fn catch_up_once<W: Writer>(
&self,
subjects: &Subjects,
mut writer: W,
) -> Result<(), ProjectionError> {
let batch_size = writer.batch_size();
AssertUnwindSafe(self.catch_up(subjects, &mut writer, batch_size, |_| {}))
.catch_unwind()
.await
.map_or_else(
|payload| Err(ProjectionError::panicked(&*payload)),
|(_, result)| result,
)
}
pub async fn catch_up_projection<P: Projection>(
&self,
subjects: &Subjects,
projection: &P,
) -> Result<(), ProjectionError> {
self.catch_up_once(subjects, RebuildWriter::new(projection))
.await
}
pub async fn catch_up_resumable_projection<P: ResumableProjection>(
&self,
subjects: &Subjects,
projection: &P,
) -> Result<(), ProjectionError> {
self.catch_up_once(subjects, ResumableWriter::new(projection))
.await
}
pub async fn catch_up_transactional_projection<P: TransactionalProjection>(
&self,
subjects: &Subjects,
projection: &P,
) -> Result<(), ProjectionError> {
self.catch_up_once(subjects, TransactionalWriter::new(projection))
.await
}
}