Sourcing::Saga
A role for saga classes that coordinate long-running multi-step business processes across multiple aggregations. Sagas maintain state machines, track compensating transactions for rollback, and support timeout handling.
saga keyword via Metamodel::SagaHOW.
What It Does
Sagas extend aggregations with additional capabilities for coordinating complex business workflows:
- Undo blocks — LIFO stack of callable blocks for rollback on failure
- State machine — Declarative state transitions via return types
- Timeout support — Schedule and handle timeouts for long-running processes
- Aggregation binding — Lazy loading and tracking of related aggregates
How It's Used
You declare a saga using the saga keyword. The metaclass automatically composes the role and generates necessary infrastructure:
use Sourcing;
saga AccountTransfer {
has Str $.saga-id is projection-id;
has Str $.state = 'pending';
has Account $.from-account;
has Account $.to-account;
has Rat $.amount;
multi method apply(TransferRequested $e --> 'ready') {
$!from-account = sourcing Account, :id($e.from-id);
$!to-account = sourcing Account, :id($e.to-id);
$!amount = $e.amount;
self.timeout-in: 'cancel-transfer', :5minutes;
}
method execute() is on-state('ready') is command {
$!from-account.withdraw: $!amount;
'transferring'
}
multi method apply(Withdrawn $e --> 'depositing') {
$!to-account.deposit: $!amount;
self.undo: *.from-account.reverse-withdraw: $e.amount;
self.undo: *.to-account.reverse-deposit: $!amount;
self.timeout-in: 'confirm-delivery', :5minutes;
}
multi method apply(Deposited $e --> 'completed') { }
method cancel-transfer() {
self.rollback;
'cancelled'
}
method confirm-delivery() is on-state('depositing') {
'completed'
}
}
Attributes
The saga role provides these built-in attributes:
| Attribute | Type | Description |
|---|---|---|
$.state | Str | Current saga state |
@.undo-blocks | Callable | LIFO stack of undo blocks for rollback |
@.timeout-schedule | Pair | Ordered array of DateTime => Set of handler names |
%.timeout-handlers | Hash | Handler name => Hash{:date-time, :method-name} |
Public Methods
method start
Creates a new saga and emits a SagaCreated event to initialize the saga in the event store.
method undo(Callable $block)
Stores a callable (undo block) on the LIFO stack for potential rollback. Unlike compensation events which are emitted to the event store, undo blocks are executed directly during rollback. This is useful for calling methods directly on aggregates without generating new events.
During rollback, all undo blocks are executed in reverse order (LIFO). The blocks receive the saga as the topic ($_), allowing method chaining:
# After withdrawing from an account, register the undo block
$!account.withdraw: 100;
$.undo: *.account.reverse-withdraw: 100;
# During rollback, this block executes:
# $!account.reverse-withdraw: 100;
Note: Undo blocks complement compensation events — use events when you need auditability, use undo blocks for direct aggregate manipulation.
method timeout-in(Str $method-name, *%params)
Schedules a timeout that fires after a specified duration. If no method name is provided (or if the method name is 'rollback'), the default rollback method will be called when the timeout fires:
- Computes
scheduled-at = DateTime.now.later(|%params) - Adds to
%!timeout-handlers - Adds to
@!timeout-schedule(keeping it ordered) - Emits
TimeOutScheduledevent
Default behavior: If you call self.timeout-in: 'rollback', :5minutes or simply don't specify a method name that exists, the timeout will automatically call self.rollback when it fires. This is useful for automatically canceling long-running processes that exceed their time limit.
Note: Can only be called from within an apply() method.
method rollback() is command
Emits all registered compensations in LIFO order and executes all undo blocks in reverse order. Clears both stacks. Called automatically when any exception is thrown.
method verify-timeouts() is command
Iterates over the ordered timeout schedule. While the next entry's DateTime is <= DateTime.now, emits TimedOut events. Should be called periodically (e.g., every 10 seconds) by an external scheduler.
method cancel-timeout(Str $method-name)
Cancels a scheduled timeout by name. Removes the entry from %!timeout-handlers and all entries with that method name from @!timeout-schedule.
method lookup(Sourcing::Saga:U $type, *%ids)
Retrieves a saga by ID from the event store. Returns a new saga instance with all events applied.
method send-command(Mu $aggregate-type, %ids, Mu $command)
Sends a command to an aggregate. Emits a command event to the event store directed at the specified aggregate type.
method bind-aggregate(Str $attr-name, Mu $aggregate-type, %ids)
Binds an aggregate reference to the saga and emits a SagaAggregationBound event to track the relationship in the event store.
Internal Events
Sagas work with these internal event classes (in Sourcing::Saga::Events):
| Event | Attributes | Purpose |
|---|---|---|
SagaCreated | $.saga-id, $.saga-type, %.aggregation-ids | Emitted when saga starts |
SagaAggregationBound | $.saga-id, $.attribute-name, $.aggregation-type, %.ids | Emitted when binding aggregates |
TimeOutScheduled | $.saga-id, $.handler-name, $.scheduled-at | Persists timeout schedule |
TimedOut | $.saga-id, $.handler-name | Fires when timeout expires |
How Timeout Scheduling Works
- Scheduling — Call
self.timeout-in: "handler", :5minutesinsideapply() - Replaying —
apply(TimeOutScheduled)restores the schedule from the event store - Firing — External scheduler calls
verify-timeouts()periodically - Handling —
apply(TimedOut)looks up the handler and dispatches to it - Canceling — Call
self.cancel-timeout("handler")to remove
State Machine
Each apply method declares its resulting state via the return type syntax --> 'state-name'. The saga automatically tracks the current state in $.state.
Command methods can use the is on-state('state-name') trait to restrict them to specific states.
See Also
- Metaclasses — how the
sagakeyword works viaMetamodel::SagaHOW - Sourcing::Aggregation — sagas inherit aggregation functionality
- Traits — the
is on-statetrait for state guards