很长一段时间后SqlDependency错误

adr*_*anm 8 .net c# sqldependency

我有一个Windows服务使用SqlDependency类监听表中的插入.

它工作好几天但突然停止工作.

在正常情况下,我收到更改事件

e.Type = SqlNotificationType.更改
e.Info = SqlNotificationInfo.插入
e.Source = SqlNotificationSource.数据

如果没有任何变化,我会每3600秒获得一次超时事件

e.Type = SqlNotificationType.更改
e.Info = SqlNotificationInfo.错误
e.Source = SqlNotificationSource.超时

或(不知道为什么有两个不同的超时事件)

e.Type = SqlNotificationType.更改
e.Info = SqlNotificationInfo.未知的
e.Source = SqlNotificationSource.超时

这可以工作一周或更长时间但突然间我不再接收更改事件,而是每60秒接收一次事件

e.Type = SqlNotificationType.更改
e.Info = SqlNotificationInfo.错误
e.Source = SqlNotificationSource.客户

SqlNotificationSource.Client的msdn文档说

发生客户端启动的通知,例如客户端超时或尝试将命令添加到已触发的依赖项.

我认为这意味着在创建依赖项时发生了一个时间.

相同的代码一直在运行,如下所示:

private void CreateDependency() {
    using (var connection = new SqlConnection(_connectionString)) {
        connection.Open();

        var command = new SqlCommand();
        command.CommandText = "SELECT ...";
        command.Connection = connection;

        new SqlDependency(command, "ServiceName", DependencyTimeout).OnChange += OnChange;

        command.ExecuteNonQuery();
    }
}

private void OnChange(object sender, SqlNotificationEventArgs e) {
    ((SqlDependency)sender).OnChange -= OnChange;

    if (e.Type == SqlNotificationType.Change && e.Info == SqlNotificationInfo.Insert) {
        _changeWorkerNotifier.Set(); // AutoResetEvent
    }

    CreateDependency();
}
Run Code Online (Sandbox Code Playgroud)

如果我重新启动我的服务它再次正常工作.

我做了一些调查,发现错误似乎是在服务器上的计划备份之后开始的(每天都会发生).得到另一个应用程序,我大约在同一时间得到错误

从服务器接收结果时发生传输级错误.(提供者:TCP提供者,错误:0 - 信号量超时期限已过期.)

我最初的猜测是,SqlDependency.Start()在计划备份期间创建与服务器的连接,并且永远不会恢复.但msdn文档中的第一行说

当SQL Server连接中发生错误时,SqlDependency侦听器将重新启动.

关于如何解决这个问题的任何想法?
(我当然可以让服务失败并让服务管理器重新启动它.问题是该服务还做了其他需要正常关闭的事情,所以我不能只是从那里做Enviroment.Exit(-1)事件处理程序

adr*_*anm 9

找到原因和解决方案.

首先,我发现了这条线

当SQL Server连接中发生错误时,SqlDependency侦听器将重新启动.

仅出现在.Net 4文档中.

一些测试表明,这不仅仅是文档的变化!使用CLR4运行时不会出现客户端/错误事件.

因此原因是连接错误,它在.Net 4中的SqlDependency内处理,但在早期版本中没有.

在.NET中2-3.5它可能有错误后恢复SqlDependency.Stop()/ SqlDependency.Start().

不太喜欢Stop/Start解决方案,因为如果连接错误无法恢复,我需要逻辑来打破循环.我决定在出现错误时停止服务,让服务管理器重新启动它.(这使问题在事件日志等中可见)

我的处理程序现在如下所示:

private void OnChange(object sender, SqlNotificationEventArgs e) {
    ((SqlDependency)sender).OnChange -= OnChange;

    if (e.Source == SqlNotificationSource.Timeout) {
        // just restart notification
    }
    else if (e.Source != SqlNotificationSource.Data) {
        Logger.Error("Unhandled change notification {0}/{1} ({2})", e.Type, e.Info, e.Source);
        ServiceRunner.ShutDown(true);
    }
    else if (e.Type == SqlNotificationType.Change && e.Info == SqlNotificationInfo.Insert) {
        _changeWorkerNotifier.Set(); // AutoResetEvent
    }
    else {
        Logger.Log("Ignored change notification {0}/{1} ({2})", e.Type, e.Info, e.Source);
    }

    CreateDependency();
}
Run Code Online (Sandbox Code Playgroud)

**编辑**

这是我在启动时调用的代码

private void ClearOldSubscriptions() {
    using (var connection = new SqlConnection(_connectionString))
    using (var command = new SqlCommand()) {
        string sql =
            ////@"DECLARE @UniqueTimeout AS int = 3586; " +
            @"DECLARE @SubscriptionId AS int; " +
            @"DECLARE @Sql AS varchar(max); " +
            @"DECLARE SubscriptionCursor CURSOR LOCAL FAST_FORWARD " +
            @"    FOR " +
            @"        SELECT id " +
            @"        FROM sys.dm_qn_subscriptions " +
            @"      WHERE database_id = DB_ID() " +
            @"            AND timeout = @UniqueTimeout " +
            @"OPEN SubscriptionCursor; " +
            @"FETCH NEXT FROM SubscriptionCursor INTO @SubscriptionId; " +
            @"WHILE @@FETCH_STATUS = 0 " +
            @"BEGIN " +
            @"    SET @Sql = 'KILL QUERY NOTIFICATION SUBSCRIPTION ' + CONVERT(varchar, @SubscriptionId); " +
            @"    EXEC(@Sql); " +
            @" " +
            @"    FETCH NEXT FROM SubscriptionCursor INTO @SubscriptionId; " +
            @"END";

        command.Connection = connection;
        command.CommandType = CommandType.Text;
        command.CommandText = sql;
        command.Parameters.Add("@UniqueTimeout", SqlDbType.Int).Value = DependencyTimeout;

        connection.Open();

        command.ExecuteNonQuery();
    }
}

private void ClearNotificationQueue() {
    using (var connection = new SqlConnection(_connectionString))
    using (var command = new SqlCommand()) {
        string sql = 
            @"DECLARE @Conversation AS uniqueidentifier; " +
            @"DECLARE ConversationCursor CURSOR LOCAL FAST_FORWARD  " +
            @"    FOR " +
            @"        SELECT conversation_handle  " +
            @"        FROM {@Queue} " +
            @"     " +
            @"OPEN ConversationCursor; " +
            @"FETCH NEXT FROM ConversationCursor INTO @Conversation; " +
            @"WHILE @@FETCH_STATUS = 0  " +
            @"BEGIN " +
            @"    END CONVERSATION @Conversation WITH CLEANUP; " +
            @" " +
            @"    FETCH NEXT FROM ConversationCursor INTO @Conversation; " +
            @"END " +
            @"";
        sql = sql.Replace("{@Queue}", NotificationQueue);

        command.Connection = connection;
        command.CommandType = CommandType.Text;
        command.CommandText = sql;

        connection.Open();

        command.ExecuteNonQuery();
    }
}
Run Code Online (Sandbox Code Playgroud)