Skip to main content

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