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:** 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;