3

I'm using deferred messages and manage to retrieve them and process them the way I want. Now I need a way to delete them (normal messages can be "Completed") so they don't stay forever but I can't find out how.

Here's how I retrieve the message:

var message = await ServiceBusReceiver.ReceiveDeferredMessageAsync(
    deferredMessage.SequenceNumber,
    cancellationToken
);

And this is what I tried first to delete them

await ServiceBusReceiver.CompleteMessageAsync(message, cancellationToken);

Which failed with an error claiming the lock was not valid so I tried

await ServiceBusReceiver.RenewMessageLockAsync(message, cancellationToken);
await ServiceBusReceiver.CompleteMessageAsync(message, cancellationToken);

But the error persist.

EDIT:

I created a demo:

enter image description here

using Azure.Identity;
using Azure.Messaging.ServiceBus;

const string ToDefer = nameof(ToDefer);
const string UnDefer = nameof(UnDefer);

const string TopicName = "demo-deferred-sj";
const string ServiceBusNamespace = "#######";
const string SubscriptionName = "all";

var oneSecondMoreThanLockDuration = TimeSpan.FromSeconds(6);

var userName = System.Environment.GetEnvironmentVariable("USERNAME");

var serviceBusClient = new ServiceBusClient(
    $"{ServiceBusNamespace}.servicebus.windows.net",
    new VisualStudioCredential()
);

var sender = serviceBusClient.CreateSender(TopicName);
var processor = serviceBusClient.CreateProcessor(TopicName, SubscriptionName, new ServiceBusProcessorOptions
{
    AutoCompleteMessages = false
});
var receiver = serviceBusClient.CreateReceiver(TopicName, SubscriptionName);

async Task SendMessagesAsync(string kind, string param = "", DateTimeOffset? scheduleTime = null)
{
    var messageId = $"{kind}|{userName}|{param}|{Guid.NewGuid()}";
    Console.WriteLine($"Sending {messageId}"
        + (scheduleTime.HasValue ? $" scheduled for {scheduleTime}" : "")
    );
    var serviceBusMessage = new ServiceBusMessage()
    {
        MessageId = messageId
    };
    if (scheduleTime.HasValue)
    {
        serviceBusMessage.ScheduledEnqueueTime = scheduleTime.Value;
    }
    await sender.SendMessageAsync(serviceBusMessage);
}

async Task ProcessMessageAsync(ProcessMessageEventArgs args)
{
    Console.WriteLine($"Handling {args.Message.MessageId} ");

    var messageParts = args.Message.MessageId.Split('|');
    var kind = messageParts[0];
    var user = messageParts[1];
    var param = messageParts[2];

    if (user != userName)
    {
        Console.WriteLine($"Caution handling message of another user: {user}");
    }

    switch (kind)
    {
        case ToDefer:
            await args.DeferMessageAsync(args.Message);
            var scheduleTime = args.Message.EnqueuedTime + oneSecondMoreThanLockDuration;
            await SendMessagesAsync(UnDefer, args.Message.SequenceNumber.ToString(), scheduleTime);
            break;
        case UnDefer:
            var deferredMessage = await receiver.ReceiveDeferredMessageAsync(long.Parse(param));
            Console.WriteLine($"Deferd message {deferredMessage.MessageId} processed");
            await args.CompleteMessageAsync(args.Message);
            try
            {
                // THIS WOULD DELETE THE MESSAGE IF THE LOCK WAS STILL ON
                await receiver.CompleteMessageAsync(deferredMessage);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Ignoring {ex.Message}");
            }

            break;
    }
}

processor.ProcessMessageAsync += ProcessMessageAsync;
processor.ProcessErrorAsync += eventArgs =>
{
    Console.WriteLine(eventArgs.Exception.Message);

    return Task.CompletedTask;
};

var cancellationTokenSource = new CancellationTokenSource();
await processor.StartProcessingAsync(cancellationTokenSource.Token);

await SendMessagesAsync(ToDefer);

Console.ReadKey();
cancellationTokenSource.Cancel();
Jason Evans
  • 28,906
  • 14
  • 90
  • 154
serge_portima
  • 111
  • 1
  • 10

2 Answers2

0

According to this SO discussion, Deferred messages can also be returned by peeking messages from a queue or subscription. You may acquire the sequence numbers of the deferred messages this way, and then process or delete them.
Please refer the above given link for more information.

  • I'm not having issue losing track of deferred message, I have an issue deleting them. All the answers I found on SO don't tackle that point. – serge_portima Feb 11 '22 at 07:49
  • What's ridiculous is there's no way to undefer a message. So if you defer it, and then receive it by sequence number, you're only option is to Complete it. You cannot Abandon it to return it to a queue. This renders deferral useless. There's no ability to return a message to the queue if lengthy reprocessing fails. – Triynko May 25 '22 at 15:38
0

I was able to execute your demo code and process all messages as expected. I was also able to create a ServiceBusException by ensuring the CompleteMessageAsync was called after the 5 second lock duration.

