web-dev-qa-db-ja.com

EasyNetQ / RabbitMQでエラー処理を行う方法

EasyNetQライブラリを使用してC#でRabbitMQを使用しています。ここではpub/subパターンを使用しています。私はまだ誰かが私を助けてくれることを願っているいくつかの問題があります:

  1. メッセージの消費中にエラーが発生すると、メッセージは自動的にエラーキューに移動されます。再試行を実装するにはどうすればよいですか(元のキューに戻され、X回の処理に失敗すると、デッドレターキューに移動されます)。
  2. 私が見る限り、他のすべてのキューからメッセージをダンプするために使用されるエラーキューは常に1つあります。タイプごとに1つのエラーキューを作成して、各キューに独自のエラーキューを関連付けるにはどうすればよいですか?
  3. エラーキューにあるメッセージを簡単に再試行するにはどうすればよいですか? Hosepipeを試しましたが、元のキューではなくエラーキューにメッセージを再公開するだけです。コンソールをいじりたくないので、このオプションもあまり好きではありません。できれば、エラーキューに対してプログラムするだけです。

誰でも?

18
Leon Cullens

EasyNetQ/RabbitMQで発生している問題は、SQSやAzure Service Bus/Queuesなどの他のメッセージングサービスと比較すると、はるかに「生」であるということですが、正しい方向を示すように最善を尽くします。

質問1。

これはあなた次第です。最も簡単な方法は、RabbitMQ/EasyNetQでメッセージをNo-Ackできることです。メッセージはキューの先頭に配置され、再試行できます。これは、ほぼ即座に(時間遅延なしで)再試行され、他のメッセージの処理もブロックされるため(プリフェッチカウントが1の単一のサブスクライバーがある場合)、実際にはお勧めできません。

「MessageEnvelope」を使用する他の実装を見てきました。したがって、メッセージが失敗したときに、MessageEnvelopeの再試行変数をインクリメントし、メッセージをキューに再配信するラッパークラス。これを実行し、メッセージハンドラーの周りにラッピングコードを記述する必要があります。これはEasyNetQの機能ではありません。

上記を使用して、私は人々が封筒を使用するのを見ましたが、メッセージが完全に文字化されることを許可します。デッドレターキューに入ると、デッドレターキューからアイテムを読み取る別のアプリケーション/ワーカーが存在します。

上記のこれらのアプローチはすべて、メッセージの処理に対数/指数/あらゆる種類の増加する遅延を持たせるための優れた方法が実際にはないという小さな問題があります。メッセージをキューに戻す前に、しばらくの間コードで「保持」することができますが、それは良い方法ではありません。

これらすべてのオプションの中で、デッドレターキューを読み取り、再試行回数を含むエンベロープに基づいてメッセージを再ルーティングするかどうかを決定する独自のカスタムアプリケーションがおそらく最良の方法です。

質問2。

