Skip to content

Commit bfa7119

Browse files
authored
feat: Projection trait is now async and might fail (#97)
* feat: Projection trait is now async and might fail * fix: solve some clippy linter warnings
1 parent b1ace11 commit bfa7119

6 files changed

Lines changed: 83 additions & 100 deletions

File tree

eventually-core/src/projection.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
//! [`Projection`]: trait.Projection.html
88
//! [`Aggregate`]: ../aggregate/trait.Aggregate.html
99
10+
use futures::future::BoxFuture;
11+
1012
use crate::store::Persisted;
1113

1214
/// A `Projection` is an optimized read model (or materialized view)
@@ -18,7 +20,7 @@ use crate::store::Persisted;
1820
///
1921
/// [`Aggregate`]: ../aggregate/trait.Aggregate.html
2022
/// [`EventStore`]: ../store/trait.EventStore.html
21-
pub trait Projection: Default {
23+
pub trait Projection {
2224
/// Type of the Source id, typically an [`AggregateId`].
2325
///
2426
/// [`AggregateId`]: ../aggregate/type.AggregateId.html
@@ -29,7 +31,13 @@ pub trait Projection: Default {
2931
/// [`Aggregate::Event`]: ../aggregate/trait.Aggregate.html#associatedtype.Event
3032
type Event;
3133

32-
/// Updates the next value of the `Projection` using the provided
33-
/// event value.
34-
fn project(self, event: Persisted<Self::SourceId, Self::Event>) -> Self;
34+
/// Type of the possible error that might occur when projecting
35+
/// the next state.
36+
type Error;
37+
38+
/// Updates the next value of the `Projection` using the provided event value.
39+
fn project<'a>(
40+
&'a mut self,
41+
event: Persisted<Self::SourceId, Self::Event>,
42+
) -> BoxFuture<'a, Result<(), Self::Error>>;
3543
}

eventually-test/src/api.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ pub(crate) async fn history(req: Request<AppState>) -> Result<Response, Error> {
6969
.await
7070
.map_err(Error::from)?
7171
.try_filter(|event| {
72-
futures::future::ready(match from {
72+
ready(match from {
7373
None => true,
7474
Some(from) => event.happened_at() >= &from,
7575
})

eventually-test/src/lib.rs

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ use eventually::aggregate::Optional;
99
use eventually::inmemory::{EventStoreBuilder, ProjectorBuilder};
1010
use eventually::{AggregateRootBuilder, Repository};
1111

12-
use futures::stream::StreamExt;
13-
1412
use tokio::sync::RwLock;
1513

1614
use crate::config::Config;
@@ -38,18 +36,13 @@ pub async fn run(config: Config) -> anyhow::Result<()> {
3836
// Put the store behind an Arc to allow for clone-ness of a single instance.
3937
let store = Arc::new(store);
4038

39+
// Create a new in-memory projection to keep the total orders computed by
40+
// the application.
41+
let total_orders_projection = Arc::new(RwLock::new(TotalOrdersProjection::default()));
42+
4143
// Create a new Projector for the desired projection.
4244
let mut total_orders_projector =
43-
ProjectorBuilder::new(store.clone(), store.clone()).build::<TotalOrdersProjection>();
44-
45-
// Get a watch channel from the Projector: updates to the projector values
46-
// will be sent here.
47-
let mut total_orders_projector_rx = total_orders_projector.watch();
48-
49-
// Keep the projection value in memory.
50-
// We can use it to access it from the context of an endpoint and serialize the read model.
51-
let total_orders_projection = Arc::new(RwLock::new(TotalOrdersProjection::default()));
52-
let total_orders_projection_state = total_orders_projection.clone();
45+
ProjectorBuilder::new(store.clone(), store.clone()).build(total_orders_projection.clone());
5346

5447
// Spawn a dedicated coroutine to run the projector.
5548
//
@@ -58,19 +51,6 @@ pub async fn run(config: Config) -> anyhow::Result<()> {
5851
// and it will progressively update the projection as events arrive.
5952
tokio::spawn(async move { total_orders_projector.run().await.expect("should not fail") });
6053

61-
// Spawn a dedicated coroutine to listen to changes to the projection.
62-
//
63-
// In this case we're logging the latest version, but in more advanced
64-
// scenario you might want to do something more with it.
65-
//
66-
// In some cases you might not need to watch the projection changes.
67-
tokio::spawn(async move {
68-
while let Some(total_orders) = total_orders_projector_rx.next().await {
69-
log::info!("Total orders: {:?}", total_orders);
70-
*total_orders_projection_state.write().await = total_orders;
71-
}
72-
});
73-
7454
// Set up the HTTP router.
7555
let mut app = tide::new();
7656

eventually-test/src/order.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
22

33
use chrono::{DateTime, Utc};
44

5-
use futures::{future, future::BoxFuture};
5+
use futures::future;
6+
use futures::future::{BoxFuture, FutureExt};
67

78
use serde::{Deserialize, Serialize};
89

@@ -20,16 +21,20 @@ pub struct TotalOrdersProjection {
2021
impl Projection for TotalOrdersProjection {
2122
type SourceId = String;
2223
type Event = OrderEvent;
24+
type Error = std::convert::Infallible;
2325

24-
fn project(mut self, event: Persisted<Self::SourceId, Self::Event>) -> Self {
26+
fn project<'a>(
27+
&'a mut self,
28+
event: Persisted<Self::SourceId, Self::Event>,
29+
) -> BoxFuture<'a, Result<(), Self::Error>> {
2530
match event.take() {
2631
OrderEvent::Created { .. } => self.created += 1,
2732
OrderEvent::Completed { .. } => self.completed += 1,
2833
OrderEvent::Cancelled { .. } => self.cancelled += 1,
2934
_ => (),
3035
};
3136

32-
self
37+
future::ok(()).boxed()
3338
}
3439
}
3540

eventually-util/src/inmemory/projector.rs

Lines changed: 29 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
use std::error::Error as StdError;
2-
use std::fmt::Debug;
32
use std::sync::atomic::{AtomicU32, Ordering};
43
use std::sync::Arc;
54

65
use eventually_core::projection::Projection;
76
use eventually_core::store::{EventStore, Select};
87
use eventually_core::subscription::EventSubscriber;
98

10-
use futures::stream::{Stream, StreamExt, TryStreamExt};
9+
use futures::stream::{StreamExt, TryStreamExt};
1110

12-
use tokio::sync::watch::{channel, Receiver, Sender};
11+
use tokio::sync::RwLock;
1312

1413
/// Reusable builder for multiple [`Projector`] instances.
1514
///
@@ -29,21 +28,21 @@ impl<Store, Subscriber> ProjectorBuilder<Store, Subscriber> {
2928
Self { store, subscriber }
3029
}
3130

32-
/// Builds a new [`Projector`] for the [`Projection`]
33-
/// specified in the function type.
31+
/// Builds a new [`Projector`] for the [`Projection`] specified in the function type.
3432
///
3533
/// [`Projector`]: struct.Projector.html
3634
/// [`Projection`]: ../../../eventually-core/projection/trait.Projection.html
37-
pub fn build<P>(&self) -> Projector<P, Store, Subscriber>
35+
pub fn build<P>(&self, projection: Arc<RwLock<P>>) -> Projector<P, Store, Subscriber>
3836
where
3937
// NOTE: these bounds are required for Projector::run.
40-
P: Projection + Debug + Clone,
38+
P: Projection,
4139
Store: EventStore<SourceId = P::SourceId, Event = P::Event>,
4240
Subscriber: EventSubscriber<SourceId = P::SourceId, Event = P::Event>,
41+
<P as Projection>::Error: StdError + Send + Sync + 'static,
4342
<Store as EventStore>::Error: StdError + Send + Sync + 'static,
4443
<Subscriber as EventSubscriber>::Error: StdError + Send + Sync + 'static,
4544
{
46-
Projector::new(self.store.clone(), self.subscriber.clone())
45+
Projector::new(projection, self.store.clone(), self.subscriber.clone())
4746
}
4847
}
4948

@@ -68,44 +67,31 @@ pub struct Projector<P, Store, Subscriber>
6867
where
6968
P: Projection,
7069
{
71-
tx: Sender<P>,
72-
rx: Receiver<P>, // Keep the receiver to be able to clone it in watch().
70+
projection: Arc<RwLock<P>>,
7371
store: Arc<Store>,
7472
subscriber: Arc<Subscriber>,
75-
state: P,
7673
last_sequence_number: AtomicU32,
77-
projection: std::marker::PhantomData<P>,
7874
}
7975

8076
impl<P, Store, Subscriber> Projector<P, Store, Subscriber>
8177
where
82-
P: Projection + Debug + Clone,
78+
P: Projection,
8379
Store: EventStore<SourceId = P::SourceId, Event = P::Event>,
8480
Subscriber: EventSubscriber<SourceId = P::SourceId, Event = P::Event>,
8581
// NOTE: these bounds are needed for anyhow::Error conversion.
82+
<P as Projection>::Error: StdError + Send + Sync + 'static,
8683
<Store as EventStore>::Error: StdError + Send + Sync + 'static,
8784
<Subscriber as EventSubscriber>::Error: StdError + Send + Sync + 'static,
8885
{
89-
fn new(store: Arc<Store>, subscriber: Arc<Subscriber>) -> Self {
90-
let state: P = Default::default();
91-
let (tx, rx) = channel(state.clone());
92-
86+
fn new(projection: Arc<RwLock<P>>, store: Arc<Store>, subscriber: Arc<Subscriber>) -> Self {
9387
Self {
94-
tx,
95-
rx,
9688
store,
9789
subscriber,
98-
state,
90+
projection,
9991
last_sequence_number: Default::default(),
100-
projection: std::marker::PhantomData,
10192
}
10293
}
10394

104-
/// Provides a `Stream` that receives the latest copy of the `Projection` state.
105-
pub fn watch(&self) -> impl Stream<Item = P> {
106-
self.rx.clone()
107-
}
108-
10995
/// Starts the update of the `Projection` by processing all the events
11096
/// coming from the [`EventStore`].
11197
///
@@ -124,32 +110,30 @@ where
124110
let subscription = self.subscriber.subscribe_all().await?;
125111
let one_off_stream = self.store.stream_all(Select::All).await?;
126112

127-
let mut stream = one_off_stream
113+
let stream = one_off_stream
128114
.map_err(anyhow::Error::from)
129115
.chain(subscription.map_err(anyhow::Error::from));
130116

131-
while let Some(event) = stream.next().await {
132-
let event = event?;
133-
let expected_sequence_number = self.last_sequence_number.load(Ordering::SeqCst);
134-
let event_sequence_number = event.sequence_number();
117+
stream
118+
.try_for_each(|event| async {
119+
let expected_sequence_number = self.last_sequence_number.load(Ordering::SeqCst);
120+
let event_sequence_number = event.sequence_number();
135121

136-
if event_sequence_number < expected_sequence_number {
137-
continue; // Duplicated event detected, let's skip it.
138-
}
122+
if event_sequence_number < expected_sequence_number {
123+
return Ok(()); // Duplicated event detected, let's skip it.
124+
}
139125

140-
self.state = P::project(self.state.clone(), event);
126+
self.projection.write().await.project(event).await?;
141127

142-
self.last_sequence_number.compare_and_swap(
143-
expected_sequence_number,
144-
event_sequence_number,
145-
Ordering::SeqCst,
146-
);
128+
self.last_sequence_number.compare_and_swap(
129+
expected_sequence_number,
130+
event_sequence_number,
131+
Ordering::SeqCst,
132+
);
147133

148-
// Notify watchers of the latest projection state.
149-
self.tx.broadcast(self.state.clone()).expect(
150-
"since this struct holds the original receiver, failures should not happen",
151-
);
152-
}
134+
Ok(())
135+
})
136+
.await?;
153137

154138
Ok(())
155139
}

eventually-util/src/inmemory/store.rs

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,30 @@ const SUBSCRIBE_CHANNEL_DEFAULT_CAP: usize = 128;
2323
/// Error returned by the [`EventStore::append`] when a conflict has been detected.
2424
///
2525
/// [`EventStore::append`]: trait.EventStore.html#method.append
26-
#[derive(Debug, thiserror::Error, PartialEq)]
27-
pub enum Error {
28-
/// Version conflict registered.
29-
#[error(
30-
"inmemory::EventStore: conflicting versions, expected {expected}, got instead {actual}"
31-
)]
32-
Conflict {
33-
/// The last version value found the Store.
34-
expected: u32,
35-
/// The actual version passed by the caller to the Store.
36-
actual: u32,
37-
},
38-
39-
#[error("inmemory::EventStore: failed to read event from subscription: {0}")]
40-
ReceiveEvent(#[source] RecvError),
26+
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
27+
#[error("conflicting versions, expected {expected}, got instead {actual}")]
28+
pub struct ConflictError {
29+
/// The last version value found the Store.
30+
pub expected: u32,
31+
/// The actual version passed by the caller to the Store.
32+
pub actual: u32,
4133
}
4234

43-
impl AppendError for Error {
35+
impl AppendError for ConflictError {
4436
fn is_conflict_error(&self) -> bool {
4537
true
4638
}
4739
}
4840

41+
/// Error returned by the [`EventStore::append`] when a conflict has been detected.
42+
///
43+
/// [`EventStore::append`]: trait.EventStore.html#method.append
44+
#[derive(Debug, thiserror::Error)]
45+
pub enum SubscriberError {
46+
#[error("failed to read event from subscription watch channel: {0}")]
47+
ReceiveEvent(#[source] RecvError),
48+
}
49+
4950
/// Builder for [`EventStore`] instances.
5051
///
5152
/// [`EventStore`]: struct.EventStore.html
@@ -119,7 +120,7 @@ where
119120
{
120121
type SourceId = Id;
121122
type Event = Event;
122-
type Error = Error;
123+
type Error = SubscriberError;
123124

124125
fn subscribe_all(
125126
&self,
@@ -130,7 +131,12 @@ where
130131
// with the definition of the EventStream.
131132
let rx = self.tx.subscribe();
132133

133-
Box::pin(async move { Ok(rx.into_stream().map_err(Error::ReceiveEvent).boxed()) })
134+
Box::pin(async move {
135+
Ok(rx
136+
.into_stream()
137+
.map_err(SubscriberError::ReceiveEvent)
138+
.boxed())
139+
})
134140
}
135141
}
136142

@@ -141,7 +147,7 @@ where
141147
{
142148
type SourceId = Id;
143149
type Event = Event;
144-
type Error = Error;
150+
type Error = ConflictError;
145151

146152
fn append(
147153
&mut self,
@@ -160,7 +166,7 @@ where
160166

161167
if let Expected::Exact(actual) = version {
162168
if expected != actual {
163-
return Err(Error::Conflict { expected, actual });
169+
return Err(ConflictError { expected, actual });
164170
}
165171
}
166172

@@ -274,7 +280,7 @@ where
274280

275281
#[cfg(test)]
276282
mod tests {
277-
use super::{Error, EventStore as InMemoryStore};
283+
use super::{ConflictError, EventStore as InMemoryStore};
278284

279285
use std::cell::RefCell;
280286
use std::sync::Arc;
@@ -480,7 +486,7 @@ mod tests {
480486
.await;
481487

482488
assert_eq!(
483-
Err(Error::Conflict {
489+
Err(ConflictError {
484490
expected: last_version,
485491
actual: poisoned_last_version
486492
}),

0 commit comments

Comments
 (0)