architecturekit 0.10.0

Building blocks for DDD-based applications with CQRS and event sourcing, on top of the EventSourcingDB by the native web.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
//! Drives projections, as ArchitectureKit for Go does: it catches up from the
//! checkpoint of a projection, in batches, and verifies every event before the
//! projection applies it.

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;

/// What driving needs of a projection, which hides the three kinds of
/// projections behind one shape.
pub(crate) trait Writer: Send {
    /// Returns where to go on from.
    fn checkpoint(
        &mut self,
    ) -> impl Future<Output = Result<Option<String>, ProjectionError>> + Send;

    /// Begins a batch.
    fn begin(&mut self) -> impl Future<Output = Result<(), ProjectionError>> + Send;

    /// Applies a stored event, which has been verified.
    fn apply(
        &mut self,
        event: &eventsourcingdb::Event,
    ) -> impl Future<Output = Result<(), ProjectionError>> + Send;

    /// Ends a batch, whose last event has the given ID.
    fn commit(
        &mut self,
        last_event_id: &str,
    ) -> impl Future<Output = Result<(), ProjectionError>> + Send;

    /// Undoes a batch that has begun.
    fn rollback(&mut self) -> impl Future<Output = Result<(), ProjectionError>> + Send;

    /// Returns how many events make a batch while catching up, at least one.
    fn batch_size(&self) -> usize;
}

/// Hands a stored event to a projection, as the envelopes it gets.
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(())
}

/// Keeps no checkpoint of its own, so every start reads from the first event.
/// Within a run, it remembers the last event it applied, so that catching up
/// again after a lost stream does not apply any event twice.
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(()))
    }

    // There is nothing to undo, since nothing was begun.
    fn rollback(&mut self) -> impl Future<Output = Result<(), ProjectionError>> + Send {
        future::ready(Ok(()))
    }

    fn batch_size(&self) -> usize {
        1
    }
}

/// Saves the checkpoint after the data, not with it. Within a run, it
/// remembers the last event it applied, as [`RebuildWriter`] does, so that
/// catching up again after a lost stream goes on from there, rather than from
/// a checkpoint that a batch cut short did not get to save.
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)
    }

    // There is nothing to undo: the data is written already, and only the
    // checkpoint is missing. That is what makes this kind at-least-once
    // across a crash.
    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)
    }
}

/// Keeps data and checkpoint in step, within the transactions of the
/// projection.
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(())
    }

    // A panic is turned into an error right where it happens, so that driving
    // rolls the transaction back, as for an error, instead of leaving it open.
    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)
    }

    // A panic is turned into an error as well, so that driving reports it
    // alongside the failure that made it roll back, never instead of it.
    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)
    }
}

/// Turns a failure of reading into one of a projection.
fn read_failed(error: ClientError) -> ProjectionError {
    match read_failure(error, "reading events") {
        Sorted::Transient(failure) => ProjectionError::Transient(failure),
        Sorted::Permanent(failure) => ProjectionError::Permanent(failure),
    }
}

/// Rolls back a batch after the given failure, and reports a failing rollback
/// alongside the failure, never instead of it.
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),
        },
    }
}

/// Applies the events in batches of the given size, and returns the ID of the
/// last one it applied, together with how it ended. Every event is verified
/// before it is applied, and one that fails ends the batch like an event the
/// projection refuses. Every committed batch is reported with the ID of its
/// last event.
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(()))
}

/// Turns a checkpoint into a lower bound that leaves out the event it names,
/// since that one has been applied already.
pub(crate) fn bound_after(checkpoint: Option<&str>) -> Option<Bound<'_>> {
    checkpoint.map(|id| Bound {
        bound_type: BoundType::Exclusive,
        id,
    })
}

impl Store {
    /// Reads the events stored so far from the checkpoint of the writer on,
    /// and applies them, in batches of the given size. It returns where it
    /// stopped, which is the checkpoint if there was nothing to apply.
    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)
    }

    /// Catches up once, and turns a panic on the way, such as one in the
    /// projection, into the error a run ends with.
    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,
            )
    }

    /// Applies the events of the subjects that are stored so far to a
    /// projection, from the first one on, and returns. Use it to build a read
    /// model once, for a batch job or in a test, instead of following the
    /// events as they are written. Dropping the future stops it, so a read
    /// model that is built only in part never looks complete.
    ///
    /// # Errors
    ///
    /// Fails with the first failure, of reading, of verifying an event, or of
    /// the projection, including a panic of the projection, which is
    /// [`ProjectionError::Panicked`].
    pub async fn catch_up_projection<P: Projection>(
        &self,
        subjects: &Subjects,
        projection: &P,
    ) -> Result<(), ProjectionError> {
        self.catch_up_once(subjects, RebuildWriter::new(projection))
            .await
    }

    /// Applies the events of the subjects that are stored so far to a
    /// resumable projection, from its checkpoint on, saving the checkpoint
    /// after every batch, and returns. See
    /// [`catch_up_projection`](Store::catch_up_projection).
    ///
    /// # Errors
    ///
    /// Fails as [`catch_up_projection`](Store::catch_up_projection) does, and
    /// if the checkpoint can not be read or saved.
    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
    }

    /// Applies the events of the subjects that are stored so far to a
    /// transactional projection, from its checkpoint on, one transaction per
    /// batch, and returns. See
    /// [`catch_up_projection`](Store::catch_up_projection).
    ///
    /// # Errors
    ///
    /// Fails as [`catch_up_projection`](Store::catch_up_projection) does, and
    /// if the checkpoint can not be read, or a transaction can not be begun or
    /// committed. A transaction that fails is rolled back, and a failure of
    /// the rollback is reported alongside the failure that caused it.
    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
    }
}