5

When using async/await I'm concerned with disposing of the client while it's in the middle of handling a message. Consider the following:

  1. Initialize a queue client queueClient and store a reference to it in the global scope of the class

  2. Queue client handles a message and calls some application code to handle it, which might end up doing some async database work or call a remote api.

  3. Consider the application is a windows service with a CloseAsync method that is signaled when the service should be shutdown. Within this method I call queueClient.CloseAsync()

  4. The work being done in step 2 finishes and goes to call message.Complete(). At this point I'm assuming the queueClient has been closed and the message would be treated as a failure.

What is the best practice for ensuring that the queue client doesn't handle anymore messages and waits to close until any currently processing messages are finished?

Peter Bons
  • 26,826
  • 4
  • 50
  • 74
The Muffin Man
  • 19,585
  • 30
  • 119
  • 191

1 Answers1

7

You can either cancel the work of step 2 using a CancellationToken and/or await the async message handling code in step 4 before awaiting the call to queueClient.CloseAsync(). I take it you are familiair with Tasks and Cancellation.

Await message handling task

  1. Initialize a queue client queueClient and store a reference to it in the global scope of the class

  2. Queue client handles a message and calls some application code to handle it, which might end up doing some async database work or call a remote api, for example public Task HandleMessageAsync() {..}. Store a reference to this Task in the global scope of the class. For example private Task messageHandleTask;

  3. Consider the application is a windows service with a CloseAsync method that is signaled when the service should be shutdown. Within this method I call first await messageHandleTask and then await queueClient.CloseAsync()

  4. We all live long and happily.

In this scenario the service won't be completely stopped until the message handling is complete.

Cancel message handling task

  1. Initialize a queue client queueClient and store a reference to it in the global scope of the class

  2. Queue client handles a message and calls some application code to handle it, passing along a CancellationToken, which might end up doing some async database work or call a remote api, for example public Task HandleMessageAsync(CancellationToken token) {..}. Store a reference to this Task in the global scope of the class.

  3. Consider the application is a windows service with a CloseAsync method that is signaled when the service should be shutdown. Within this method I call first cancellationTokenSource.Cancel(), then await messageHandleTask and finally await queueClient.CloseAsync()

  4. We all live long and happily.

In this scenario, in the message handling code, just before the call to message.Complete(). you check for any cancellations: token.ThrowIfCancellationRequested. This case, when the service is closed the message never reaches the completed state and will be processed later on. (Be aware, I do not know the code involved so this scenario could be complex if the work is already partly done before the cancellation occures) Be sure to handle any OperationCanceledException.

Concurrent message handling

In a scenario where multiple messages are handled concurrent there is some more logic involved. The flow would be something like this:

  1. When the windows service is about to close we somehow have to stop processing more messages
  2. The process should wait for messages being processed at that moment to complete
  3. Now we can call queueClient.CloseAsync().

Unfortunately there is no standard mechanism to stop accepting more messages, so we have to build that ourselves. There is an Azure Feedback item requesting this, but it is still open.

I came up with the following solution that implements the flow described above, based on this documentation example:

QueueClient queueClient;
CancellationTokenSource cts = new CancellationTokenSource();
ActionBlock<Message> actionBlock;

async Task Main()
{
    // Define message processing pipeline
    actionBlock = new ActionBlock<Message>(ProcessMessagesAsync, new ExecutionDataflowBlockOptions
    {
        BoundedCapacity = 10,
        MaxDegreeOfParallelism = 10
    });
    
    queueClient = new QueueClient("Endpoint=sb:xxx", "test");

    RegisterOnMessageHandlerAndReceiveMessages(cts.Token);

    Console.WriteLine("Press [Enter] to stop processing messages");
    Console.ReadLine();
    
    // Signal the message handler to stop processing messages, step 1 of the flow
    cts.Cancel();
    
    // Signal the processing pipeline that no more message will come in,  step 1 of the flow
    actionBlock.Complete();
    
    // Wait for all messages to be done before closing the client, step 2 of the flow
    await actionBlock.Completion;
        
    await queueClient.CloseAsync(); // step 3 of the flow
    Console.ReadLine();
}

