调用OnMessage()后自动处理BrokeredMessage

时间:2015-05-26 20:05:28

标签: c# multithreading azure queue servicebus

我正在尝试从Azure Service Bus排队项目,以便我可以批量处理它们。我知道Azure Service Bus有一个ReceiveBatch()但由于以下原因似乎有问题:

  • 我一次最多只能获得256条消息,然后根据消息大小,这可能是随机的。
  • 即使我查看有多少邮件在等待,我也不知道要拨打多少条RequestBatch,因为我不知道每次通话会给我多少条消息。由于邮件会一直存在,我无法继续发出请求,直到它变空为止,因为它永远不会是空的。

我决定只使用比浪费偷看更便宜的消息监听器,并且会给我更多控制权。

  

基本上我试图让一定数量的消息积累起来   然后立即处理它们。我使用计时器强制延迟,但我需要   能够在他们进来时对我的物品进行排队。

根据我的计时器要求,似乎阻止集合不是一个好选项,所以我尝试使用ConcurrentBag。

var batchingQueue = new ConcurrentBag<BrokeredMessage>();
myQueueClient.OnMessage((m) =>
{
    Console.WriteLine("Queueing message");
    batchingQueue.Add(m);
});

while (true)
{
    var sw = WaitableStopwatch.StartNew();
    BrokeredMessage msg;
    while (batchingQueue.TryTake(out msg)) // <== Object is already disposed
    {
        ...do this until I have a thousand ready to be written to DB in batch
        Console.WriteLine("Completing message");
        msg.Complete(); // <== ERRORS HERE
    }

    sw.Wait(MINIMUM_DELAY);
}
  

但是,只要我在OnMessage之外访问该消息   管道显示BrokeredMessage已被处置。

我认为这必须是OnMessage的一些自动行为,除了立即处理之外我不会对消息做任何事情,我不想做。

5 个答案:

答案 0 :(得分:3)

使用BlockingCollection非常容易。

var batchingQueue = new BlockingCollection<BrokeredMessage>();

myQueueClient.OnMessage((m) =>
{
    Console.WriteLine("Queueing message");
    batchingQueue.Add(m);
});

您的消费者话题:

foreach (var msg in batchingQueue.GetConsumingEnumerable())
{
    Console.WriteLine("Completing message");
    msg.Complete();
}

GetConsumingEnumerable返回一个迭代器,它使用队列中的项目,直到设置IsCompleted属性并且队列为空。如果队列为空但IsCompletedFalse,则会对下一个项目进行非忙等待。

要取消使用者线程(即关闭程序),您将停止向队列添加内容并让主线程调用batchingQueue.CompleteAdding。消费者将清空队列,看到IsCompleted属性为True,然后退出。

此处使用BlockingCollection优于ConcurrentBagConcurrentQueue,因为BlockingCollection界面更易于使用。特别是,GetConsumingEnumerable的使用使您无需担心检查计数或进行忙等待(轮询循环)。它只是有效。

另请注意,ConcurrentBag有一些相当奇怪的删除行为。特别是,删除项目的顺序根据哪个线程删除项目而不同。创建包的线程以与其他线程不同的顺序删除项目。有关详细信息,请参阅Using the ConcurrentBag Collection

您还没有说明为什么要在输入中批量处理项目。除非有一个压倒一切的性能原因,否则使用该批处理逻辑使代码复杂化似乎不是一个特别好的主意。

如果要对数据库进行批量写入,那么我建议使用简单的List<T>来缓冲项目。如果必须在将项目写入数据库之前对其进行处理,请使用上面显示的技术来处理它们。然后,而是直接写入数据库,将项目添加到列表中。当列表获得1,000个项目或经过一定时间后,分配新列表并启动任务以将旧列表写入数据库。像这样:

// at class scope

// Flush every 5 minutes.
private readonly TimeSpan FlushDelay = TimeSpan.FromMinutes(5);
private const int MaxBufferItems = 1000;

// Create a timer for the buffer flush.
System.Threading.Timer _flushTimer = new System.Threading.Timer(TimedFlush, FlushDelay.TotalMilliseconds, Timeout.Infinite);  

// A lock for the list. Unless you're getting hundreds of thousands
// of items per second, this will not be a performance problem.
object _listLock = new Object();

List<BrokeredMessage> _recordBuffer = new List<BrokeredMessage>();

然后,在您的消费者中:

foreach (var msg in batchingQueue.GetConsumingEnumerable())
{
    // process the message
    Console.WriteLine("Completing message");
    msg.Complete();
    lock (_listLock)
    {
        _recordBuffer.Add(msg);
        if (_recordBuffer.Count >= MaxBufferItems)
        {
            // Stop the timer
            _flushTimer.Change(Timeout.Infinite, Timeout.Infinite);

            // Save the old list and allocate a new one
            var myList = _recordBuffer;
            _recordBuffer = new List<BrokeredMessage>();

            // Start a task to write to the database
            Task.Factory.StartNew(() => FlushBuffer(myList));

            // Restart the timer
            _flushTimer.Change(FlushDelay.TotalMilliseconds, Timeout.Infinite);
        }
    }
}

