繁体   English   中英

如何在关闭连接和模型后正确完成 RabbitMq 消费者并等待 RabbitMq 消费者线程(库消费者回调)?

[英]How to correctly finish RabbitMq consumer and wait to RabbitMq consumer threads (library consumer callbacks) after close connection and model?

我有处理传入消息的 RabbitMq 使用者( RabbitMQ.Client.Events.EventingBasicConsumer )。

但我注意到,如果关闭连接和模型,它们不会等待完成库处理线程。 例如:

  1. 如果我将几秒钟的线程睡眠添加到EventingBasicConsumer::Received回调中,那么我注意到Close函数( IModelIConnection Close() )在退出此消费者回调之前完成
  2. 完成Close函数后,我继续接收到EventingBasicConsumer::Received回调的一些消息。

那么如何正确关闭消费者并等待完成库消费者线程中的所有处理? 我想确保在关闭所有连接/消费者后,我不会从我的消费者收到任何来自图书馆的传入消息。

简化代码:

RunTest()
{
    MyConsumer consumer = new MyConsumer();
    consumer.Connect();
    
    // Wait before close for process some count of incoming messages
    Thread.Sleep(10 * 1000);
    
    consumer.Disconnect();
}

class MyConsumer
{
    private RabbitMQ.Client.IConnection     m_Connection = null;
    private RabbitMQ.Client.IModel          m_Channel = null;
        
    public void Connect()
    {
        //
        // ...
        //

        m_Channel = m_Connection.CreateModel();
        m_Consumer = new RabbitMQ.Client.Events.EventingBasicConsumer(m_Channel);
        m_Consumer.Received += OnRequestReceived;
        m_ConsumerTag = m_Channel.BasicConsume(m_Config.RequestQueue, false, m_Consumer);
    }
        
    public void Disconnect()
    {
        Console.WriteLine("---> IModel::Close()");
        m_Channel.Close();
        Console.WriteLine("<--- IModel::Close()");
        
        Console.WriteLine("---> RabbitMQ.Client.IConnection::Close()");
        m_Connection.Close();
        Console.WriteLine("<--- RabbitMQ.Client.IConnection::Close()");
        
        //
        // Maybe there is need to do some RabbitMQ API call of channel/model
        // for wait to finish of all consumer callbacks?
        //
        
        m_Channel = null;
        m_Connection = null;
    }

    private void OnRequestReceived(object sender, RabbitMQ.Client.Events.BasicDeliverEventArgs mqMessage)
    {
        Console.WriteLine("---> MyConsumer::OnReceived");
        
        Console.WriteLine("MyConsumer: ThreadSleep started");
        Thread.Sleep(10000);
        Console.WriteLine("MyConsumer: ThreadSleep finished");
        
        if (m_Channel != null)
        {
            m_Channel.BasicAck(mqMessage.DeliveryTag, false);
        }
        else
        {
            Console.WriteLine("MyConsumer: already closed");
        }
        
        Console.WriteLine("<--- MyConsumer::OnReceived");
    }
}

结果:

---> MyConsumer::OnReceived
MyConsumer: ThreadSleep started

---> IModel::Close() 
<--- IModel::Close() 
---> RabbitMQ.Client.IConnection::Close() 
<--- RabbitMQ.Client.IConnection::Close() 

MyConsumer: ThreadSleep finished
MyConsumer: already closed
<--- MyConsumer::OnReceived

---> MyConsumer::OnReceived
MyConsumer: ThreadSleep started
MyConsumer: ThreadSleep finished
MyConsumer: already closed
<--- MyConsumer::OnReceived

从 Consumer 和 Connection 的Close()函数退出后,我们如何看到MyConsumer::OnReceived已完成。 此外,我们如何看到还有一条消息是在上次调用 OnReceived 并关闭连接后的收入(这意味着 RqbbitMq 继续处理消费者消息,直到内部库队列为空,而忽略消费者和连接已经关闭的事实) .

