ddd_toolkit_core/async_outbox_lifecycle.rs
1#![allow(clippy::type_complexity)]
2
3use crate::{AggregateRoot, AppendError, AppendResult, AsyncTransactionalOutbox, ExpectedRevision};
4use std::future::Future;
5use std::pin::Pin;
6
7/// **English:** Failure of an asynchronous enqueue attempt or rejection of a repeated attempt.
8///
9/// **日本語:** 非同期登録の試行による失敗、または繰り返しの試行に対する拒否です。
10#[derive(Debug, PartialEq, Eq)]
11pub enum AsyncTransactionalOutboxOperationError<E> {
12 /// **English:** The outbox's append error, including validation, revision, or storage failures.
13 /// Pulled events are not restored when this error is returned.
14 ///
15 /// **日本語:** 検証、リビジョン、ストレージの失敗を含むアウトボックスの追加エラーです。
16 /// このエラーを返す際、取り出したイベントは復元しません。
17 Adapter(AppendError<E>),
18
19 /// **English:** A previous call started an attempt, even if its future was never polled.
20 /// The rejected call does not pull events or invoke the adapter.
21 ///
22 /// **日本語:** 以前の呼び出しで試行を開始済みです。その Future が未ポーリングでも該当します。
23 /// 拒否された呼び出しでは、イベントを取り出さず、アダプターも呼び出しません。
24 AlreadyEnqueued,
25}
26
27/// **English:** Reason completion could not confirm a successful asynchronous enqueue.
28///
29/// **日本語:** 完了確認で非同期登録の成功を確認できなかった理由です。
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum AsyncTransactionalOutboxCompletionError {
32 /// **English:** No call to enqueue was made before completing the context.
33 ///
34 /// **日本語:** コンテキストの完了確認までに登録が呼び出されていません。
35 MissingEnqueue,
36
37 /// **English:** An attempt started without recorded success, including an error or dropped future.
38 /// The state is set before pulling events, so a caught panic can also leave this result.
39 ///
40 /// **日本語:** エラーや Future の破棄などにより、試行開始後に成功が記録されていません。
41 /// イベントの取り出し前にこの状態を設定するため、パニックを捕捉した後もこの結果になり得ます。
42 EnqueueFailedConsumed,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46enum OperationState {
47 Pending,
48 Succeeded,
49 FailedConsumed,
50}
51
52/// **English:** Single-attempt asynchronous enqueue tracking with exclusive aggregate and outbox borrows.
53///
54/// Calling [`Self::save_and_enqueue`] consumes the attempt and pulls events immediately, before
55/// its future is polled. Failure or dropping that future does not restore events or enable retry.
56/// Only a successful adapter result records success. [`Self::complete`] checks this record;
57/// it does not commit a transaction. Dropping the context performs no check or rollback.
58/// Transaction and cancellation effects on storage belong to the outbox adapter.
59///
60/// The aggregate must be `Send + Sync`, its event must be `Send`, and the outbox and error must
61/// be `Send` to support the returned `Send` future. No executor or locking is provided.
62///
63/// **日本語:** 集約とアウトボックスを排他的に借用し、非同期登録の一回の試行を追跡します。
64///
65/// [`Self::save_and_enqueue`] の呼び出し時に、Future のポーリング前に試行を消費して
66/// イベントを取り出します。失敗や Future の破棄でもイベントは復元せず、再試行もできません。
67/// アダプターの成功結果だけが成功を記録します。[`Self::complete`] はこの記録を検査するだけで、
68/// コミットしません。コンテキストの破棄でも検査やロールバックは行いません。
69/// トランザクションとキャンセルがストレージに及ぼす効果はアダプターが定めます。
70///
71/// `Send` な Future のため、集約には `Send + Sync`、イベント、アウトボックス、エラーには
72/// `Send` が必要です。実行器やロックは提供しません。
73///
74/// **English:** Await a successful enqueue before consuming the context to check completion.
75/// The assertion follows from recording success after the adapter returns `Ok`.
76///
77/// **日本語:** 登録成功を待ってからコンテキストを消費して完了確認します。
78/// アダプターが `Ok` を返した後に成功を記録するため、以下のアサーションが成立します。
79///
80/// ```rust
81/// use ddd_toolkit_core::{
82/// AggregateRoot, AppendResult, AsyncTransactionalOutbox,
83/// AsyncTransactionalOutboxLifecycleContext, AsyncTransactionalOutboxOperationError,
84/// ExpectedRevision,
85/// };
86///
87/// async fn enqueue<A, O>(
88/// aggregate: &mut A,
89/// outbox: &mut O,
90/// expected: ExpectedRevision,
91/// ) -> Result<AppendResult, AsyncTransactionalOutboxOperationError<O::Error>>
92/// where
93/// A: AggregateRoot + Send + Sync,
94/// A::Event: Send,
95/// O: AsyncTransactionalOutbox<Aggregate = A> + Send,
96/// {
97/// let mut context = AsyncTransactionalOutboxLifecycleContext::new(aggregate, outbox, expected);
98/// let result = context.save_and_enqueue().await?;
99/// assert_eq!(context.complete(), Ok(()));
100/// Ok(result)
101/// }
102/// ```
103pub struct AsyncTransactionalOutboxLifecycleContext<'storage, A, O>
104where
105 A: AggregateRoot + Send + Sync,
106 A::Event: Send,
107 O: AsyncTransactionalOutbox<Aggregate = A> + Send,
108 O::Error: Send,
109{
110 aggregate: &'storage mut A,
111 outbox: &'storage mut O,
112 expected: ExpectedRevision,
113 state: OperationState,
114}
115
116impl<'storage, A, O> AsyncTransactionalOutboxLifecycleContext<'storage, A, O>
117where
118 A: AggregateRoot + Send + Sync,
119 A::Event: Send,
120 O: AsyncTransactionalOutbox<Aggregate = A> + Send,
121 O::Error: Send,
122{
123 /// **English:** Borrows the aggregate and outbox for `'storage` and stores `expected` unchanged.
124 /// The context starts pending; construction does not pull events or call the adapter.
125 ///
126 /// **日本語:** 集約とアウトボックスを `'storage` の間借用し、`expected` を変更せず保持します。
127 /// 未試行で開始し、構築時にはイベントを取り出さず、アダプターも呼び出しません。
128 pub fn new(
129 aggregate: &'storage mut A,
130 outbox: &'storage mut O,
131 expected: ExpectedRevision,
132 ) -> Self {
133 Self {
134 aggregate,
135 outbox,
136 expected,
137 state: OperationState::Pending,
138 }
139 }
140
141 /// **English:** Immediately reserves the attempt and pulls events, then returns a boxed `Send` future.
142 ///
143 /// The future borrows this context for `'ctx`. Polling it calls the adapter with the borrowed
144 /// aggregate, stored revision, and owned batch, including an empty batch. It returns the
145 /// [`AppendResult`] or wraps the unchanged [`AppendError`]. Dropping the future before its first
146 /// poll discards the pulled batch without calling the adapter; dropping it while pending leaves
147 /// the attempt unsuccessful, with storage effects adapter-defined. Neither path restores events.
148 /// A later call returns a future yielding [`AsyncTransactionalOutboxOperationError::AlreadyEnqueued`].
149 ///
150 /// Panics from pulling events or invoking/polling the adapter propagate. State is marked
151 /// unsuccessful before pulling; catching a panic does not enable another attempt.
152 ///
153 /// **日本語:** 呼び出し時に試行を確保してイベントを取り出し、ボックス化された `Send` Future を返します。
154 ///
155 /// Future はコンテキストを `'ctx` の間借用します。ポーリング時に、借用した集約、保存済み
156 /// リビジョン、所有されたバッチをアダプターへ渡します。空バッチも渡します。
157 /// [`AppendResult`] を返すか、変更していない [`AppendError`] を包んで返します。
158 /// 初回ポーリング前の破棄ではアダプターを呼ばずにバッチを破棄します。待機中の破棄では
159 /// 試行が未成功のままとなり、ストレージへの効果はアダプターに依存します。
160 /// どちらもイベントを復元しません。後続の呼び出しは
161 /// [`AsyncTransactionalOutboxOperationError::AlreadyEnqueued`] を返す Future を返します。
162 ///
163 /// イベントの取り出しやアダプターの呼び出し・ポーリングのパニックは伝播します。
164 /// 取り出し前に未成功と記録するため、パニックを捕捉しても再試行できません。
165 pub fn save_and_enqueue<'ctx>(
166 &'ctx mut self,
167 ) -> Pin<
168 Box<
169 dyn Future<
170 Output = Result<AppendResult, AsyncTransactionalOutboxOperationError<O::Error>>,
171 > + Send
172 + 'ctx,
173 >,
174 >
175 where
176 'storage: 'ctx,
177 A: 'ctx,
178 A::Event: 'ctx,
179 O: 'ctx,
180 O::Error: 'ctx,
181 {
182 if self.state != OperationState::Pending {
183 return Box::pin(async {
184 Err(AsyncTransactionalOutboxOperationError::AlreadyEnqueued)
185 });
186 }
187
188 self.state = OperationState::FailedConsumed;
189 let state = &mut self.state;
190 let events = self.aggregate.pull_events();
191 let aggregate: &'ctx A = &*self.aggregate;
192 let outbox: &'ctx mut O = &mut *self.outbox;
193 let expected = self.expected;
194
195 Box::pin(async move {
196 let mut guard = AttemptGuard::new(state);
197 match outbox.save_and_enqueue(aggregate, expected, events).await {
198 Ok(result) => {
199 guard.succeed();
200 Ok(result)
201 }
202 Err(error) => Err(AsyncTransactionalOutboxOperationError::Adapter(error)),
203 }
204 })
205 }
206
207 /// **English:** Consumes the context and returns `Ok(())` only after recorded adapter success.
208 /// No enqueue call yields [`AsyncTransactionalOutboxCompletionError::MissingEnqueue`]; a call
209 /// without recorded success yields [`AsyncTransactionalOutboxCompletionError::EnqueueFailedConsumed`].
210 /// This makes no storage call. Rejection of a repeated attempt does not erase an earlier success.
211 /// Any returned enqueue future must release its mutable borrow before this method is called.
212 ///
213 /// **日本語:** コンテキストを消費し、アダプターの成功を記録済みの場合のみ `Ok(())` を返します。
214 /// 登録未呼び出しなら [`AsyncTransactionalOutboxCompletionError::MissingEnqueue`]、呼び出し済みで
215 /// 未成功なら [`AsyncTransactionalOutboxCompletionError::EnqueueFailedConsumed`] を返します。
216 /// ストレージは呼び出しません。再試行の拒否は以前の成功を消しません。
217 /// 呼び出す前に、登録の Future が保持する可変借用を解放する必要があります。
218 pub fn complete(self) -> Result<(), AsyncTransactionalOutboxCompletionError> {
219 match self.state {
220 OperationState::Succeeded => Ok(()),
221 OperationState::Pending => Err(AsyncTransactionalOutboxCompletionError::MissingEnqueue),
222 OperationState::FailedConsumed => {
223 Err(AsyncTransactionalOutboxCompletionError::EnqueueFailedConsumed)
224 }
225 }
226 }
227}
228
229struct AttemptGuard<'a> {
230 state: &'a mut OperationState,
231 succeeded: bool,
232}
233
234impl<'a> AttemptGuard<'a> {
235 fn new(state: &'a mut OperationState) -> Self {
236 Self {
237 state,
238 succeeded: false,
239 }
240 }
241
242 fn succeed(&mut self) {
243 *self.state = OperationState::Succeeded;
244 self.succeeded = true;
245 }
246}
247
248impl Drop for AttemptGuard<'_> {
249 fn drop(&mut self) {
250 if !self.succeeded {
251 *self.state = OperationState::FailedConsumed;
252 }
253 }
254}
255
256/// **English:** Short alias for [`AsyncTransactionalOutboxLifecycleContext`] with the same cancellation rules.
257///
258/// **日本語:** キャンセルの規則も同じ [`AsyncTransactionalOutboxLifecycleContext`] の短い別名です。
259pub type AsyncOutboxLifecycleContext<'a, A, O> = AsyncTransactionalOutboxLifecycleContext<'a, A, O>;
260
261/// **English:** Alias for [`AsyncTransactionalOutboxOperationError`] covering enqueue and repeated-attempt errors.
262///
263/// **日本語:** 登録と再試行拒否のエラーを表す [`AsyncTransactionalOutboxOperationError`] の別名です。
264pub type AsyncOutboxLifecycleOperationError<E> = AsyncTransactionalOutboxOperationError<E>;
265
266/// **English:** Alias for [`AsyncTransactionalOutboxCompletionError`] used for the final state check.
267///
268/// **日本語:** 最終状態の確認に使う [`AsyncTransactionalOutboxCompletionError`] の別名です。
269pub type AsyncOutboxLifecycleCompletionError = AsyncTransactionalOutboxCompletionError;