The ServiceBusException message was:

Ignoring The lock supplied is invalid. Either the lock expired, or the message has already been removed from the queue, or was received by a different receiver instance. (MessageLockLost)

Is it possible that your subscription's lock duration is less than the time it takes to process the message? I would try increasing the lock duration to ensure the processing completes before the lock expires.

You can execute a method like this to process any lingering deferred messages, the ones you no longer have SequenceIds for.

async Task ProcessDeferred(int maximumMessageToSearch)
{
    var messages = await receiver.PeekMessagesAsync(maximumMessageToSearch);

    if (messages != null && messages.Any())
    {
        foreach (var deferred in messages.Where(m => m.State == ServiceBusMessageState.Deferred))
        {
            Console.WriteLine($"Processing deferred message {deferred.MessageId}");
            var message = await receiver.ReceiveDeferredMessageAsync(deferred.SequenceNumber);

            // { add you processing here }

            await receiver.CompleteMessageAsync(message);

            Console.WriteLine($"Deferred message {deferred.MessageId} has been processed.");
        }
    }
}

Not being sure how closely your demo code matches your actual code, if you are using a separate time delayed message to cause trigger the processing of the deferred message, I think there is a better pattern to accomplish your goals but I'll address that thought after this - modify the handling of the Undefer portion in your switch statement to update the trigger message AFTER you know if the deferred message was processed successfully. Something like this:

switch (kind)
{
    case ToDefer:
        await args.DeferMessageAsync(args.Message);
        var scheduleTime = args.Message.EnqueuedTime + oneSecondMoreThanLockDuration;
        await SendMessagesAsync(UnDefer, args.Message.SequenceNumber.ToString(), scheduleTime);
        break;
    case UnDefer:
        var deferredMessage = await receiver.ReceiveDeferredMessageAsync(long.Parse(param));
        Console.WriteLine($"Deferd message {deferredMessage.MessageId} processed");
        try
        {
            // THIS WOULD DELETE THE MESSAGE IF THE LOCK WAS STILL ON
            await receiver.CompleteMessageAsync(deferredMessage);
            await args.CompleteMessageAsync(args.Message);
        }
        catch (Exception ex)
        {
            await args.AbandonMessageAsync(args.Message);
            Console.WriteLine($"Ignoring {ex.Message}");
        }

        break;
}

Now back to a couple better ways of handling deferring messages; instead of using a separate message to trigger the processing of a deferred message (as in the demo). I'll list a couple options that I think are better, first would be to send a copy of the original message that has delayed processing and marking the original as completed. This avoids the need to get deferred messages by SequenceId. The other option would be to have something similar to the ProcessDeferred method (above) that is triggered by a timer and possibly check the message timestamps to determine if it should wait to process until next time.

Hopefully this helps.

Larry Dukek
  • 2,179
  • 15
  • 16
  • Thanks for sharing your opinion, I would like to avoid resending a copy of the message as I would lose the "DeliveryCount" value. I tried calling the method "ProcessDeferred" you wrote, but it has the same issue, It doesn't allow to Complete the message due to a lock issue (if you wait the original lock to expire before running the method). – serge_portima Feb 21 '22 at 07:55
  • Have you measured the time it takes to process the message and compared it to your lock duration? Once the lock duration expires, you can not renew it, it must be renewed before it expires. It is possible to keep you lock from expiring, it would need to be scheduled to renew before the lock expires and would need to run in a separate thread from the processing thread. It is important to ensure that the renewal thread is terminated when the message is no longer being processed, no matter how the processing thread exited. – Larry Dukek Feb 21 '22 at 15:01
  • Does this occur on every message that is sent through the system? I ask this because the receiver can have prefetch enabled. The lock timer starts when it is fetched, whether the processing has started on the message is irrelevant. I just want to clear that up to ensure you aren't jumping down the wrong rabbit hole. – Larry Dukek Feb 21 '22 at 15:41
  • When you stated that you executed the "ProcessDeferred" method, did you add the message processing logic or did you execute it as it is written above? If you still got the lock exception without any processing... we need to look at how everything is configured. Or I would have to ask if you are debugging with breakpoints set as sitting on breakpoints does not pause the lock timer. – Larry Dukek Feb 21 '22 at 15:57
  • the process time is fast enough (a few Milliseconds). I Also get the issue with ProcessDeferred. All it take is to wait for any previously acquired locks to expire before running the method. – serge_portima Feb 22 '22 at 07:57
  • Apparently you cannot undefer a message. Abandoning the message after receiving the deferred message fails. Unbelievable. This makes it impossible to reprocess some messages. Without deferral, you cannot skip over messages. And if you defer them, you can't return them to the queue after receiving/processing all other messages. So the messages are just stuck in a deferred state, and your only option is to complete and lose them, even though you're not ready to reprocess those messages. So then you have to transfer them somewhere else and cannot just leave them in a DL queue. Bad design. – Triynko May 25 '22 at 15:35