1use ddd_toolkit::{
15 AggregateRoot as _, AppendError, AppendResult, AsyncDomainEventPublisher,
16 AsyncIdempotentProjection, AsyncProjectionCheckpointStore, AsyncTransactionalOutbox,
17 AsyncTransactionalOutboxLifecycleContext, CurrentRevision, ExpectedRevision, NoStream,
18 OutboxClaim, OutboxMessageId, ProjectionApplication, ProjectionCheckpoint, RevisionConflict,
19 StreamRevision,
20};
21use std::{
22 collections::{HashSet, VecDeque},
23 future::Future,
24 pin::{Pin, pin},
25 task::{Context, Poll, Waker},
26};
27
28type PortFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, &'static str>> + Send + 'a>>;
29
30fn ready<F: Future>(future: F) -> F::Output {
33 let mut future = pin!(future);
34 match future
35 .as_mut()
36 .poll(&mut Context::from_waker(Waker::noop()))
37 {
38 Poll::Ready(value) => value,
39 Poll::Pending => panic!("demo adapter unexpectedly suspended; use a real executor for I/O"),
40 }
41}
42#[derive(Debug, Clone, ddd_toolkit::DomainEvent)]
43struct Credited(u32);
44#[derive(Clone, ddd_toolkit::Entity, ddd_toolkit::AggregateRoot)]
45struct Account {
46 #[entity(id)]
47 id: u64,
48 balance: u32,
49 #[aggregate(events)]
50 events: Vec<Credited>,
51}
52
53#[derive(Default)]
56struct Outbox {
57 saved: Option<Account>,
58 revision: Option<StreamRevision>,
59 queue: VecDeque<(OutboxMessageId, Credited)>,
60 fail_ack: bool,
61}
62impl AsyncTransactionalOutbox for Outbox {
63 type Aggregate = Account;
64 type Error = &'static str;
65 fn save_and_enqueue<'a>(
66 &'a mut self,
67 account: &'a Account,
68 expected: ExpectedRevision,
69 events: Vec<Credited>,
70 ) -> Pin<Box<dyn Future<Output = Result<AppendResult, AppendError<Self::Error>>> + Send + 'a>>
71 {
72 Box::pin(async move {
73 if events.is_empty() {
74 return Err(AppendError::EmptyBatch);
75 }
76 if self
77 .saved
78 .as_ref()
79 .is_some_and(|saved| saved.id != account.id)
80 {
81 return Err(AppendError::Adapter("demo stores only one account"));
82 }
83 let matches = match (expected, self.revision) {
84 (ExpectedRevision::NoStream(_), None) => true,
85 (ExpectedRevision::Revision(want), Some(have)) => want == have,
86 _ => false,
87 };
88 if !matches {
89 return Err(AppendError::Conflict(RevisionConflict {
90 expected,
91 current: self.revision.map_or(
92 CurrentRevision::NoStream(NoStream),
93 CurrentRevision::Revision,
94 ),
95 }));
96 }
97 let first = match self.revision {
98 None => 0,
99 Some(current) => current
100 .value()
101 .checked_add(1)
102 .ok_or(AppendError::RevisionExhausted { current })?,
103 };
104 let last = first.checked_add(events.len() as u64 - 1).ok_or(
105 AppendError::RevisionExhausted {
106 current: StreamRevision::new(first),
107 },
108 )?;
109 let mut snapshot = account.clone();
110 snapshot.pull_events();
111 self.saved = Some(snapshot);
112 self.revision = Some(StreamRevision::new(last));
113 for (offset, event) in events.into_iter().enumerate() {
114 self.queue.push_back((
115 OutboxMessageId::new(format!("{}:{}", account.id, first + offset as u64)),
116 event,
117 ));
118 }
119 Ok(AppendResult {
120 resulting_revision: StreamRevision::new(last),
121 })
122 })
123 }
124 fn claim<'a>(&'a mut self) -> PortFuture<'a, Option<OutboxClaim<Credited>>> {
125 Box::pin(async move {
126 Ok(self
127 .queue
128 .front()
129 .map(|(id, event)| OutboxClaim::new(id.clone(), event.clone())))
130 })
131 }
132 fn acknowledge<'a>(&'a mut self, id: &'a OutboxMessageId) -> PortFuture<'a, ()> {
133 Box::pin(async move {
134 if self.fail_ack {
135 return Err("acknowledgement lost");
136 }
137 if self.queue.front().is_none_or(|(head, _)| head != id) {
138 return Err("unknown claim");
139 }
140 self.queue.pop_front();
141 Ok(())
142 })
143 }
144}
145
146#[derive(Default)]
147struct Broker {
148 fail: bool,
149 accepted: Vec<Credited>,
150}
151impl AsyncDomainEventPublisher for Broker {
152 type Event = Credited;
153 type Error = &'static str;
154 fn publish<'a>(&'a mut self, event: &'a Credited) -> PortFuture<'a, ()> {
155 Box::pin(async move {
156 if self.fail {
157 return Err("broker down");
158 }
159 self.accepted.push(event.clone());
160 Ok(())
161 })
162 }
163}
164#[derive(Default)]
165struct ReadModel {
166 balance: u32,
167 applied: HashSet<String>,
168}
169impl AsyncIdempotentProjection for ReadModel {
170 type Event = Credited;
171 type Cursor = String;
172 type Error = &'static str;
173 fn apply_once<'a>(
174 &'a mut self,
175 event: &'a Credited,
176 checkpoint: &'a ProjectionCheckpoint<String>,
177 ) -> PortFuture<'a, ProjectionApplication> {
178 Box::pin(async move {
179 if self.applied.contains(checkpoint.cursor()) {
180 return Ok(ProjectionApplication::AlreadyApplied);
181 }
182 let balance = self
183 .balance
184 .checked_add(event.0)
185 .ok_or("balance overflow")?;
186 self.applied.insert(checkpoint.cursor().clone());
188 self.balance = balance;
189 Ok(ProjectionApplication::Applied)
190 })
191 }
192}
193#[derive(Default)]
194struct Checkpoints {
195 last: Option<ProjectionCheckpoint<String>>,
196 fail_save: bool,
197}
198impl AsyncProjectionCheckpointStore for Checkpoints {
199 type Cursor = String;
200 type Error = &'static str;
201 fn load<'a>(&'a self) -> PortFuture<'a, Option<ProjectionCheckpoint<String>>> {
202 Box::pin(async move { Ok(self.last.clone()) })
203 }
204 fn save<'a>(&'a mut self, checkpoint: ProjectionCheckpoint<String>) -> PortFuture<'a, ()> {
205 Box::pin(async move {
206 if self.fail_save {
207 return Err("checkpoint unavailable");
208 }
209 self.last = Some(checkpoint);
210 Ok(())
211 })
212 }
213}
214
215async fn deliver(
216 outbox: &mut Outbox,
217 broker: &mut Broker,
218 model: &mut ReadModel,
219 checkpoints: &mut Checkpoints,
220) -> Result<ProjectionApplication, &'static str> {
221 let claim = outbox.claim().await?.ok_or("queue empty")?;
222 let (id, event) = claim.into_parts();
223 broker.publish(&event).await?;
224 let checkpoint = ProjectionCheckpoint::new(id.as_str().to_owned());
227 let result = model.apply_once(&event, &checkpoint).await?;
228 checkpoints.save(checkpoint).await?;
229 outbox.acknowledge(&id).await?;
230 Ok(result)
231}
232fn main() {
233 let mut account = Account {
234 id: 1,
235 balance: 10,
236 events: vec![Credited(10)],
237 };
238 let mut outbox = Outbox::default();
239 let mut context = AsyncTransactionalOutboxLifecycleContext::new(
240 &mut account,
241 &mut outbox,
242 ExpectedRevision::NoStream(NoStream),
243 );
244 ready(context.save_and_enqueue()).unwrap();
245 context.complete().unwrap();
246 assert!(account.pull_events().is_empty());
247 assert_eq!(outbox.saved.as_ref().unwrap().balance, 10);
248 let mut broker = Broker {
249 fail: true,
250 ..Broker::default()
251 };
252 let mut model = ReadModel::default();
253 let mut checkpoints = Checkpoints::default();
254 assert_eq!(
255 ready(deliver(
256 &mut outbox,
257 &mut broker,
258 &mut model,
259 &mut checkpoints
260 )),
261 Err("broker down")
262 );
263 assert_eq!(outbox.queue.len(), 1);
264 assert_eq!(model.balance, 0);
265
266 broker.fail = false;
267 checkpoints.fail_save = true;
268 assert_eq!(
269 ready(deliver(
270 &mut outbox,
271 &mut broker,
272 &mut model,
273 &mut checkpoints
274 )),
275 Err("checkpoint unavailable")
276 );
277 assert_eq!(model.balance, 10);
278 assert!(ready(checkpoints.load()).unwrap().is_none());
279 checkpoints.fail_save = false;
280 outbox.fail_ack = true;
281 assert_eq!(
282 ready(deliver(
283 &mut outbox,
284 &mut broker,
285 &mut model,
286 &mut checkpoints
287 )),
288 Err("acknowledgement lost")
289 );
290 assert_eq!(model.balance, 10);
291 assert_eq!(outbox.queue.len(), 1);
292 outbox.fail_ack = false;
293 assert_eq!(
294 ready(deliver(
295 &mut outbox,
296 &mut broker,
297 &mut model,
298 &mut checkpoints
299 )),
300 Ok(ProjectionApplication::AlreadyApplied)
301 );
302 assert_eq!(model.balance, 10);
303 assert_eq!(broker.accepted.len(), 3); assert_eq!(ready(checkpoints.load()).unwrap().unwrap().cursor(), "1:0");
305 assert!(ready(outbox.claim()).unwrap().is_none());
306 println!("reliable_delivery: retried broker/checkpoint/ack failures; 3 deliveries, balance=10");
307}