高度なAPIを使用して、キューごとのデッドレター交換を指定できます。 ( https://github.com/EasyNetQ/EasyNetQ/wiki/The-Advanced-API#declaring-queues )。ただし、これは、サブスクライブ/パブリッシュの単純なIBus実装を使用して、メッセージタイプとサブスクライバー名の両方に基づいて名前が付けられたキューを検索するため、ほとんどすべての場所で高度なAPIを使用する必要があることを意味します。キューのカスタム宣言を使用すると、キューの名前を自分で処理することになります。つまり、サブスクライブするときに、必要なものの名前などを知る必要があります。自動サブスクライブはもう必要ありません。

質問3

エラーキュー/デッドレターキューは単なる別のキューです。このキューをリッスンして、必要な処理を実行できます。しかし、それがあなたのニーズに合うように聞こえる、すぐに使えるソリューションは実際にはありません。

15
MindingData

私はあなたが説明したことを正確に実装しました。ここに私の経験に基づいて、あなたの質問のそれぞれに関連するいくつかのヒントがあります。

Q1(X回再試行する方法):

これには、_iMessage.Body.BasicProperties.Headers_を使用できます。エラーキューからメッセージを消費する場合は、選択した名前のヘッダーを追加するだけです。エラーキューに入ってくる各メッセージでこのヘッダーを探し、インクリメントします。これにより、実行中の再試行回数がわかります。

メッセージがXの再試行制限を超えたときに何をするかについての戦略があることは非常に重要です。そのメッセージを失いたくない。私の場合、その時点でメッセージをディスクに書き込みます。 EasyNetQは元のメッセージをエラー情報で自動的にラップするため、後で戻るために役立つデバッグ情報がたくさん提供されます。また、元のメッセージが含まれているため、必要に応じて、手動で(または、バッチ再処理コードを使用して自動化して)、後で制御された方法でメッセージを再キューイングできます。

Hosepipeユーティリティのコードを見て、これを行う良い方法を確認できます。実際、そこに表示されているパターンに従うと、後でHosepipeを使用して、必要に応じてメッセージを再キューイングすることもできます。

Q2(元のキューごとにエラーキューを作成する方法):

EasyNetQ Advanced Busを使用して、これをきれいに行うことができます。 _IBus.Advanced.Container.Resolve<IConventions>_を使用して、コンベンションインターフェイスにアクセスします。次に、_conventions.ErrorExchangeNamingConvention_および_conventions.ErrorQueueNamingConvention_を使用してエラーキューの命名規則を設定できます。私の場合、元のキューの名前に基づくように規則を設定して、キューを作成するたびにqueue/queue_errorのキューのペアを取得するようにしました。

Q3(エラーキュー内のメッセージを処理する方法):

他のキューと同じ方法で、エラーキューのコンシューマーを宣言できます。繰り返しになりますが、AdvancedBusでは、キューから出てくるタイプが_EasyNetQ.SystemMessage.Error_であることを指定することで、これをきれいに行うことができます。したがって、IAdvancedBus.Consume<EasyNetQ.SystemMessage.Error>()はそこに到達します。再試行とは、元の取引所に再公開することを意味し(ヘッダーに入力した再試行回数に注意して(上記のQ1に対する私の回答を参照)、エラーキューから消費したエラーメッセージの情報は、ターゲットを見つけるのに役立ちます再発行。

9
BitMask777

私はこれが古い投稿であることを知っていますが、誰かを助ける場合に備えて、ここにあります 私の自己回答の質問 (既存のヘルプでは不十分だったので質問する必要がありました)再試行の実装方法を説明しています元のキューで失敗したメッセージ。以下はあなたの質問#1と#3に答えるはずです。 #2の場合、私が使用したことのないAdvanced APIを使用する必要があるかもしれません(そして、それはEasyNetQ; oneの目的を損なうと思いますRabbitMQクライアントを直接使用することもできます)。ただし、IConsumerErrorStrategyの実装も検討してください。

1)メッセージのコンシューマーは複数存在する可能性があり、すべてがメッセージを再試行する必要がない可能性があるため、EasyNetQは(すぐに使用できる)複雑なタイプをサポートしていないため、メッセージの本文にDictionary<consumerId, RetryInfo>があります。メッセージヘッダー。

public interface IMessageType
{
    int MsgTypeId { get; }

    Dictionary<string, TryInfo> MsgTryInfo {get; set;}

}

2)フレームワークによって呼び出されるたびにTryCountやその他の情報を更新するだけのclass RetryEnabledErrorMessageSerializer : IErrorMessageSerializerを実装しました。 EasyNetQが提供するIoCサポートを介して、このカスタムシリアライザーをコンシューマーごとにフレームワークにアタッチします。

 public class RetryEnabledErrorMessageSerializer<T> : IErrorMessageSerializer where T : class, IMessageType
 {
        public string Serialize(byte[] messageBody)
        {
             string stringifiedMsgBody = Encoding.UTF8.GetString(messageBody);
             var objectifiedMsgBody = JObject.Parse(stringifiedMsgBody);

             // Add/update RetryInformation into objectifiedMsgBody here
             // I have a dictionary that saves <key:consumerId, val: TryInfoObj>

             return JsonConvert.SerializeObject(objectifiedMsgBody);
        }
  }

