web-dev-qa-db-ja.com

MassTransit3.0でスキャッター/ギャザーパターンを使用してサガを実装する方法

ジミー・ボアガードは、マクドナルドのファーストフードチェーンについて説明しています ここ それを スキャッターギャザーパターンと比較しています。

上記の記事から盗まれたワークフロー画像: enter image description here

最初の実装の考え:

すべてのフードステーションが取得するすべてのタイプのFoodOrderedイベントに共通のインターフェイスを設定すると、各フードステーションはそれぞれのアイテムを消費/作成し、共通の完了イベントを公開できます。例:フライドポテトとハンバーガーステーションはフライドポテトの注文に関するメッセージを受け取ります。フライドポテトステーションは注文を消費し、佐賀がリッスンしているItemDoneEventをアナウンスします。

最初の懸念:

佐賀は完成した料理の種類を気にしないので、すべての料理が完成したという事実だけで、これは[〜#〜] ok [〜#〜] 解決。ただし、after警告を読んだ ここ キューの共有とそのことに気づいた Consumer.ConditionalフィルタリングはMassTransitで削除されました3. このタイプのアプローチでは、フレームワークが「BadThings(TM)が発生する」と言っているように感じます。しかし、メッセージのリクエストとレスポンスを作成し、キッチンの各食品のイベントを関連付けることなく、他にどのようにそれを行うかはわかりません。例:FriesOrdered、BurgerOrdered FriesCooked、BurgerCooked。あなたが台所のすべてのアイテムのためにそれをしなければならなかったら、これは非常に退屈でしょう?

上記の懸念を考えると、このタイプのワークフローの良いサガの例はどのようになりますか?

43
Ashtonian

私は同様の問題に遭遇しました-数十のコマンド(すべて同じインターフェース、IMyRequest)を公開し、すべてを待つ必要があります。

実際、私のコマンドは他のsagaを開始し、処理の最後にsagaの完了をマークせずにIMyRequestDoneを公開します。 (後で完了する必要があります。)したがって、完了したネストされたsagaの数を親sagaに保存する代わりに、子sagaインスタンスの状態を照会するだけです。

すべてのMyRequestDoneメッセージを確認してください:

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)

すべてのリクエストが実行されたことを定期的に確認します(「NServiceBusSagaの負荷を減らす」による)。

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));
1
smg

イベントパラメータとして、オブジェクトを「単に」キューに渡すことができませんでしたか? sagaリスナーが「注文完了」イベントを取得すると、イベントで完了したオブジェクトが含まれますか?

オブジェクトがIFoodOrderedを実装する必要があるGenericメソッドを介してキューに送信されることを想像します

次に、サガがピックアップされたときに「一般的な」ことを行うために使用できる仮想メソッドを実装できます。特別な何かが発生する必要がある特別なアイテムのオーバーロードを実装するだけで済みますか?

1
Morten Bork

終了したイベントをサガにキックバックする際の問題は、共有リソース(つまりサガの状態)で競合が発生することです。

ジムには、あなたが参照したものの後に、問題と解決策の概要を説明する別の投稿があります。もちろん、彼は特にNServiceBusについて話しているが、問題と概念は同じである。

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

外部ストレージを作成します。各作業項目の記録を入れてください。佐賀が遅延メッセージングを使用して効果的にポーリングし、すべての作業が完了したかどうかを確認しながら、各ワーカーに自分の作業を完了に設定させます。

その後、まだスキャッターギャザーを実行していますが、競合を減らすために「アグリゲーター」がプロセスマネージャーパターンに置き換えられています。

0
Keith