SQL Service Broker 在数据库阻塞后接收消息时出现严重错误

SQL Service Broker Severe Error Receiving Messages after DB Blocking

我们使用 SQL Service Broker 队列来通知我们的应用程序满足特定条件的新记录被添加到另一个应用程序数据库中的 table。这是通过 after insert 触发 运行 对 inserted 虚拟 table 的 for xml 查询并将任何结果插入特定服务代理队列来实现的。然后我们有一个 Notifier 对象,它从服务代理队列接收消息并为收到的每条消息调用回调。我们从 Service Broker 队列接收的代码如下:

let receiveXmlMessage connection transaction (cancellation: CancellationToken) queueName messageTypeName =
    task {
        let commandTimeout = if cancellation.IsCancellationRequested then 1 else 0 
        let receiveQuery = 
            sprintf """WAITFOR
                        (
                            RECEIVE TOP(1)
                                @message     = CONVERT(xml, message_body),
                                @messageType = message_type_name,
                                @dialogId    = conversation_handle
                            FROM dbo.[%s]
                        ), TIMEOUT 60000;""" (sanitize queueName)
        use receiveCommand = 
            match transaction with
            | Some tx -> new SqlCommand(receiveQuery, connection, tx, CommandTimeout = commandTimeout)
            | None -> new SqlCommand(receiveQuery, connection, CommandTimeout = commandTimeout)
        receiveCommand.Parameters.AddRange([| SqlParameter("@message", SqlDbType.Xml, Direction = ParameterDirection.Output); 
                                              SqlParameter("@messageType", SqlDbType.NVarChar, Direction = ParameterDirection.Output, Size = 256); 
                                              SqlParameter("@dialogId", SqlDbType.UniqueIdentifier, Direction = ParameterDirection.Output); |])
        try
            let! receiveResult = receiveCommand.ExecuteNonQueryAsync(if commandTimeout = 0 then cancellation else CancellationToken.None)
            if receiveResult > 0
            then let messageType = receiveCommand.Parameters.["@messageType"].Value |> unbox<string>
                 let dialogId = receiveCommand.Parameters.["@dialogId"].Value |> unbox<Guid>
                 if messageType = messageTypeName
                 then do! endConversation connection transaction dialogId
                      return receiveCommand.Parameters.["@message"].Value |> unbox<string> |> XDocument.Parse 
                 else return XDocument()
            else return XDocument()
        with | ex -> 
            log.errorxf ex "Failed to receive message from Service Broker Queue %s" queueName
            return! Task.FromException ex
    }

这几个月来都运行良好,处理了数百万条消息,直到几天前,当我们有另一个进程导致我们监视的数据库出现大量阻塞时,我们的 DBA 不得不终止几个数据库会话以缓解争用.自此事件发生后,我们的应用程序在尝试从 Service Broker 队列接收时遇到以下错误:

2018-01-11 07:50:27.183-05:00 [31] ERROR - Failed to receive message from Service Broker Queue Notifier_Queue
System.Data.SqlClient.SqlException (0x80131904): A severe error occurred on the current command.  The results, if any, should be discarded.
Operation cancelled by user.
   at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
   at System.Data.SqlClient.SqlCommand.CompleteAsyncExecuteReader()
   at System.Data.SqlClient.SqlCommand.EndExecuteNonQueryInternal(IAsyncResult asyncResult)
   at System.Data.SqlClient.SqlCommand.EndExecuteNonQueryAsync(IAsyncResult asyncResult)
   at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Application.Common.Sql.ServiceBroker.receiveXmlMessage@257-3.Invoke(Unit unitVar0)
   at Application.Common.TaskBuilder.tryWith[a](FSharpFunc`2 step, FSharpFunc`2 catch)

新消息已成功添加到队列中,我们可以使用 SSMS 从同一队列接收消息,甚至可以作为与应用程序相同的用户使用 F# 交互式会话 运行ning。它似乎只是我们的应用程序受到影响,但它似乎确实影响了我们应用程序在不同服务器上的所有实例,只要它们连接到这个特定的数据库。我们已经尝试重新启动应用程序和 SQL 服务器,并且我们已经尝试 运行 宁 ALTER DATABASE ... SET NEW_BROKER WITH ROLLBACK IMMEDIATE。我们尝试过的任何事情都没有改变,我们最终还是遇到了同样的异常,并且我们有数十万个对话保持 CONVERSING 状态,因为我们调用 END CONVERSATION 的代码仅在之后被调用成功接收消息。

我们的 SQL Service Broker 队列设置为模拟独白模式,如 this blog post 中所述。

我们如何诊断我们的应用程序从 SQL 服务器返回的这个非特定异常的原因?当问题首次出现时,我们是否可以尝试诊断 and/or 更正我们的应用程序和 SQL Service Broker 之间的任何更改?

当我们尝试从 Service Broker 队列接收时,我们终于弄清楚了导致此错误的原因。事实证明,传递给我们的 receiveXmlMessage 函数的 CancellationToken 被我们应用程序中的其他逻辑取消了,这些逻辑监视 conversing 对话的数量并尝试重新创建我们的 Notifier 如果 conversing 对话的数量超过某个阈值并且最近关闭的对话早于另一个阈值,则对象。由于最近关闭对话年龄的逻辑错误,实际上只有 conversing 个对话的数量被用于重置 Notifier,并且当上周发生数据库阻塞时,超过累积了 150,000 conversing 次对话。这导致我们的应用程序在尝试从 Service Broker 接收消息时不断取消 CancellationToken。一旦我们关闭了我们的应用程序,清理了所有 conversing 对话,并修复了最后一个关闭对话的日期数学中的错误,错误就停止了。

对于遇到此消息的任何人,请注意:

A severe error occurred on the current command.  The results, if any, should be discarded.

这可能是 CancellationToken 传递给 ExecuteNonQueryAsync/ExecuteReaderAsync/ExecuteScalarAsync/ExecuteXmlReaderAsync 方法的结果 SqlCommand 在方法执行时被取消。