eventuate-tram / eventuate-tram-sagas

Sagas for microservices
Other
994 stars 225 forks source link

Support nested sagas #82

Open cer opened 1 year ago

cer commented 1 year ago

Currently

Proposed:

Support saga command handlers that

cer commented 1 year ago

Builds on https://github.com/eventuate-tram/eventuate-tram-core/issues/179

cer commented 1 year ago

Design overview

See NestedSagaTest and other classes in that package.

There are two sagas:

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());
    }
warslett commented 10 months ago

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.