[Discussion] Service Bus - Track 2 - Processor
See original GitHub issueThe intent of this issue is to solicit feedback from the community in advance of the GA of the next major version of the Service Bus library (which will follow several Preview releases). As a reference, the .NET Azure SDK guidelines can be found here: https://azure.github.io/azure-sdk/dotnet_introduction.html Terminology: Track 0 refers to the WindowsAzure.ServiceBus library Track 1 refers to the Microsoft.Azure.ServiceBus library Track 2 refers to the new library, Azure.Messaging.ServiceBus, we are working on releasing.
Background In the previous versions of the Service Bus library there were two models for receiving messages, which we will refer to here as “Push” and “Pull”. In Track 1, pull was used with the Receive method in the MessageReceiver. Push could be used on the QueueClient/SubscriptionClient/MessageReceiver by calling the RegisterMessageHandler/RegisterSessionHandler methods and passing in callbacks. The push methods provided a few features to make interacting with messages easier:
- automatically renew message/session locks
- option to automatically complete a message after the callback finishes executing.
- option to specify how many concurrent calls to the user callback to use
Processor In the new version of the Service Bus library, we are offering support for both the push and pull modes of receiving messages. However, instead of intermixing these modes on the same type, we offer a separate dedicated type for the push model, called the ServiceBusProcessor. The same set of features for push-based processing that was available within the Track 1 library will be available in Track 2. The pull operations are available on the ServiceBusReceiver.
var client = new ServiceBusClient("connectionString");
ServiceBusReceiver receiver = client.GetReceiver("myQueue");
// I can use the receiver to directly receive messages. I will need to
// handle completing the message, and renewing message locks myself.
IList<ServiceBusReceivedMessage> messages = await receiver.ReceiveBatchAsync(maximumMessageCount: 10);
// available processing options set to their defaults
var options = new ServiceBusProcessorOptions
{
MaxConcurrentCalls = 1,
AutoComplete = true,
MaxAutoLockRenewalDuration = TimeSpan.FromMinutes(5),
ReceiveMode = ReceiveMode.PeekLock,
PrefetchCount = 0
};
ServiceBusProcessor processor = client.GetProcessor("myQueue", options);
The model of passing in callbacks to a method, has been amended to setting event handlers on the processor.
processor.ProcessMessageAsync += MessageHandler;
processor.ProcessErrorAsync += ExceptionHandler;
async Task MessageHandler(ProcessMessageEventArgs args)
{
// if the handler returns without causing an exception to be thrown, the processor will
// automatically complete the received message
// the message is available within the event args
ServiceBusReceivedMessage message = args.Message;
// the settlement methods are also available within the event args
// for instance, if I wanted to deadletter this message for some reason, I would do so like this
await args.DeadLetterAsync(message);
}
// the exception handler would be triggered if an exception is thrown at any point while receiving
// and processing a message. This includes if the user message handler throws an exception.
Task ExceptionHandler(ProcessErrorEventArgs eventArgs)
{
// the error source indicates at which point in the processing the error occurred
// e.g. Receive/UserCallback/LockRenewal
string errorSource = eventArgs.ErrorSource;
Exception ex = eventArgs.Exception;
string namespace = eventArgs.FullyQualifiedNamespace;
string entityPath = eventArgs.EntityPath;
// I can do some application logging with this information
return Task.CompletedTask;
}
// now that we have registered our event handlers, we can actually start processing
await processor.StartProcessingAsync();
// to stop processing
await processor.StopProcessingAsync();
Session Processor The configuration used for setting up processing for a session entity is very similar to that shown above for non-session entities.
// available processing options set to their defaults
var options = new ServiceBusProcessorOptions
{
// the default used for MaxConcurrentCalls is different when getting a session processor
MaxConcurrentCalls = 8,
AutoComplete = true,
// this refers to the session lock when used with a session processor, as opposed to a
// message lock, as in the case for the regular processor.
MaxAutoLockRenewalDuration = TimeSpan.FromMinutes(5),
ReceiveMode = ReceiveMode.PeekLock,
PrefetchCount = 0
};
// we get back a ServiceBusSessionProcessor as opposed to a ServiceBusProcessor
ServiceBusSessionProcessor processor = client.GetSessionProcessor("myQueue", options);
processor.ProcessMessageAsync += MessageHandler;
processor.ProcessErrorAsync += ExceptionHandler;
// note that the args type used for the message handler is different than
// it is for the regular processor
async Task MessageHandler(ProcessSessionMessageEventArgs args)
{
// we can perform session operations ...
var sessionState = Encoding.UTF8.GetBytes("my session state");
await args.SetSessionStateAsync(sessionState);
// in addition to the message settlement operations
await args.DeadLetterAsync(message);
}
// now that we have registered our event handlers, we can actually start processing
await processor.StartProcessingAsync();
// to stop processing
await processor.StopProcessingAsync();
One other difference under the hood is that when using concurrency (MaxConcurrentCalls > 1) with a session processor, we will have separate receiver links for each thread that is processing messages. Doing this allows us to process multiple sessions at the same time as each receiver link would be scoped to a different session. For the regular processor case, we are following the Track 1 pattern of sharing a single receiver link among the multiple threads.
Issue Analytics
- State:
- Created 3 years ago
- Comments:29 (28 by maintainers)
Top GitHub Comments
In the new library, StopProcessing will await all ongoing message callbacks. This means they will be allowed to finish no longer how long they take. You will have access to the CancellationToken in the callback event args so that you can cancel early when the processor is being stopped.
If the user callback throws, then the message will not be completed automatically.
This is not aligned with the most real-world scenario. A normal flow would demand message completion if the processing does not fail. Otherwise, If there’s a deviation such as a need to defer or dead-letter, it’s an explicit operation and intentional.
I am not using a user callback / handler / processor anymore. It’s not providing me with what I need. The model of auto-completion by default and override that if you don’t want to is very convenient until complex scenarios need to be handled. For Azure Functions, it requires the change to the host.json and opt into the model of completing the messages. If this was inconvenient, it would be changed long time ago to set autoComplete to
false
.Of course. When working on a repro code, you want to send a message and repeat the error rather than keep resending messages - I’d do exactly that. The intent was to find issues that explicitly raised about the default behaviour of the handler options, not scan how many times a code snippet has that property set to
false
😄If you have 10 or 20 customers that say it doesn’t work for them, are you going to change the default and ignore thousands that didn’t raise anything? If this was a bad default, do you think everyone would be silent? Don’t think so.