How to implement a saga using a scatter/Gather pattern In MassTransit 3.0

后端 未结 3 1472
既然无缘
既然无缘 2021-02-03 18:00

Jimmy Boagard describes a McDonalds fast food chain here comparing it to a scatter gather pattern.

Workflow image stolen from above article:

Initial Im

相关标签:
3条回答
  • 2021-02-03 18:54

    I came into similar problem - need to publish few dozends of commands (all same interface, IMyRequest) and wait all.

    Actually my command initiates other saga, which publish IMyRequestDone at the end of processing without marking saga completed. (Need to complete them at some time later.) So instead of saving number of completed nested sagas in parent saga I just query state of child saga instances.

    Check on every MyRequestDone message:

    Schedule(() => FailSagaOnRequestsTimeout, x => x.CheckToken, x =>
    {
        // timeout for all requests
        x.Delay = TimeSpan.FromMinutes(10);
        x.Received = e => e.CorrelateById(context => context.Message.CorrelationId);
    });
    
    
    During(Active,
        When(Xxx)
            .ThenAsync(async context =>
            {
                await context.Publish(context => new MyRequestCommand(context.Instance, "foo"));
                await context.Publish(context => new MyRequestCommand(context.Instance, "bar"));
    
                context.Instance.WaitingMyResponsesTimeoutedAt = DateTime.UtcNow + FailSagaOnRequestsTimeout.Delay;
                context.Instance.WaitingMyResponsesCount = 2;
            })
            .TransitionTo(WaitingMyResponses)
            .Schedule(FailSagaOnRequestsTimeout, context => new FailSagaCommand(context.Instance))
        );
    
    During(WaitingMyResponses,
        When(MyRequestDone)
            .Then(context =>
            {
                if (context.Instance.WaitingMyResponsesTimeoutedAt < DateTime.UtcNow)
                    throw new TimeoutException();
            })
            .If(context =>
            {
                var db = serviceProvider.GetRequiredService<DbContext>();
                var requestsStates = db.MyRequestStates.Where(x => x.ParentSagaId == context.Instance.CorrelationId).Select(x => x.State).ToList();
                var allDone = requestsStates.Count == context.Instance.WaitingMyResponsesCount &&
                    requestsStates.All(x => x != nameof(MyRequestStateMachine.Processing)); // assume 3 states of request - Processing, Done and Failed
                return allDone;
            }, x => x
                .Unschedule(FailSagaOnRequestsTimeout)
                .TransitionTo(Active))
            )
            .Catch<TimeoutException>(x => x.TransitionTo(Failed))
    );
    
    During(WaitingMyResponses,
        When(FailSagaOnRequestsTimeout.Received)
            .TransitionTo(Failed)
    

    Periodically check that all requests done (by "Reducing NServiceBus Saga load"):

    Schedule(() => CheckAllRequestsDone, x => x.CheckToken, x =>
    {
        // check interval
        x.Delay = TimeSpan.FromSeconds(15);
        x.Received = e => e.CorrelateById(context => context.Message.CorrelationId);
    });
    
    During(Active,
        When(Xxx)
            .ThenAsync(async context =>
            {
                await context.Publish(context => new MyRequestCommand(context.Instance, "foo"));
                await context.Publish(context => new MyRequestCommand(context.Instance, "bar"));
    
                context.Instance.WaitingMyResponsesTimeoutedAt = DateTime.UtcNow.AddMinutes(10);
                context.Instance.WaitingMyResponsesCount = 2;
            })
            .TransitionTo(WaitingMyResponses)
            .Schedule(CheckAllRequestsDone, context => new CheckAllRequestsDoneCommand(context.Instance))
        );
    
    During(WaitingMyResponses,
        When(CheckAllRequestsDone.Recieved)
            .Then(context =>
            {
                var db = serviceProvider.GetRequiredService<DbContext>();
                var requestsStates = db.MyRequestStates.Where(x => x.ParentSagaId == context.Instance.CorrelationId).Select(x => x.State).ToList();
                var allDone = requestsStates.Count == context.Instance.WaitingMyResponsesCount &&
                    requestsStates.All(x => x != nameof(MyRequestStateMachine.Processing));
                if (!allDone)           
                {
                    if (context.Instance.WaitingMyResponsesTimeoutedAt < DateTime.UtcNow + CheckAllRequestsDone.Delay)              
                        throw new TimeoutException();
                    throw new NotAllDoneException();
                }
            })
            .TransitionTo(Active)
            .Catch<NotAllDoneException>(x => x.Schedule(CheckAllRequestsDone, context => new CheckAllRequestsDoneCommand(context.Instance)))
            .Catch<TimeoutException>(x => x.TransitionTo(Failed));
    
    0 讨论(0)
  • 2021-02-03 18:54

    The problem with kicking back finished events to the saga is that it creates contention on a shared resource (i.e the saga state).

    Jim has another post that came after the one you referenced that outlines the issue and solution. Of course, he's specifically talking about NServiceBus, but the problem and concepts are the same.

    https://lostechies.com/jimmybogard/2014/02/27/reducing-nservicebus-saga-load/

    Create an external storage. Put in a record for each work item. Let each workers set their own work to completed while the saga effectively polls using delayed messaging to see if all work is done.

    Then you are still doing scatter-gather, but the "aggregator" has been replaced by the process manager pattern to reduce contention.

    0 讨论(0)
  • 2021-02-03 18:57

    Couldn't you "simply" pass the object along in the queue, as an event parameter? When the saga listener gets an "order completed" Event it would contain the object that is completed in the event?

    I imagine it being sent to the queue via a Generic method, where the object must implement IFoodOrdered

    Then you can on the implement a virtual method that the saga can use to do the "generic" thing when it's picked up, and you only have to implement overloads for those special items, that require something special to happen?

    0 讨论(0)
提交回复
热议问题