Skip to content

Commit 47f2d24

Browse files
fix(saga): require Arc<S> in SagaAdapter to guarantee shared state across bus subscriptions
SagaAdapter previously accepted S by value, meaning saga.clone() could produce independent copies with diverging state for callers whose saga struct does not internally Arc its own mutable fields. Changes: - Add blanket SubscriberId impl for Arc<S> in epoch_core::subscriber_id - Add blanket Saga<ED> impl for Arc<S> in epoch_core::saga (requires S::EventType: Sync for async future Send bounds) - Change SagaAdapter.saga field and new() parameter from S to Arc<S> - Update saga_adapter_integration_tests to use Arc::new(saga)
1 parent faf8936 commit 47f2d24

3 files changed

Lines changed: 68 additions & 11 deletions

File tree

epoch_core/src/saga.rs

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,49 @@ where
176176
}
177177
}
178178

179+
/// Blanket [`Saga`] implementation for `Arc<S>` where `S: Saga<ED>`.
180+
///
181+
/// This allows an `Arc`-wrapped saga to be used directly with [`SagaHandler`] and
182+
/// [`SagaAdapter`], enabling shared ownership across multiple bus subscriptions:
183+
///
184+
/// ```ignore
185+
/// let saga = Arc::new(MySaga::new(...));
186+
/// native_bus.subscribe(SagaHandler::new(saga.clone())).await?;
187+
/// foreign_bus.subscribe(SagaAdapter::new(saga.clone(), "saga:my:foreign", |e| ...)).await?;
188+
/// ```
189+
#[async_trait]
190+
impl<ED, S> Saga<ED> for Arc<S>
191+
where
192+
ED: EventData + Send + Sync + 'static,
193+
S: Saga<ED> + Send + Sync,
194+
S::EventType: Sync,
195+
{
196+
type State = S::State;
197+
type StateStore = S::StateStore;
198+
type SagaError = S::SagaError;
199+
type EventType = S::EventType;
200+
201+
fn get_state_store(&self) -> Self::StateStore {
202+
(**self).get_state_store()
203+
}
204+
205+
async fn handle_event(
206+
&self,
207+
state: Self::State,
208+
event: &Event<Self::EventType>,
209+
) -> Result<Option<Self::State>, Self::SagaError> {
210+
(**self).handle_event(state, event).await
211+
}
212+
213+
fn get_id_from_event(&self, event: &Event<Self::EventType>) -> Uuid {
214+
(**self).get_id_from_event(event)
215+
}
216+
217+
fn priority(&self) -> u8 {
218+
(**self).priority()
219+
}
220+
}
221+
179222
/// A wrapper type that provides an [`EventObserver`] implementation for [`Saga`] types.
180223
///
181224
/// Since Rust doesn't allow multiple blanket implementations of the same trait, and
@@ -300,7 +343,7 @@ where
300343
TargetEvent: EventData + Send + Sync + 'static,
301344
F: Fn(&SourceEvent) -> Option<TargetEvent> + Send + Sync,
302345
{
303-
saga: S,
346+
saga: Arc<S>,
304347
subscriber_id: String,
305348
converter: F,
306349
_marker: PhantomData<fn(SourceEvent) -> TargetEvent>,
@@ -315,15 +358,17 @@ where
315358
{
316359
/// Creates a new `SagaAdapter`.
317360
///
318-
/// * `saga` - The saga to wrap, shared via `Arc` so it can be subscribed
319-
/// to multiple buses.
361+
/// * `saga` - `Arc`-wrapped saga shared across bus subscriptions. Both
362+
/// [`SagaHandler`] and `SagaAdapter` accept `Arc<S>` (via the blanket
363+
/// `Saga` impl for `Arc<S>`), so a single `Arc::new(saga)` can back
364+
/// any number of adapters without cloning internal state.
320365
/// * `subscriber_id` - Unique identifier for *this specific subscription*.
321366
/// Used by the event bus to track an independent checkpoint per bus.
322367
/// Convention: `"saga:<saga-name>:<source-bus-name>"`.
323368
/// * `converter` - Closure mapping the foreign bus's event variants into
324369
/// the saga's native parent event type. Return `None` to skip events
325370
/// the saga does not care about.
326-
pub fn new(saga: S, subscriber_id: impl Into<String>, converter: F) -> Self {
371+
pub fn new(saga: Arc<S>, subscriber_id: impl Into<String>, converter: F) -> Self {
327372
Self {
328373
saga,
329374
subscriber_id: subscriber_id.into(),
@@ -341,7 +386,7 @@ where
341386
impl<S, SourceEvent, TargetEvent, F> crate::SubscriberId
342387
for SagaAdapter<S, SourceEvent, TargetEvent, F>
343388
where
344-
S: Saga<TargetEvent>,
389+
S: Saga<TargetEvent> + Send + Sync,
345390
SourceEvent: EventData + Send + Sync + 'static,
346391
TargetEvent: EventData + Send + Sync + 'static,
347392
F: Fn(&SourceEvent) -> Option<TargetEvent> + Send + Sync,
@@ -384,7 +429,7 @@ where
384429
causation_id: event.causation_id,
385430
correlation_id: event.correlation_id,
386431
};
387-
self.saga.process_event(&target_event).await?;
432+
self.saga.as_ref().process_event(&target_event).await?;
388433
Ok(())
389434
}
390435

epoch_core/src/subscriber_id.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,15 @@ pub trait SubscriberId {
8383
/// The ID must be stable across deployments and restarts.
8484
fn subscriber_id(&self) -> &str;
8585
}
86+
87+
/// Delegates [`SubscriberId`] through an `Arc` so `Arc<S>` can be used
88+
/// wherever `S: SubscriberId` is required (e.g. with [`SagaHandler`] and
89+
/// [`SagaAdapter`]).
90+
///
91+
/// [`SagaHandler`]: crate::saga::SagaHandler
92+
/// [`SagaAdapter`]: crate::saga::SagaAdapter
93+
impl<S: SubscriberId + ?Sized> SubscriberId for std::sync::Arc<S> {
94+
fn subscriber_id(&self) -> &str {
95+
(**self).subscriber_id()
96+
}
97+
}

epoch_pg/tests/saga_adapter_integration_tests.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ async fn saga_adapter_receives_events_from_foreign_bus() {
229229
let target_stream = Uuid::new_v4();
230230
let source_stream = Uuid::new_v4();
231231

232-
let saga = CounterSaga::new(&native_sub_id, saga_id);
232+
let saga = Arc::new(CounterSaga::new(&native_sub_id, saga_id));
233233

234234
target_bus
235235
.subscribe(SagaHandler::new(saga.clone()))
@@ -293,7 +293,7 @@ async fn saga_adapter_preserves_event_metadata_through_conversion() {
293293
let saga_id = Uuid::new_v4();
294294
let stream_id = Uuid::new_v4();
295295

296-
let saga = CounterSaga::new(format!("saga:meta:{}", tag), saga_id);
296+
let saga = Arc::new(CounterSaga::new(format!("saga:meta:{}", tag), saga_id));
297297
source_bus
298298
.subscribe(SagaAdapter::new(
299299
saga.clone(),
@@ -350,7 +350,7 @@ async fn saga_adapter_advances_independent_checkpoints() {
350350
let target_stream = Uuid::new_v4();
351351
let source_stream = Uuid::new_v4();
352352

353-
let saga = CounterSaga::new(&native_sub_id, saga_id);
353+
let saga = Arc::new(CounterSaga::new(&native_sub_id, saga_id));
354354
target_bus
355355
.subscribe(SagaHandler::new(saga.clone()))
356356
.await
@@ -425,7 +425,7 @@ async fn saga_adapter_resumes_from_checkpoint_after_restart() {
425425

426426
// First run.
427427
{
428-
let saga = CounterSaga::new(format!("saga:restart:{}", tag), saga_id);
428+
let saga = Arc::new(CounterSaga::new(format!("saga:restart:{}", tag), saga_id));
429429
source_bus
430430
.subscribe(SagaAdapter::new(
431431
saga.clone(),
@@ -463,7 +463,7 @@ async fn saga_adapter_resumes_from_checkpoint_after_restart() {
463463
let source_store2 = PgEventStore::new(pool.clone(), source_bus2.clone());
464464
source_bus2.setup_trigger().await.unwrap();
465465

466-
let saga2 = CounterSaga::new(format!("saga:restart:{}", tag), saga_id);
466+
let saga2 = Arc::new(CounterSaga::new(format!("saga:restart:{}", tag), saga_id));
467467
source_bus2
468468
.subscribe(SagaAdapter::new(
469469
saga2.clone(),

0 commit comments

Comments
 (0)