Azure Service Bus Essentials — Message Settlement with Peek-Lock Mode
How does a consumer use the SDK client to complete, abandon, defer, or dead-letter a received message?
What is message settlement?
When a consumer receives a message from the broker, the consumer decides what to do with the message.
The consumer can make different decisions here. For example, the processing was successful, and the message can be deleted from the queue. Perhaps, the message violates business rules, so that it should go into the dead letter queue. Maybe, it is not the right time to process the message, so the consumer decides to defer the processing.
Whatever the action had been taken on the message, the consumer must communicate that to the broker to maintain a reliable processing guarantee.
This communication is called the message settlement.
In this post, we’ll discuss the message settlement options available at Azure Service Bus.
Receive-and-Delete and Peek-Lock modes
To consume messages, the Service Bus API client provides two explicit consumption modes: Receive-and-Delete and Peek-Lock.
They cater to different use cases and offer you a different level of control for message consumption.
Let’s have a look.
Receive-and-Delete mode (Destructive read)
The Service Bus documentation says:
The receive-and-delete mode tells the broker to consider all messages it sends to the receiving client as settled when sent. That means that the message is considered consumed as soon as the broker has put it onto the wire. If the message transfer fails, the message is lost.
Receive-and-Delete is a destructive read operation.
When the consumer receives a message, it has already been settled at the broker. Meaning, the broker has already deleted the message, hoping that the consumer may process it successfully. That takes the settlement complexity away from the consumer, allowing high throughput message processing.
However, this mode is not suitable for processing high-value messages. The reason is the lack of a delivery guarantee. Suppose the consumer dies in the middle of processing, the message will be lost forever. If individual messages have low value and/or are only meant for a very short time, this is a reasonable choice.
Peek-Lock mode (Non-destructive read)
As the name implies, this mode allows consumers to peek at a message from the queue, then lock it for processing.
When a consumer locks a message, the broker temporarily hides it from other consumers. However, the lock on the message has a timeout, which is set for 30 seconds by default. The consumer must process the message and settle it before the lock times out. Otherwise, the broker will release the lock and puts the message back in the queue, allowing it to be processed by other competing consumers.
This mode is slower but provides safety and persistence.
Within the lock duration, the consumer can settle a message in different methods. Let’s look at each option in detail.
Message settlement options
Based on the message’s payload and the availability of dependencies, a consumer can settle a message in the following ways. Message settlement can only be used when using a receiver in Peek-Lock mode, which is the default behavior.
1. Complete a message
When the processing completes successfully, the consumer sends a positive acknowledgment to the broker by calling Complete
API. It indicates to the broker that the message has been successfully processed, and the message can then be removed from the queue. The broker then responds with the settlement result.
The following shows how to complete a message with the C#.NET SDK.
string connectionString = "<connection_string>";
string queueName = "<queue_name>";
// since ServiceBusClient implements IAsyncDisposable we create it with "await using"
await using var client = new ServiceBusClient(connectionString);// create the sender
ServiceBusSender sender = client.CreateSender(queueName);// create a message that we can send
ServiceBusMessage message = new ServiceBusMessage("Hello world!");// send the message
await sender.SendMessageAsync(message);// create a receiver that we can use to receive and settle the message
ServiceBusReceiver receiver = client.CreateReceiver(queueName);// the received message is a different type as it contains some //service set properties
ServiceBusReceivedMessage receivedMessage = await receiver.ReceiveMessageAsync();// complete the message, thereby deleting it from the service
await receiver.CompleteMessageAsync(receivedMessage);
However, a request to settle a message may fail at the Service Bus side due to various reasons. In some cases, it happens after minutes of processing work. The consumer can decide whether it preserves the state of the work and ignores the same message when delivered a second time or whether to process as a brand new message.
2. Abandon a message
Consider a situation where the consumer fails in the middle of processing a message. But the message needs to be redelivered to the queue.
In that case, the consumer can explicitly ask the broker to release the lock on the message immediately by calling the Abandon API.
The abandoned message can then be picked up by another consumer instantly. That guarantees the at least-once processing of a message.
ServiceBusReceivedMessage receivedMessage = await receiver.ReceiveMessageAsync();// abandon the message, thereby releasing the lock and allowing it //to be received again by this or other receiversawait receiver.AbandonMessageAsync(receivedMessage);
Another way to abandon a message is to do nothing and wait until the lock expires.
3. Dead lettering a message
If a consumer fails to process a message and knows that redelivering the message and retrying the operation will not help, it can reject it.
An example would be a message with a malformed payload. If abandoned, it will be picked up by another consumer, and it can repeat the same. These are called poisonous messages, and they must be rejected as early as possible in the processing.
A consumer can reject a message by calling the DeadLetter
API, which moves it into the dead-letter queue. When rejecting a message, you can set a custom property including a reason code that can be retrieved with the message from the dead-letter queue.
ServiceBusReceivedMessage receivedMessage = await receiver.ReceiveMessageAsync();
// dead-letter the message, thereby preventing the message from //being received again without receiving from the dead letter queue.await receiver.DeadLetterMessageAsync(receivedMessage);
4. Deferring a message
When a consumer receives a message that it’s willing to process, but the processing isn’t currently possible because of special circumstances, it has the option of “deferring” retrieval of the message to a later point. The message remains in the queue, but it’s set aside.
For example, a consumer receives a message, then attempts to write it to a database. But the database is not accessible at the moment. So the consumer decides to “defer” the message so that he can later retrieve it can try again.
ServiceBusReceivedMessage receivedMessage = await receiver.ReceiveMessageAsync();// defer the message, thereby preventing the message from being //received again without using
// the received deferred message API.
await receiver.DeferMessageAsync(receivedMessage);// receive the deferred message by specifying the service set //sequence number of the original
// received message
ServiceBusReceivedMessage deferredMessage = await receiver.ReceiveDeferredMessageAsync(receivedMessage.SequenceNumber);
You can find more about message deferral here.
Renewing the lock duration
If a consumer feels like it needs more time to complete a message, it can ask the broker for a lock renewal.
A consumer holding a lock on a message can renew it by calling methods on the receiver object. As an alternative, you can use the automatic lock-renewal feature to specify the time duration you want to keep getting the lock renewed.
The default value for the lock duration is 30 seconds. However, this value can be changed at the queue level.
Maximum delivery count and the Dead Letter Queue
When consumers repeatedly reject a message or let the lock expire a defined number of times (Max Delivery Count), the message is automatically removed from the queue or subscription and placed into the associated dead-letter queue.
The Service Bus does this on purpose to get rid of the poisonous messages.
Takeaways
Message settlement controls the consumer’s narrative of message consumption. To use the settlement methods we discussed above, you must use a consumer in Peek-Lock mode, which is the default behavior.
When using the Peek-Lock mode, you are doing a non-destructive reading of messages. To delete messages from the queue, you must explicitly tell the broker to do so. Otherwise, the message will remain the same in the queue.
Receive-and-Delete mode is best when you can’t control the behavior of consumers but want a high processing throughput with them.
References
Message transfers, locks, and settlement — Azure Service Bus Documentation