1
votes

Using a saga, given an event EventA, saga starts, it sends a command (or many). How can we make sure that the command is sent successfully then actual logic in other micro-service did not throw, etc.

Let's have an example of email saga: When a user register, we create a User Aggregate which publishes UserRegisteredEvent, a saga will be created and this saga is responsible to make sure that registration email is sent to user (email may contain a verification key, welcome message, etc).

Should we use :

  1. commandGateway.sendAndWait with a try/catch -> does it scale?

  2. commandGateway.send and use a deadline and use some kind of "fail event" like SendEmailFailedEvent -> requires to associate a "token" for commands so can associate the "associationProperty" with the correct saga that sent SendRegistrationEmailCommand

  3. commandGateway.send(...).handle(...) -> in handle can we reference eventGateway/commandGateway that were in MyEmailSaga? If error we send an event? Or can we modify/call a method from the saga instance we had. If no error then other service have sent an event like "RegistrationEmailSentEvent" so saga will end.

  4. use deadline because we just use "send" and do not handle the eventual error of the command which may have failed to be sent (other service is down, etc)

  5. something else?

  6. Or a combination of all?

How to handle errors below? (use deadline or .handle(...) or other)

Errors could be:

  • command has no handlers (no service up, etc)

  • command was handled but exception is raised in other service and no event is sent (no try/catch in other service)

  • command was handled, exception raised and caught, other service publish an event to notify it failed to send email (saga will receive event and do appropriate action depending on event type and data provided -> maybe email is wrong or does not exist so no need to retry)

  • other errors I missed?

@Saga
public class MyEmailSaga {

    @Autowired
    transient CommandGateway commandGateway;


    @Autowired
    transient EventGateway eventGateway;

    @Autowired
    transient SomeService someService;

    String id;
    SomeData state;
    /** count retry times we send email so can apply logic on it */
    int sendRetryCount;

    @StartSaga
    @SagaEventHandler(associationProperty = "id")
    public void on(UserRegisteredEvent event) {
        id = event.getApplicationId();
        //state = event........
        // what are the possibilities here? 
        // Can we use sendAndWait but it does not scale very well, right?
        commandGateway.send(new SendRegistrationEmailCommand(...));
        // Is deadline good since we do not handle the "send" of the command
    }

    // Use a @DeadlineHandler to retry ?

    @DeadlineHandler(deadlineName = "retry_send_registration_email")
    fun on() {
         // resend command and re-schedule a deadline, etc
    }

    @EndSaga
    @SagaEventHandler(associationProperty = "id")
    public void on(RegistrationEmailSentEvent event) {

    }

}

EDIT (after accepted answer):

Mainly two options (Sorry but kotlin code below):

First option

commandGateway.send(SendRegistrationEmailCommand(...))
    .handle({ t, result ->
    if (t != null) {
       // send event (could be caught be the same saga eventually) or send command or both
    }else{
       // send event (could be caught be the same saga eventually) or send command or both
    }
    })
// If not use handle(...) then you can use thenApply as well
    .thenApply { eventGateway.publish(SomeSuccessfulEvent(...)) }
    .thenApply { commandGateway.send(SomeSuccessfulSendOnSuccessCommand) }

2nd option: Use a deadline to make sure that saga do something if SendRegistrationEmailCommand failed and you did not receive any events on the failure (when you do not handle the command sent).

Can of course use deadline for other purposes.

When the SendRegistrationEmailCommand was received successfully, the receiver will publish an event so the saga will be notified and act on it. Could be an RegistrationEmailSentEvent or RegistrationEmailSendFailedEvent.

Summary:

It seems that it is best to use handle() only if the command failed to be sent or receiver has thrown an unexpected exception, if so then publish an event for the saga to act on it. In case of success, the receiver should publish the event, saga will listen for it (and eventually register a deadline just in case); Receiver may also send event to notify of error and do not throw, saga will also listen to this event.

1

1 Answers

3
votes

ideally, you would use the asynchronous options to deal with errors. This would either be commandGateway.send(command) or commandGateway.send(command).thenApply(). If the failure are businesslogic related, then it may make sense to emit events on these failures. A plain gateway.send(command) then makes sense; the Saga can react on the events returned as a result. Otherwise, you will have to deal with the result of the command.

Whether you need to use sendAndWait or just send().then... depends on the activity you need to do when it fails. Unfortunately, when dealing with results asynchronously, you cannot safely modify the state of the Saga anymore. Axon may have persisted the state of the saga already, causing these changes to go lost. sendAndWait resolves that. Scalability is not often an issue, because different Sagas can be executed in parallel, depending on your processor configuration.

The Axon team is currently looking at possible APIs that would allow for safe asynchronous execution of logic in Sagas, while still keeping guarantees about thread safety and state persistence.