How to do error handling with EasyNetQ / RabbitMQ

后端 未结 3 1143
慢半拍i
慢半拍i 2021-02-01 20:26

I\'m using RabbitMQ in C# with the EasyNetQ library. I\'m using a pub/sub pattern here. I still have a few issues that I hope anyone can help me with:

  1. When there\'
3条回答
  •  醉酒成梦
    2021-02-01 21:10

    I know this is an old post but - just in case it helps someone else - here is my self-answered question (I needed to ask it because existing help was not enough) that explains how I implemented retrying failed messages on their original queues. The following should answer your question #1 and #3. For #2, you may have to use the Advanced API, which I haven't used (and I think it defeats the purpose of EasyNetQ; one might as well use RabbitMQ client directly). Also consider implementing IConsumerErrorStrategy, though.

    1) Since there can be multiple consumers of a message and all may not need to retry a msg, I have a Dictionary in the body of the message, as EasyNetQ does not (out of the box) support complex types in message headers.

    public interface IMessageType
    {
        int MsgTypeId { get; }
    
        Dictionary MsgTryInfo {get; set;}
    
    }
    

    2) I have implemented a class RetryEnabledErrorMessageSerializer : IErrorMessageSerializer that just updates the TryCount and other information every time it is called by the framework. I attach this custom serializer to the framework on a per-consumer basis via the IoC support provided by EasyNetQ.

     public class RetryEnabledErrorMessageSerializer : 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 
    
                 return JsonConvert.SerializeObject(objectifiedMsgBody);
            }
      }
    

    And in my EasyNetQ wrapper class:

        public void SetupMessageBroker(string givenSubscriptionId, bool enableRetry = false)
        {
            if (enableRetry)
            {
                _defaultBus = RabbitHutch.CreateBus(currentConnString,
                                                            serviceRegister => serviceRegister.Register(serviceProvider => new RetryEnabledErrorMessageSerializer(givenSubscriptionId))
                                                    );
            }
            else // EasyNetQ's DefaultErrorMessageSerializer will wrap error messages
            {
                _defaultBus = RabbitHutch.CreateBus(currentConnString);
            }
        }
    
        public bool SubscribeAsync(Func eventHandler, string subscriptionId)
        {
            IMsgHandler currMsgHandler = new MsgHandler(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(subscriptionId, currMsgHandler.InvokeMsgCallbackFunc).Queue != null;
        }
    

    3) Once the message is added to the default error queue, you can have a simple console app/windows service that periodically republishes existing error messages on their original queues. Something like:

    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(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) I have a MessageHandler class that contains a callback func. Whenever a message is delivered to the consumer, it goes to the MessageHandler, which decides if the message try is valid and calls the actual callback if so. If try is not valid (maxRetriesExceeded/the consumer does not need to retry anyway), I ignore the message. You can choose to Dead Letter the message in this case.

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

    Here is the mediator function in MsgHandler that invokes the callback:

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

提交回复
热议问题