eventuate-tram/eventuate-tram-sagas

Support nested sagas

cer opened this issue · 3 comments

cer commented

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 the SagaReplyInfo to send a reply to the command
cer commented

Design overview

See NestedSagaTest and other classes in that package.

There are two sagas:

  • OuterSaga - the saga, which has a participant that invokes InnerSaga
  • InnerSaga - the saga that implements a step of OuterSaga

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.