そして私のEasyNetQラッパークラスでは:

    public void SetupMessageBroker(string givenSubscriptionId, bool enableRetry = false)
    {
        if (enableRetry)
        {
            _defaultBus = RabbitHutch.CreateBus(currentConnString,
                                                        serviceRegister => serviceRegister.Register<IErrorMessageSerializer>(serviceProvider => new RetryEnabledErrorMessageSerializer<IMessageType>(givenSubscriptionId))
                                                );
        }
        else // EasyNetQ's DefaultErrorMessageSerializer will wrap error messages
        {
            _defaultBus = RabbitHutch.CreateBus(currentConnString);
        }
    }

    public bool SubscribeAsync<T>(Func<T, Task> eventHandler, string subscriptionId)
    {
        IMsgHandler<T> currMsgHandler = new MsgHandler<T>(eventHandler, subscriptionId);
        // Using the msgHandler allows to add a mediator between EasyNetQ and the actual callback function
        // The mediator can transmit the retried msg or choose to ignore it
        return _defaultBus.SubscribeAsync<T>(subscriptionId, currMsgHandler.InvokeMsgCallbackFunc).Queue != null;
    }

3)メッセージがデフォルトのエラーキューに追加されると、元のキューにある既存のエラーメッセージを定期的に再公開するシンプルなコンソールアプリ/ Windowsサービスを利用できます。何かのようなもの:

var client = new ManagementClient(AppConfig.BaseAddress, AppConfig.RabbitUsername, AppConfig.RabbitPassword);
var vhost = client.GetVhostAsync("/").Result;
var aliveRes = client.IsAliveAsync(vhost).Result;
var errQueue = client.GetQueueAsync(Constants.EasyNetQErrorQueueName, vhost).Result;
var crit = new GetMessagesCriteria(long.MaxValue, Ackmodes.ack_requeue_false);
var errMsgs = client.GetMessagesFromQueueAsync(errQueue, crit).Result;
foreach (var errMsg in errMsgs)
{
    var innerMsg = JsonConvert.DeserializeObject<Error>(errMsg.Payload);
    var pubInfo = new PublishInfo(innerMsg.RoutingKey, innerMsg.Message);
    pubInfo.Properties.Add("type", innerMsg.BasicProperties.Type);
    pubInfo.Properties.Add("correlation_id", innerMsg.BasicProperties.CorrelationId);
    pubInfo.Properties.Add("delivery_mode", innerMsg.BasicProperties.DeliveryMode);
    var pubRes = client.PublishAsync(client.GetExchangeAsync(innerMsg.Exchange, vhost).Result, pubInfo).Result;
}

4)コールバック関数を含むMessageHandlerクラスがあります。メッセージがコンシューマーに配信されるたびに、メッセージハンドラーに送信されます。MessageHandlerは、メッセージの試行が有効かどうかを判断し、有効な場合は実際のコールバックを呼び出します。 tryが有効でない場合(maxRetriesExceeded /コンシューマーはとにかく再試行する必要がない)、メッセージを無視します。この場合、メッセージをデッドレターすることを選択できます。

public interface IMsgHandler<T> where T: class, IMessageType
{
    Task InvokeMsgCallbackFunc(T msg);
    Func<T, Task> MsgCallbackFunc { get; set; }
    bool IsTryValid(T msg, string refSubscriptionId); // Calls callback only 
                                                      // if Retry is valid
}

コールバックを呼び出すMsgHandlerのメディエーター関数は次のとおりです。

    public async Task InvokeMsgCallbackFunc(T msg)
    {
        if (IsTryValid(msg, CurrSubscriptionId))
        {
            await this.MsgCallbackFunc(msg);
        }
        else
        {
            // Do whatever you want
        }
    }
3