这确实是 RabbitMQ.Client (v5.1.2) 中的错误。 ConsumerWorkService.cs 的源代码:

namespace RabbitMQ.Client
{
    public class ConsumerWorkService
    {
        ...

        class WorkPool
        {
            readonly ConcurrentQueue<Action> actions;
            readonly AutoResetEvent messageArrived;
            readonly TimeSpan waitTime;
            readonly CancellationTokenSource tokenSource;
            readonly string name;

            public WorkPool(IModel model)
            {
                name = model.ToString();
                actions = new ConcurrentQueue<Action>();
                messageArrived = new AutoResetEvent(false);
                waitTime = TimeSpan.FromMilliseconds(100);
                tokenSource = new CancellationTokenSource();
            }

            public void Start()
            {
#if NETFX_CORE
                System.Threading.Tasks.Task.Factory.StartNew(Loop, System.Threading.Tasks.TaskCreationOptions.LongRunning);
#else
                var thread = new Thread(Loop)
                {
                    Name = "WorkPool-" + name,
                    IsBackground = true
                };
                thread.Start();
#endif
            }

            public void Enqueue(Action action)
            {
                actions.Enqueue(action);
                messageArrived.Set();
            }

            void Loop()
            {
                while (tokenSource.IsCancellationRequested == false)
                {
                    Action action;
                    while (actions.TryDequeue(out action))
                    {
                        try
                        {
                            action();
                        }
                        catch (Exception)
                        {
                        }
                    }

                    messageArrived.WaitOne(waitTime);
                }
            }

            public void Stop()
            {
                tokenSource.Cancel();
            }
        }
    }
}
        

正如我们所见,没有任何线程var thread = new Thread(Loop)等待。 所以真正的事件RabbitMQ.Client.Events.EventingBasicConsumer::Received可以随时触发,即使长时间没有消费者或连接,很久以前关闭,直到内部库队列清空。 正如我所想的((:

Action action;
while (actions.TryDequeue(out action))
{
    try
    {
        action();
    }
    catch (Exception)
    {
    }
}

所以 IModel::Close() 将只设置 CancelationToken 而不加入线程,并且需要一些解决这个 Bug 的方法。

只是为了确认@Alexander 的发现,此问题仍然存在于 .Net Client 的 v6.2.2 中,并且事件驱动的回调消费者也存在此问题。

我发现:

  • 在连接关闭后,在EventingBasicConsumerAsyncEventingBasicConsumer注册的“Received”回调将被调用,最多可达通道上BasicQos设置的prefetchCount设置。
  • 在手动Ack模式下,如果连接关闭,调用BasicAck将抛出RabbitMQ.Client.Exceptions.AlreadyClosedException ,并且不会从队列中确认消息,但回调将继续处理后续消息。 这可能会导致在断开连接期间多次接收相同消息的幂等问题。

这可能是确保将 prefetchCount 设置为有限且合理的值的另一个很好的理由(除了例如无限制预取计数的内存考虑)。

最后,如果我故意意外关闭连接(例如在测试期间),我发现我需要显式分离我的消费者Received处理程序并显式调用consumerChannel.Close(); (即IModel.Close ),然后我才能创建新连接(Rabbit 客户端会倾向于“挂起”)。

  • 如果我使用同步EventingBasicConsumer设置了ConsumerDispatchConcurrency > 1,则connection.ConnectionShutdown事件似乎不会可靠地触发
  • 如果我在使用异步消费者时 ConsumerDispatchConcurrency > 1`,则AsyncEventingBasicConsumer上的 Shutdown 事件也不会触发。
  • 但是,我确实发现 IModel / 通道上的 ModelShutdown 事件可以可靠地触发同步 / 异步和并发 > 1。
consumerChannel.ModelShutdown += (sender, args) =>
{
    consumer.Received -= handler; // e.g. AsyncEventingBasicConsumer
    consumerChannel.Close(); // IModel
};

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM