[英]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
)。
但我注意到,如果关闭连接和模型,它们不会等待完成库处理线程。 例如:
EventingBasicConsumer::Received
回调中,那么我注意到Close
函数( IModel
和IConnection
Close()
)在退出此消费者回调之前完成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 中,并且事件驱动的回调消费者也存在此问题。
我发现:
EventingBasicConsumer
和AsyncEventingBasicConsumer
注册的“Received”回调将被调用,最多可达通道上BasicQos
设置的prefetchCount
设置。BasicAck
将抛出RabbitMQ.Client.Exceptions.AlreadyClosedException
,并且不会从队列中确认消息,但回调将继续处理后续消息。 这可能会导致在断开连接期间多次接收相同消息的幂等问题。这可能是确保将 prefetchCount 设置为有限且合理的值的另一个很好的理由(除了例如无限制预取计数的内存考虑)。
最后,如果我故意意外关闭连接(例如在测试期间),我发现我需要显式分离我的消费者Received
处理程序并显式调用consumerChannel.Close();
(即IModel.Close
),然后我才能创建新连接(Rabbit 客户端会倾向于“挂起”)。
EventingBasicConsumer
设置了ConsumerDispatchConcurrency
> 1,则connection.ConnectionShutdown
事件似乎不会可靠地触发AsyncEventingBasicConsumer
上的 Shutdown 事件也不会触发。consumerChannel.ModelShutdown += (sender, args) =>
{
consumer.Received -= handler; // e.g. AsyncEventingBasicConsumer
consumerChannel.Close(); // IModel
};
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.