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
Initialize a queue client queueClient
and store a reference to it in the global scope of the class
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;
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()
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
Initialize a queue client queueClient
and store a reference to it in the global scope of the class
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.
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()
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:
- When the windows service is about to close we somehow have to stop processing more messages
- The process should wait for messages being processed at that moment to complete
- 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;
}