使用ServiceBus SubscriptionClient的RenewLock错误

时间:2018-12-19 14:28:30

标签: c# azure .net-core azureservicebus servicebus

我正在.net Core 2.1中编写一个控制台应用程序,目的是听取ServiceBus中某个Topic上的消息,并使用NEST api处理进入Elasticsearch的新消息(NEST可能与我的问题无关,但希望透明)。

我在ServiceBus中的主题实体称为“测试”,而我的订阅也称为“测试”(完整路径为“测试/订阅/测试”)。

在我的.net Core控制台应用程序中,我具有以下NuGet参考:

<PackageReference Include="Microsoft.Azure.ServiceBus" Version="3.2.1" />
<PackageReference Include="NEST" Version="6.4.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />

在使用.net Standard ServiceBus Api时,我经常遇到一个非常奇怪的问题,我经常收到更新锁定错误:

  

消息处理程序遇到异常   Microsoft.Azure.ServiceBus.MessageLockLostException

我在这里将代码剥离为可重现的示例:

using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Elasticsearch.Net;
using Microsoft.Azure.ServiceBus;
using Nest;
using Newtonsoft.Json;

namespace SampleApp
{
    public class Program
    {

    private static SubscriptionClient _subscriptionClient;
    private static IElasticClient _elasticClient;

    private static string ServiceBusConnectionString = "[connectionString]";
    private static string TopicName = "test";
    private static string SubscriptionName = "test";

    public static void Main(string[] args)
    {
        var elasticsearchSettings = new ConnectionSettings(new SingleNodeConnectionPool(new Uri("http://does.not.exist:9200"))).DefaultIndex("DoesNotExistIndex");
        _elasticClient = new ElasticClient(elasticsearchSettings);

        _subscriptionClient = new SubscriptionClient(ServiceBusConnectionString, TopicName, SubscriptionName);

        // 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 = 1,
            MaxAutoRenewDuration = TimeSpan.FromSeconds(400),
            // 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.
        _subscriptionClient.RegisterMessageHandler(ProcessMessagesAsync, messageHandlerOptions);

        Console.WriteLine("INFO: Process message handler registered, listening for messages");
        Console.Read();
    }

    private static async Task ProcessMessagesAsync(Message message, CancellationToken token)
    {
        // Message received.
        var content = Encoding.UTF8.GetString(message.Body);

        var messageBody = JsonConvert.DeserializeObject<string[]>(content);

        Console.WriteLine($"INFO: Message arrived: {message}");
        Console.WriteLine($"INFO: Message body: \"{string.Join(",", messageBody)}\"");
        try
        {
            var response = _elasticClient.Ping();

            if (!response.IsValid && response.OriginalException != null)
                Console.WriteLine($"ERROR: ElasticSearch could not be reached, error was \"{response.OriginalException.Message}\"");
            else
                Console.WriteLine("INFO: ElasticSearch was contacted successfully");
        }
        catch (Exception e)
        {
            Console.WriteLine("!ERROR!: " + e);
        }

        await _subscriptionClient.CompleteAsync(message.SystemProperties.LockToken);
        Console.WriteLine("INFO: Message completed");
    }

    // Use this handler to examine the exceptions received on the message pump.
    private static Task ExceptionReceivedHandler(ExceptionReceivedEventArgs exceptionReceivedEventArgs)
    {
        Console.WriteLine($"Message handler encountered an exception {exceptionReceivedEventArgs.Exception}: " +
                          $"{exceptionReceivedEventArgs.ExceptionReceivedContext.Action}: " +
                          $"{exceptionReceivedEventArgs.ExceptionReceivedContext.EntityPath}");
        return Task.CompletedTask;
    }

}

此代码与从此处获取的示例几乎相同: https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-how-to-use-topics-subscriptions

我故意“ ping”一个不存在的Elasticsearch实例,以产生可帮助我重现此问题的套接字异常。

我注意到的一件事是,当我创建一个新主题并具有 EnabledPartioning = false 时,不会发生此问题。

以前有人看过吗?似乎是ServiceBus代码本身内部的一个问题。

注意:我尝试使用接收器通过“ ReceiveAsync”读取消息,并且在这种情况下我也遇到此错误。另外,我的测试驱动程序是从.net Framework ServiceBus客户端(可用于分区)移至.net Core版本。

在此先感谢任何指针!

2 个答案:

答案 0 :(得分:3)

在上述情况下,问题归咎于对我的配置的轻微误解。 在Azure中,如果您导航到:

资源组> ServiceBusInstance>主题> testTopic> testSubscription

您可以找到订阅属性。在这里,您将看到发送消息时锁定的持续时间。默认值为60秒,但我将长时间运行的过程延长到最多5分钟,如下所示:

enter image description here

然后在代码中,为Subscription Client连接属性时,我需要确保正确设置MaxAutoRenewDuration属性。

我假设此属性意味着,如果为此定义了30秒,则在内部,订阅客户端将每30秒更新一次锁定,因此,例如,如果您的最大到期时间为5分钟,则锁定将被更新为长在您处理邮件时...

实际上,该属性的实际含义是,他们锁定续订的最大时间将发生在订阅客户端的内部。

因此,如果您将其设置为24小时,例如Timespan.FromHours(24),您的处理过程将需要12个小时,因此将被续签。但是,如果您使用Timespan.FromHours(12)将此时间设置为12小时,并且您的代码运行了24小时,则当您完成该消息时,它将给出一个lockLost异常(因为我在更短的时间内获得了成功!)。

我完成的一件事很容易实现,那就是在运行时从Subscription属性中动态提取LockDuration(我的所有主题可能具有不同的配置)并适当地应用MaxAutoRenewDuration使用这个。

代码示例:

sbNamespace.Topics.GetByName(“test”).Subscriptions.GetByName(“test”).LockDurationInSeconds

注意-我正在使用Azure.Management.Fluent程序包来构建sbNamespace。

希望对其他人有帮助!

答案 1 :(得分:0)

我建议您在订阅MaxAutoRenewDuration = TimeSpan.FromSeconds(xxxx)上设置较高的锁定持续时间,也可以只使用message.RenewLock()

希望有帮助!