Azure Service Bus Sessions: Solving the Competing Consumers Problem for Ordered Parallel Processing

In the world of cloud messaging, where applications often exchange a continuous stream of data, ensuring the right order and correlation amongst messages can be critical. Azure Service Bus sessions provide a mechanism to achieve this.

Completing messages in order

In some scenarios, it is important to complete all messages in the order that they are sent such as

  • Financial Transactions : Actions like deposits, withdrawals, and transfers must occur in the precise order received to maintain account accuracy. Imagine withdrawing money before depositing it.
  • Online Order Processing : Tasks like payment verification, inventory checks, shipping updates, and invoice generation have a defined sequence. Any disruption could lead to incorrect inventory or unfulfilled orders.
  • Healthcare Patient Records : Adding lab results, medication changes, and doctor’s notes to a patient’s record must be in chronological order for an accurate medical history. Out-of-order updates could have serious consequences.

Azure Service bus normal operation

Under normal operation when using Azure Service bus (or the majority of messaging transports) there are two different modes for retrieving a message from a Topic or Queue

  • Read and Acknowledge : This will remove the message from the messaging queue and ensure that only one consumer even processes it, however if the processing fails, the message is lost.
  • Peek and Lock : This will hide the messages from all other consumers (until the lock expires) and after processing has been successfully the consumer can Acknowledge the message off of the queue, if there is an issue the message lock can be cancelled so that it can be picked up by the next available consumer.

When ordered processing is required messages can-not be processed in parallel in case there is a failure and one message is processed out of turn, this can have serious impact on the time that it takes for a message to be processed as if messages are being created faster than a single consumer can process it, with every messages each message will take longer to process.

If we consider the following messaging queue with financial transactions for 4 different accounts Messaging queue with no sessions

If each of these messages to 1 second to process the above would take 10 seconds to process. The requirement for ordered processing in this case is only important in relation to an account.

Azure Service Bus Sessions

Azure Service Bus Sessions give the ability assign a SessionId to each message, and then instead of locking the next message on a queue, all messages inside of a session are locked so that the consumer can read messages from a session in the correct order. For more information about Azure Service Bus Sessions Click here

If we consider the example above, and apply the functionality of Service Bus Sessions Messaging queue with sessions

We can now have a processor for each session whilst ensuring that the messages are still in a First-in, first out (FIFO) pattern inside of the assigned sessions. This enables the ability to scale the number of consumers with the load to ensure that an application doesn’t start to fall behind.

Equivalent functionality on other messaging transports

Apache Kafka: Achieves similar functionality through transactions. Messages can be grouped into a producer transaction, ensuring they are either all committed or all discarded as a unit, mimicking in-order processing within a session.

RabbitMQ: Offers mandatory routing, which allows a message to be sent to multiple queues simultaneously. Consumers can then compete to process the message, and only the first successful recipient acknowledges it, preventing duplicates.

IBM MQ: Offers Message Groups, which act similarly to Service Bus sessions. Messages can be assigned to a group, ensuring ordered delivery within the group. Message groups also support persistent application data, similar to session state in Service Bus.

Pulsar: Utilizes key sharding, where messages with the same key are always delivered to the same consumer. This can be useful for mimicking session-like behavior for messages that share a common identifier.

Example

The following Example will send a message with a session id:

var tokenCredential = new AzureCliCredential();
var client = new ServiceBusClient("https://myBus.servicebus.net", tokenCredential);

var topicName = "MyTopic";

var messageSender = client.CreateSender(topicName);

//Send a message to update Fred's account and set the session Id to "FredAccountId"
var message = new ServiceBusMessage(Encoding.UTF8.GetBytes("Update Fred's Account"))
{
    SessionId = "FredAccountId"
};

await sender.SendMessageAsync(message);

Receiving a message from a session

var tokenCredential = new AzureCliCredential();
var client = new ServiceBusClient("https://myBus.servicebus.net", tokenCredential);

var topicName = "MyTopic";
var subscriptionName = "MySubscription";

var consumer = await client.AcceptNextSessionAsync(topicName, subscriptionName);

var messages = await consumer.ReceiveMessagesAsync(CancellationToken.None);

//Acknowledge the message from the queue
await consumer.CompleteMessageAsync(messages[0], CancellationToken.None);

//Close the session so it can be picked up by another consumer
await consumer.CloseAsync(CancellationToken.None);