Support nested sagas
cer opened this issue · 3 comments
Currently
- a Saga command handler returns a reply message.
- The command handler cannot be implemented by a saga.
Proposed:
Support saga command handlers that
- Return void
- Have a parameter -
SagaReplyInfo
- that contains the data needed to construct a reply, e.g. message headers etc. - The
SagaReplyInfo
can be passed to a Saga - A Saga's completion callbacks
onXXX()
- can use theSagaReplyInfo
to send a reply to the command
Builds on eventuate-tram/eventuate-tram-core#179
Design overview
See NestedSagaTest and other classes in that package.
There are two sagas:
OuterSaga
- the saga, which has a participant that invokesInnerSaga
InnerSaga
- the saga that implements a step ofOuterSaga
SagaCommandHandler (part of OuterSaga
) that initiates InnerSaga
public class ParticipantCommandHandlers {
public CommandHandlers commandHandlerDefinitions() {
return SagaCommandHandlersBuilder
.fromChannel("customerService")
.onMessage(ReserveCreditCommand.class, this::reserveCredit)
private void reserveCredit(CommandMessage<ReserveCreditCommand> cm, CommandReplyToken commandReplyToken) {
InnerSagaData data = new InnerSagaData(commandReplyToken);
sagaInstanceFactory.create(innerSaga, data);
}
Key point: the command handler instantiates new InnerSagaData(commandReplyToken)
, which enables InnerSaga
to send a reply to OuterSaga
when it completes
Sending a reply when InnerSaga
completes
public class InnerSaga implements SimpleSaga<InnerSagaData> {
@Override
public void onSagaCompletedSuccessfully(String sagaId, InnerSagaData innerSagaData) {
SimpleSaga.super.onSagaCompletedSuccessfully(sagaId, innerSagaData);
commandReplyProducer.sendReplies(innerSagaData.getCommandReplyToken(), withSuccess());
}
We are doing something similar to this with eventuate where we have a high level saga (what we have been calling the parent saga) which initiates child sagas in other services. This works out the box but it has the problem that the parent saga doesn't know when or if the child saga completes successfully. It just starts the child saga and then carries on.
Our solution has been to consume the command from the parent saga using a simple message consumer subscription (instead of an eventuate command handler), where we then start the saga and we set the command message headers to the saga data object. Then we have an observer hooked in to our saga which listens for a success or rollback and at that point sends the relevant reply.
This means that the parent saga doesn't complete until the child saga completes and if the child saga rolls back then it causes the parent saga to rollback. There is still one problem with this solution which is if you have multiple child sagas and a later child saga rolls back after a previous child saga has completed then we will need to create a separate mechanism to roll back the previously completed child saga.