private void TimedFlush()
{
    bool lockTaken = false;
    List<BrokeredMessage> myList = null;

    try
    {
        if (Monitor.TryEnter(_listLock, 0, out lockTaken))
        {
            // Save the old list and allocate a new one
            myList = _recordBuffer;
            _recordBuffer = new List<BrokeredMessage>();
        }
    }
    finally
    {
        if (lockTaken)
        {
            Monitor.Exit(_listLock);
        }
    }

    if (myList != null)
    {
        FlushBuffer(myList);
    }

    // Restart the timer
    _flushTimer.Change(FlushDelay.TotalMilliseconds, Timeout.Infinite);
}

这里的想法是,您将旧列表排除在外,分配新列表以便继续处理,然后将旧列表的项目写入数据库。锁是为了防止计时器和记录计数器相互踩踏。如果没有锁定,事情可能会在一段时间内正常运行,然后在不可预测的时间内发生奇怪的崩溃。

我喜欢这种设计,因为它消除了消费者的民意调查。我唯一不喜欢的是消费者必须知道计时器(即它必须停止然后重新启动计时器)。稍微考虑一下,我可以消除这个要求。但它的编写方式很有效。

答案 1 :(得分:2)

切换到 OnMessageAsync 解决了我的问题

_queueClient.OnMessageAsync(async receivedMessage =>

答案 2 :(得分:1)

我向微软询问了关于在MSDN处理问题的BrokeredMessage,这是回复:

  

非常基本的规则,我不确定这是否有记录。收到的消息需要在回调函数的生命周期内处理。在您的情况下,将在异步回调完成时处理消息,这就是您在另一个线程中使用ObjectDisposedException失败的完整尝试的原因。

     

我真的不知道进一步处理的排队消息如何帮助提高吞吐量。这肯定会给客户增加更多负担。尝试在异步回调中处理消息,这应该足够高效。

就我而言,这意味着我无法以我想要的方式使用ServiceBus,而且我必须重新思考我希望如何运作。开溜。

答案 3 :(得分:1)

开始使用Azure Service Bus服务时遇到了同样的问题。

我发现OnMessage方法总是处理BrokedMessage对象。 Jim Mischel提出的方法对我没有帮助(但阅读非常有趣 - 谢谢!)。

经过一番调查后,我发现整个方法都是错误的。让我解释一下你想要的正确方法。

  1. 仅在OnMessage方法处理程序中使用BrokedMessage.Complete()方法。
  2. 如果您需要处理此方法之外的消息,则应使用方法QueueClient.Complete(Guid lockToken)。 “LockToken”是BrokeredMessage对象的属性。
  3. 示例:

     var messageOptions = new OnMessageOptions {
          AutoComplete       = false,
          AutoRenewTimeout   = TimeSpan.FromMinutes( 5 ),
         MaxConcurrentCalls = 1
     };
     var buffer = new Dictionary<string, Guid>();
    
     // get message from queue 
     myQueueClient.OnMessage(
          m => buffer.Add(key: m.GetBody<string>(), value: m.LockToken), 
          messageOptions // this option says to ServiceBus to "froze" message in he queue until we process it
     );         
    
     foreach(var item in buffer){
        try {
            Console.WriteLine($"Process item: {item.Key}");
            myQueueClient.Complete(item.Value);// you can also use method CompleteBatch(...) to improve performance
        } 
        catch{
            // "unfroze" message in ServiceBus. Message would be delivered to other listener 
            myQueueClient.Defer(item.Value);
        }
     }
    

答案 4 :(得分:0)

我的解决方案是获取消息SequenceNumber,然后推迟消息并将SequenceNumber添加到BlockingCollection。一旦BlockingCollection拾取了一个新项目,它就可以通过SequenceNumber接收延迟的消息,并将消息标记为已完成。如果由于某种原因BlockingCollection不处理SequenceNumber,它将按延迟保留在队列中,以便稍后在重新启动过程时可以将其提取。这样可以防止在BlockingCollection中仍然有项目的情况下异常终止该过程时导致消息丢失。

BlockingCollection<long> queueSequenceNumbers = new BlockingCollection<long>();

//This finds any deferred/unfinished messages on startup. 
BrokeredMessage existingMessage = client.Peek();
while (existingMessage != null)
{
    if (existingMessage.State == MessageState.Deferred)
    {
        queueSequenceNumbers.Add(existingMessage.SequenceNumber);
    }
    existingMessage = client.Peek();
}


//setup the message handler
Action<BrokeredMessage> processMessage = new Action<BrokeredMessage>((message) =>
{
    try
    {
        //skip deferred messages if they are already in the queueSequenceNumbers collection.
        if (message.State != MessageState.Deferred || (message.State == MessageState.Deferred && !queueSequenceNumbers.Any(x => x == message.SequenceNumber)))
        {
            message.Defer();
            queueSequenceNumbers.Add(message.SequenceNumber);
        }

    }
    catch (Exception ex)
    {
         // Indicates a problem, unlock message in queue
         message.Abandon();
    }

});


// Callback to handle newly received messages
client.OnMessage(processMessage, new OnMessageOptions() { AutoComplete = false, MaxConcurrentCalls = 1 });            

//start the blocking loop to process messages as they are added to the collection
foreach (var queueSequenceNumber in queueSequenceNumbers.GetConsumingEnumerable())
{
     var message = client.Receive(queueSequenceNumber);
     //mark the message as complete so it's removed from the queue
     message.Complete();                 
     //do something with the message       
}