void RegisterOnMessageHandlerAndReceiveMessages(CancellationToken stoppingToken)
{
    // Configure the message handler options in terms of exception handling, number of concurrent messages to deliver, etc.
    var messageHandlerOptions = new MessageHandlerOptions(ExceptionReceivedHandler)
    {
        // Maximum number of concurrent calls to the callback ProcessMessagesAsync(), set to 1 for simplicity.
        // Set it according to how many messages the application wants to process in parallel.
        MaxConcurrentCalls = 10,

        // Indicates whether the message pump should automatically complete the messages after returning from user callback.
        // False below indicates the complete operation is handled by the user callback as in ProcessMessagesAsync().
        AutoComplete = false
    };

    // Register the function that processes messages.
    queueClient.RegisterMessageHandler(async (msg, token) =>
    {
        // When the stop signal is given, do not accept more messages for processing
        if(stoppingToken.IsCancellationRequested)
            return;
            
        await actionBlock.SendAsync(msg);
        
    }, messageHandlerOptions);
}

async Task ProcessMessagesAsync(Message message)
{
    Console.WriteLine($"Received message: SequenceNumber:{message.SystemProperties.SequenceNumber} Body:{Encoding.UTF8.GetString(message.Body)}");

    // Process the message.
    await Task.Delay(5000);
    
    // Complete the message so that it is not received again.
    // This can be done only if the queue Client is created in ReceiveMode.PeekLock mode (which is the default).
    await queueClient.CompleteAsync(message.SystemProperties.LockToken);

    Console.WriteLine($"Completed message: SequenceNumber:{message.SystemProperties.SequenceNumber} Body:{Encoding.UTF8.GetString(message.Body)}");
}

Task ExceptionReceivedHandler(ExceptionReceivedEventArgs exceptionReceivedEventArgs)
{
    Console.WriteLine($"Message handler encountered an exception {exceptionReceivedEventArgs.Exception}.");
    var context = exceptionReceivedEventArgs.ExceptionReceivedContext;
    Console.WriteLine("Exception context for troubleshooting:");
    Console.WriteLine($"- Endpoint: {context.Endpoint}");
    Console.WriteLine($"- Entity Path: {context.EntityPath}");
    Console.WriteLine($"- Executing Action: {context.Action}");
    return Task.CompletedTask;
}
Martin Wickman
  • 19,662
  • 12
  • 82
  • 106
Peter Bons
  • 26,826
  • 4
  • 50
  • 74
  • 2
    By the way, if this answer suites you, send me a muffin, they look delicious ;-) – Peter Bons Aug 11 '17 at 16:16
  • Thanks for the detailed answer. To be clear, I'd rather not introduce the complexity of rolling back partial changes that the application was working on while handling a message, I'd rather just let it finish before allowing the service to close, so it seems the first example would be the simplest and what I'm currently doing is the right way? I had assumed, since await would let program execution continue while it was waiting on the database, that it would allow the `queueClient.CloseAsync` method to be called before the database returned control to the application. – The Muffin Man Aug 11 '17 at 17:23
  • 1
    The first example best fits your scenario then, indeed. If you `await` the method that communicates with de database first and then await the call to `queueClient.CloseAsync()` then you can be assured that the first task is completed before the call to close the client. – Peter Bons Aug 11 '17 at 17:28
  • With multiple concurrent messages, will this work? Messages can continue to be received and new tasks added to `taskList` during `await Task.WhenAll(taskList)`. `WhenAll` converts the list to an array before waiting so newly created tasks will not be waited on before `CloseAsync()` is called. – Justin J Stark Feb 19 '20 at 15:02
  • The problem with this is if you're using MaxConcurrentCalls (which you should be) you now don't have such a nice simple cleanup. – Shane Courtrille Feb 19 '20 at 18:11
  • You are right, I will try to address this in the answer tomorrow – Peter Bons Feb 19 '20 at 21:10
  • 1
    To anyone finding this comment at this stage and wanting to use option 3 - please be aware that the use of the ActionBlock introduces a bug where the background processing job to renew the peeklock lock was not firing whilst awaiting the processing to be completed. This means for long running tasks, you are unable to complete the message and it will end up requeued until dead lettered. I have instead introduced a hashset of message ID's which I add to until 'max capacity', removing once processed, and a while loop with timeout instead of awaiting completion of the ActionBlock. – Jonno Apr 22 '21 at 09:10
  • Scratch that - there's a method on QueueClient of 'UnregisterMessageHandlerAsync' which takes a timeout timespan, await that and it waits for the processes to end gracefully. – Jonno Apr 23 '21 at 10:14