use std::{
fmt,
future::{self, Future},
ops::Deref,
panic::AssertUnwindSafe,
sync::Arc,
time::{Duration, Instant},
};
use eventsourcingdb::{
Client,
error::ClientError,
request_options::{Bound, BoundType, ObserveEventsOptions, ReadEventsOptions},
};
use futures_util::{FutureExt, Stream, StreamExt};
use tokio_util::sync::CancellationToken;
use super::{
Store, Verification,
failure::{Sorted, read_failure},
verify,
};
use crate::{
Categorized, Category, Failure, Permanent, Projected, Projection, ProjectionError,
ProjectionOptions, ProjectionRun, Reconnect, ResumableProjection, Subjects, Transaction,
TransactionalProjection, Transient,
};
#[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<R> {
projection: R,
last_applied: Option<String>,
}
impl<R> RebuildWriter<R> {
pub(crate) fn new(projection: R) -> Self {
Self {
projection,
last_applied: None,
}
}
}
impl<R> Writer for RebuildWriter<R>
where
R: Deref + Send + Sync,
R::Target: Projection,
{
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,
<R::Target as Projection>::upcasters(),
|envelope| async move {
projection
.apply(envelope)
.await
.map_err(ProjectionError::of)
},
)
.await?;
projection.seen(event.id());
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<R> {
projection: R,
last_applied: Option<String>,
}
impl<R> ResumableWriter<R> {
pub(crate) fn new(projection: R) -> Self {
Self {
projection,
last_applied: None,
}
}
}
impl<R> Writer for ResumableWriter<R>
where
R: Deref + Send + Sync,
R::Target: ResumableProjection,
{
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,
<R::Target as Projection>::upcasters(),
|envelope| async move {
projection
.apply(envelope)
.await
.map_err(ProjectionError::of)
},
)
.await?;
projection.seen(event.id());
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)
}
}
type TransactionalEvent<R> = <<R as Deref>::Target as TransactionalProjection>::Event;
pub(crate) struct TransactionalWriter<R>
where
R: Deref,
R::Target: TransactionalProjection,
{
projection: R,
transaction: Option<<R::Target as TransactionalProjection>::Transaction>,
}
impl<R> TransactionalWriter<R>
where
R: Deref,
R::Target: TransactionalProjection,
{
pub(crate) fn new(projection: R) -> Self {
Self {
projection,
transaction: None,
}
}
}
impl<R> Writer for TransactionalWriter<R>
where
R: Deref + Send + Sync,
R::Target: TransactionalProjection,
{
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 TransactionalEvent::<R>::project(
event,
<R::Target as TransactionalProjection>::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,
token: &CancellationToken,
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;
loop {
let next = tokio::select! {
biased;
() = token.cancelled() => None,
next = events.next() => Some(next),
};
let Some(next) = next else {
if is_open && let Err(failure) = writer.rollback().await {
return (last_event_id, Err(failure));
}
return (last_event_id, Ok(()));
};
let Some(event) = next else {
break;
};
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,
})
}
pub(crate) type Observer = Arc<dyn Fn(&Reconnect) + Send + Sync>;
#[derive(Clone)]
pub(crate) struct Reconnecting {
pub(crate) initial_delay: Duration,
pub(crate) max_delay: Duration,
pub(crate) observer: Option<Observer>,
}
impl Default for Reconnecting {
fn default() -> Self {
Self {
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
observer: None,
}
}
}
impl fmt::Debug for Reconnecting {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Reconnecting")
.field("initial_delay", &self.initial_delay)
.field("max_delay", &self.max_delay)
.field("has_observer", &self.observer.is_some())
.finish()
}
}
#[derive(Clone)]
pub(crate) struct Reader {
client: Arc<Client>,
verification: Verification,
}
impl Reader {
pub(crate) async fn catch_up<W: Writer>(
&self,
subjects: &Subjects,
writer: &mut W,
batch_size: usize,
token: &CancellationToken,
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 requested = tokio::select! {
biased;
() = token.cancelled() => None,
requested = self.client.read_events(subjects.subject(), Some(options)) => Some(requested),
};
let events = match requested {
None => return (checkpoint.clone(), Ok(())),
Some(Ok(events)) => events,
Some(Err(error)) => return (checkpoint.clone(), Err(read_failed(error))),
};
let (last_event_id, result) = drive(
writer,
events,
|event| verify(&self.verification, event),
batch_size,
token,
committed,
)
.await;
(last_event_id.or_else(|| checkpoint.clone()), result)
}
async fn follow<W: Writer>(
&self,
subjects: &Subjects,
writer: &mut W,
batch_size: usize,
token: &CancellationToken,
run: &ProjectionRun,
) -> (Duration, Result<(), ProjectionError>) {
let (last_event_id, caught_up) = self
.catch_up(subjects, writer, batch_size, token, |id| run.committed(id))
.await;
if let Err(failure) = caught_up {
return (Duration::ZERO, Err(failure));
}
if token.is_cancelled() {
return (Duration::ZERO, Ok(()));
}
let caught_up_at = Instant::now();
run.caught_up_now();
let options = ObserveEventsOptions {
lower_bound: bound_after(last_event_id.as_deref()),
recursive: subjects.is_tree(),
..ObserveEventsOptions::default()
};
let requested = tokio::select! {
biased;
() = token.cancelled() => None,
requested = self.client.observe_events(subjects.subject(), Some(options)) => Some(requested),
};
let events = match requested {
None => return (caught_up_at.elapsed(), Ok(())),
Some(Ok(events)) => events,
Some(Err(error)) => return (caught_up_at.elapsed(), Err(read_failed(error))),
};
let (_, followed) = drive(
writer,
events,
|event| verify(&self.verification, event),
1,
token,
|id| run.committed(id),
)
.await;
let ended = match followed {
Err(failure) => Err(failure),
Ok(()) if token.is_cancelled() => Ok(()),
Ok(()) => Err(ProjectionError::Transient(Transient::Other(Failure::new(
"reading events: the stream of events ended",
None,
)))),
};
(caught_up_at.elapsed(), ended)
}
async fn run<W: Writer>(
&self,
subjects: &Subjects,
writer: &mut W,
reconnecting: &Reconnecting,
token: &CancellationToken,
run: &ProjectionRun,
) -> Option<ProjectionError> {
let batch_size = writer.batch_size();
let mut delay = reconnecting.initial_delay;
let mut attempt = 0;
loop {
let checkpoint_before = writer.checkpoint().await.ok().flatten();
let (live_for, followed) = self.follow(subjects, writer, batch_size, token, run).await;
if token.is_cancelled() {
return None;
}
let failure = followed.err()?;
if failure.has_panicked() || failure.category() != Some(Category::Transient) {
return Some(failure);
}
let failure = Arc::new(failure);
run.disrupted(Arc::clone(&failure));
let checkpoint_after = writer.checkpoint().await.ok().flatten();
if checkpoint_after != checkpoint_before || live_for > reconnecting.initial_delay {
delay = reconnecting.initial_delay;
attempt = 0;
}
attempt += 1;
if let Some(observe) = &reconnecting.observer {
let reconnect = Reconnect {
projection: run.name().map(ToOwned::to_owned),
subject: subjects.subject().to_owned(),
error: Arc::clone(&failure),
delay,
attempt,
};
observe(&reconnect);
}
tokio::select! {
() = token.cancelled() => return None,
() = tokio::time::sleep(delay) => {}
}
delay = (delay * 2).min(reconnecting.max_delay);
}
}
}
impl Store {
pub(crate) fn reader(&self) -> Reader {
Reader {
client: Arc::clone(&self.client),
verification: self.verification.clone(),
}
}
async fn catch_up_once<W: Writer>(
&self,
subjects: &Subjects,
mut writer: W,
) -> Result<(), ProjectionError> {
let batch_size = writer.batch_size();
let reader = self.reader();
let token = CancellationToken::new();
AssertUnwindSafe(reader.catch_up(subjects, &mut writer, batch_size, &token, |_| {}))
.catch_unwind()
.await
.map_or_else(
|payload| Err(ProjectionError::panicked(&*payload)),
|(_, result)| result,
)
}
fn start<W: Writer + 'static>(
&self,
subjects: Subjects,
mut writer: W,
token: CancellationToken,
options: ProjectionOptions,
) -> ProjectionRun {
let run = ProjectionRun::new(options);
let reader = self.reader();
let reconnecting = self.reconnecting.clone();
let progress = run.clone();
drop(tokio::spawn(async move {
let failure = AssertUnwindSafe(reader.run(
&subjects,
&mut writer,
&reconnecting,
&token,
&progress,
))
.catch_unwind()
.await
.unwrap_or_else(|payload| Some(ProjectionError::panicked(&*payload)));
progress.stop(failure);
}));
run
}
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
}
pub fn start_projection<P: Projection>(
&self,
subjects: Subjects,
projection: Arc<P>,
token: CancellationToken,
options: ProjectionOptions,
) -> ProjectionRun {
self.start(subjects, RebuildWriter::new(projection), token, options)
}
pub fn start_resumable_projection<P: ResumableProjection>(
&self,
subjects: Subjects,
projection: Arc<P>,
token: CancellationToken,
options: ProjectionOptions,
) -> ProjectionRun {
self.start(subjects, ResumableWriter::new(projection), token, options)
}
pub fn start_transactional_projection<P: TransactionalProjection>(
&self,
subjects: Subjects,
projection: Arc<P>,
token: CancellationToken,
options: ProjectionOptions,
) -> ProjectionRun {
self.start(
subjects,
TransactionalWriter::new(projection),
token,
options,
)
}
}