问题
I am using SQL Server Broker in 2008 R2 in a C# application and am trying to handle the case where SQL Server has detected a poison message and has disabled the target queue.
When this case occurs, an SqlException is thrown when I'm trying to receive a message. At that point, the SqlTransaction I'm using seems to no longer be committable.
I'll use this tutorial to demonstrate along with my C# code.
First use the T-SQL code from the tutorial to create the necessary service broker objects and send a message so that it's sitting in the Target queue.
CREATE MESSAGE TYPE
[//AWDB/1DBSample/RequestMessage]
VALIDATION = WELL_FORMED_XML;
CREATE MESSAGE TYPE
[//AWDB/1DBSample/ReplyMessage]
VALIDATION = WELL_FORMED_XML;
GO
CREATE CONTRACT [//AWDB/1DBSample/SampleContract]
([//AWDB/1DBSample/RequestMessage]
SENT BY INITIATOR,
[//AWDB/1DBSample/ReplyMessage]
SENT BY TARGET
);
GO
CREATE QUEUE TargetQueue1DB;
CREATE SERVICE
[//AWDB/1DBSample/TargetService]
ON QUEUE TargetQueue1DB
([//AWDB/1DBSample/SampleContract]);
GO
CREATE QUEUE InitiatorQueue1DB;
CREATE SERVICE
[//AWDB/1DBSample/InitiatorService]
ON QUEUE InitiatorQueue1DB;
GO
DECLARE @InitDlgHandle UNIQUEIDENTIFIER;
DECLARE @RequestMsg NVARCHAR(100);
BEGIN TRANSACTION;
BEGIN DIALOG @InitDlgHandle
FROM SERVICE
[//AWDB/1DBSample/InitiatorService]
TO SERVICE
N'//AWDB/1DBSample/TargetService'
ON CONTRACT
[//AWDB/1DBSample/SampleContract]
WITH
ENCRYPTION = OFF;
SELECT @RequestMsg =
N'<RequestMsg>Message for Target service.</RequestMsg>';
SEND ON CONVERSATION @InitDlgHandle
MESSAGE TYPE
[//AWDB/1DBSample/RequestMessage]
(@RequestMsg);
SELECT @RequestMsg AS SentRequestMsg;
COMMIT TRANSACTION;
GO
Next run this C# code which is a console application.
using System.Data.SqlClient;
namespace ServerConsoleApplication
{
class Program
{
static SqlConnection conn = null;
static void Main(string[] args)
{
conn = new SqlConnection("connection string");
conn.Open();
Receive(); // 1
Receive(); // 2
Receive(); // 3
Receive(); // 4
Receive(); // 5
Receive(); // 6 - Poison Message exception invoked
conn.Close();
}
static void Receive()
{
using (SqlTransaction tran = conn.BeginTransaction())
{
try
{
using (SqlCommand waitCommand = conn.CreateCommand())
{
waitCommand.Transaction = tran;
waitCommand.CommandText = string.Format("WAITFOR (RECEIVE TOP (1) conversation_handle, convert(xml,message_body) FROM TargetQueue1DB), TIMEOUT 1000");
using (SqlDataReader reader = waitCommand.ExecuteReader())
{
}
}
// Rollback on purpose to cause the poison message
tran.Rollback();
}
catch (SqlException ex)
{
if (ex.Number == 9617)
{
// Re-Enable the queue
using (SqlCommand enableCmd = conn.CreateCommand())
{
enableCmd.Transaction = tran;
enableCmd.CommandText = string.Format(@"ALTER QUEUE TargetQueue1DB WITH STATUS = ON");
enableCmd.ExecuteNonQuery();
}
System.Data.SqlTypes.SqlGuid handle = System.Data.SqlTypes.SqlGuid.Null;
// Pull the poison message off the queue
using (SqlCommand waitCommand = conn.CreateCommand())
{
waitCommand.Transaction = tran;
waitCommand.CommandText = string.Format("WAITFOR (RECEIVE TOP (1) conversation_handle, convert(xml,message_body) FROM TargetQueue1DB), TIMEOUT 1000");
using (SqlDataReader reader = waitCommand.ExecuteReader())
{
while (reader.Read())
{
handle = reader.GetSqlGuid(0);
}
}
}
// End the conversation just for clean up
using (SqlCommand endCmd = conn.CreateCommand())
{
endCmd.Transaction = tran;
endCmd.CommandText = "End Conversation @handle";
endCmd.Parameters.Add("@handle", System.Data.SqlDbType.UniqueIdentifier);
endCmd.Parameters["@handle"].Value = handle;
endCmd.ExecuteNonQuery();
}
// Commit the transaction so the message is removed from queue.
tran.Commit();
}
}
}
}
}
}
The code above is just a demonstration of the behavior. Of course you wouldn't receive and call Rollback like this normally.
The Receive method receives the message and calls Rollback on the transaction to stimulate the poison message behavior. On the sixth call to Receive an SQLException is thrown because the queue is disabled as expected.
At this point I'd like to re-enable the queue, pull the poison message off and end the conversation (ending is not necessary). This all works, but then I Commit the transaction because I really want that poison message off the queue.
Result An exception is thrown on the Commit call stating
This SqlTransaction has completed; it is no longer usable.
How did this transaction get completed without a Rollback or Commit being called on that 6th invocation of Receive?
Also, how did the message in TargetQueue1DB get removed? I thought the receive wouldn't remove the message off the queue unless it was in a transaction that got committed. However, if you look at the TargetQueue1DB before that commit is called, the queue is empty.
If you modify the code a little so that the waitCommand is in scope when the SqlException is caught you will see the Connection and Transaction properties of the waitCommand instance have been set to null. Which is strange behavior to me.
回答1:
The client state of your SqlTransaction does not necessarily reflect the transaction state on the server. Consider if the exception you catch is 1205, deadlock. In such a case the transaction had already been rolled back on the server before the exception was raised in the server, even though you have a SqlTransaction object in your current frame that was neither committed nor rolled back.
In your catch block you need to dispose of your current transaction object and start a new one to do your error handling logic.
The message got removed because you executed your catch handling logic w/o an actual transaction being started on the server. You used the expired tran
object that is no longer relevant. Your RECEIVE got committed immediately (no surrounding transaction at all).
来源:https://stackoverflow.com/questions/25515101/sql-server-broker-transaction-completed-on-poison-message-exception