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