Skip to main content

ddd_toolkit_core/
outbox_lifecycle.rs

1#![allow(clippy::type_complexity)]
2
3use crate::{AggregateRoot, AppendError, AppendResult, ExpectedRevision, TransactionalOutbox};
4
5/// **English:** Failure from an enqueue attempt or rejection of a repeated attempt.
6///
7/// **日本語:** 登録の試行による失敗、または繰り返しの試行に対する拒否です。
8#[derive(Debug, PartialEq, Eq)]
9pub enum TransactionalOutboxOperationError<E> {
10    /// **English:** The outbox's append error, preserved without restoring pulled events.
11    /// This includes validation and revision errors, not only storage failures.
12    ///
13    /// **日本語:** 取り出したイベントを復元せずに保持するアウトボックスの追加エラーです。
14    /// ストレージ障害だけでなく、検証やリビジョンのエラーも含みます。
15    Adapter(AppendError<E>),
16
17    /// **English:** An attempt has already started in this context, whether it succeeded or failed.
18    /// The rejected call neither pulls events nor invokes the outbox again.
19    ///
20    /// **日本語:** このコンテキストで試行が開始済みであることを示します。成功・失敗は問いません。
21    /// 拒否された呼び出しは、イベントの取り出しもアウトボックスの再呼び出しも行いません。
22    AlreadyEnqueued,
23}
24
25/// **English:** Reason that consuming a lifecycle context did not confirm successful enqueueing.
26///
27/// **日本語:** ライフサイクルコンテキストの消費時に登録成功を確認できなかった理由です。
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum TransactionalOutboxCompletionError {
30    /// **English:** No enqueue attempt was made before completion.
31    ///
32    /// **日本語:** 完了確認までに登録が試行されていません。
33    MissingEnqueue,
34
35    /// **English:** An attempt started but did not record success; pulled events are not restored.
36    /// This state is set before pulling events, so it also covers an unwound attempt if the caller
37    /// catches a panic and later completes the context.
38    ///
39    /// **日本語:** 試行が開始されましたが、成功が記録されていません。取り出したイベントは復元しません。
40    /// この状態はイベントの取り出し前に設定するため、呼び出し側がパニックを捕捉して後で
41    /// 完了確認した場合は、巻き戻された試行も含みます。
42    EnqueueFailedConsumed,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46enum OperationState {
47    Pending,
48    Succeeded,
49    FailedConsumed,
50}
51
52/// **English:** Tracks one save-and-enqueue attempt while exclusively borrowing an aggregate and outbox.
53///
54/// [`Self::save_and_enqueue`] pulls pending events and passes ownership to the outbox. An error
55/// does not restore them, and a second attempt in the same context is rejected. The supplied
56/// expected revision is forwarded unchanged. Transactions, durability, and concurrent-writer
57/// coordination remain the responsibility of [`TransactionalOutbox`]'s implementation.
58/// Calling [`Self::complete`] checks recorded success; it does not commit a transaction.
59/// Dropping this context performs no completion check, rollback, or event restoration.
60///
61/// **日本語:** 集約とアウトボックスを排他的に借用し、保存・登録の一回の試行を追跡します。
62///
63/// [`Self::save_and_enqueue`] は保留イベントを取り出してアウトボックスへ所有権を渡します。
64/// エラー時も復元せず、同じコンテキストでの二回目の試行は拒否します。指定された期待
65/// リビジョンは変更せずに渡します。トランザクション、永続性、並行書き込みの調整は
66/// [`TransactionalOutbox`] の実装の責務です。[`Self::complete`] は記録された成功を
67/// 確認するだけで、コミットは行いません。コンテキストの破棄では完了確認、ロールバック、
68/// イベント復元のいずれも行いません。
69///
70/// **English:** A helper can propagate an enqueue error and check completion only after success.
71/// The assertion below follows from the context recording a successful outbox return.
72///
73/// **日本語:** ヘルパーでは登録エラーを伝播し、成功後にのみ完了確認できます。
74/// 以下のアサーションは、アウトボックスが成功を返したことが記録されるため成立します。
75///
76/// ```rust
77/// use ddd_toolkit_core::{
78///     AggregateRoot, AppendResult, ExpectedRevision, TransactionalOutbox,
79///     TransactionalOutboxLifecycleContext, TransactionalOutboxOperationError,
80/// };
81///
82/// fn enqueue<A, O>(
83///     aggregate: &mut A,
84///     outbox: &mut O,
85///     expected: ExpectedRevision,
86/// ) -> Result<AppendResult, TransactionalOutboxOperationError<O::Error>>
87/// where
88///     A: AggregateRoot,
89///     O: TransactionalOutbox<Aggregate = A>,
90/// {
91///     let mut context = TransactionalOutboxLifecycleContext::new(aggregate, outbox, expected);
92///     let result = context.save_and_enqueue()?;
93///     assert_eq!(context.complete(), Ok(()));
94///     Ok(result)
95/// }
96/// ```
97pub struct TransactionalOutboxLifecycleContext<'a, A, O>
98where
99    A: AggregateRoot,
100    O: TransactionalOutbox<Aggregate = A>,
101{
102    aggregate: &'a mut A,
103    outbox: &'a mut O,
104    expected: ExpectedRevision,
105    state: OperationState,
106}
107
108impl<'a, A, O> TransactionalOutboxLifecycleContext<'a, A, O>
109where
110    A: AggregateRoot,
111    O: TransactionalOutbox<Aggregate = A>,
112{
113    /// **English:** Borrows `aggregate` and `outbox` with no attempt yet recorded.
114    /// Construction neither pulls events nor calls the outbox; `expected` is saved for the attempt.
115    ///
116    /// **日本語:** `aggregate` と `outbox` を借用し、未試行の状態で作成します。
117    /// 構築時にはイベントを取り出さず、アウトボックスも呼び出しません。`expected` は試行用に保持します。
118    pub fn new(aggregate: &'a mut A, outbox: &'a mut O, expected: ExpectedRevision) -> Self {
119        Self {
120            aggregate,
121            outbox,
122            expected,
123            state: OperationState::Pending,
124        }
125    }
126
127    /// **English:** Pulls events once and calls the outbox with the aggregate, revision, and batch.
128    ///
129    /// Returns the outbox's [`AppendResult`] or wraps its [`AppendError`] without changing it.
130    /// Even an empty batch is passed to the outbox for validation. Once an attempt starts,
131    /// later calls return [`TransactionalOutboxOperationError::AlreadyEnqueued`], including
132    /// after failure. Pulled events are not recorded back into the aggregate on any error.
133    /// Panics from [`AggregateRoot::pull_events`] or the outbox propagate; the context marks
134    /// the attempt as unsuccessful before either call and provides no panic recovery.
135    ///
136    /// **日本語:** イベントを一回取り出し、集約、リビジョン、バッチをアウトボックスへ渡します。
137    ///
138    /// アウトボックスの [`AppendResult`] を返すか、[`AppendError`] を変更せずに包んで返します。
139    /// 空バッチも検証のためにアウトボックスへ渡します。一度試行を開始すると、失敗後も含め、
140    /// 後続の呼び出しは [`TransactionalOutboxOperationError::AlreadyEnqueued`] を返します。
141    /// いずれのエラーでも取り出したイベントは集約へ記録し直しません。
142    /// [`AggregateRoot::pull_events`] やアウトボックスのパニックは伝播します。両呼び出しの前に
143    /// 試行を未成功として記録しますが、パニックからの回復処理は提供しません。
144    pub fn save_and_enqueue(
145        &mut self,
146    ) -> Result<AppendResult, TransactionalOutboxOperationError<O::Error>> {
147        if self.state != OperationState::Pending {
148            return Err(TransactionalOutboxOperationError::AlreadyEnqueued);
149        }
150        self.state = OperationState::FailedConsumed;
151        let events = self.aggregate.pull_events();
152        match self
153            .outbox
154            .save_and_enqueue(self.aggregate, self.expected, events)
155        {
156            Ok(result) => {
157                self.state = OperationState::Succeeded;
158                Ok(result)
159            }
160            Err(error) => Err(TransactionalOutboxOperationError::Adapter(error)),
161        }
162    }
163
164    /// **English:** Consumes the context and reports whether its enqueue attempt succeeded.
165    ///
166    /// Returns `Ok(())` after success, [`TransactionalOutboxCompletionError::MissingEnqueue`]
167    /// before any attempt, or [`TransactionalOutboxCompletionError::EnqueueFailedConsumed`]
168    /// after an attempt without recorded success. No storage call or retry is performed;
169    /// rejecting a second enqueue does not erase a previously recorded success.
170    ///
171    /// **日本語:** コンテキストを消費し、登録の試行が成功したかを報告します。
172    ///
173    /// 成功後は `Ok(())`、未試行なら [`TransactionalOutboxCompletionError::MissingEnqueue`]、
174    /// 成功が記録されていない試行の後なら
175    /// [`TransactionalOutboxCompletionError::EnqueueFailedConsumed`] を返します。
176    /// ストレージの呼び出しや再試行は行いません。二回目の登録を拒否しても、記録済みの成功は消しません。
177    pub fn complete(self) -> Result<(), TransactionalOutboxCompletionError> {
178        match self.state {
179            OperationState::Succeeded => Ok(()),
180            OperationState::Pending => Err(TransactionalOutboxCompletionError::MissingEnqueue),
181            OperationState::FailedConsumed => {
182                Err(TransactionalOutboxCompletionError::EnqueueFailedConsumed)
183            }
184        }
185    }
186}
187
188/// **English:** Short name for [`TransactionalOutboxLifecycleContext`] with identical borrow and retry rules.
189///
190/// **日本語:** 借用と再試行の規則が同じ [`TransactionalOutboxLifecycleContext`] の短い別名です。
191pub type OutboxLifecycleContext<'a, A, O> = TransactionalOutboxLifecycleContext<'a, A, O>;
192
193/// **English:** Alias for [`TransactionalOutboxOperationError`], including append failures and repeated attempts.
194///
195/// **日本語:** 追加失敗と繰り返しの試行を表す [`TransactionalOutboxOperationError`] の別名です。
196pub type OutboxLifecycleOperationError<E> = TransactionalOutboxOperationError<E>;
197
198/// **English:** Alias for [`TransactionalOutboxCompletionError`] used when checking lifecycle completion.
199///
200/// **日本語:** ライフサイクルの完了確認に使う [`TransactionalOutboxCompletionError`] の別名です。
201pub type OutboxLifecycleCompletionError = TransactionalOutboxCompletionError;