I came across an issue related to error handling with some service broker code I have that implements conversation pools (http://rusanu.com/2007/05/03/recycling-conversations/). In my development environment I broke permissions on my target service, and when I tried to send a message to the target I got an error back at my initiator (as expected). In my initiator queue I have the following code:
IF @messageType = N'http://schemas.microsoft.com/SQL/ServiceBroker/Error' BEGIN END CONVERSATION @handle; -- Logging code here. Store error message and contents of transmission_queue. END
So, the conversation was ended in response to the error and everything seemed fine. I then went to send another message and got an error. The error happened on the SEND command and stated that the conversation did not exist. This makes sense because the conversation still exists in the tracking table but was ended in response to the error. At first I thought: why not also DELETE the record in the activated procedure in response to the error? But this presents a problem: the sending process and the activated proc can deadlock due to the locking order; the activated proc will "RECEIVE" => "DELETE on table" and the sending process will "SELECT(WITH UPDLOCK on table)" => "SEND". I had the same issue with using dialog timers to expire conversations and moved the conversation expiration to the sending process because of this unavoidable locking order. Also, there would be issues if the listening process went down (the conversation is still in an ER state and sending will still fail).
So, another approach I started to entertain was to leave the activation process alone, and have the sending process detect when these errors would occur and try with a new conversation. I can't simply wrap the SEND process in a try/catch because I need to send from triggers, and trigger execution will fail even with the try/catch. This means I would need to be able to "detect" when the conversation is mismatched/invalid and act accordingly before I try to send. So I think querying the sys.conversation_endpoints might be an option. The code looks like:
-- Seek an eligible conversation in [ServiceBrokerConversations] -- We will hold an UPDLOCK on the composite primary key SELECT @handle = Handle , @conversationCreateDate = CreateDate FROM [ServiceBrokerConversations] WITH (UPDLOCK) WHERE ProcessId = @@SPID AND FromService = @fromService AND ToService = @toService AND OnContract = @onContract; -- Check the current state of the conversation to make sure it -- is valid for sending messages on. IF @handle IS NOT NULL BEGIN DECLARE @conversationState CHAR(2) SELECT @conversationState = [state] FROM sys.conversation_endpoints AS ce WITH(NOLOCK) WHERE ce.[conversation_handle] = @handle -- If NULL then no conversation exists, DELETE the record and create a new one. -- If the conversation is in an error state the queue will receive a 'http://schemas.microsoft.com/SQL/ServiceBroker/Error' message and it should handle it by ending the conversation. -- In both these cases we should just DELETE the handle from the table and create a new one IF @conversationState IS NULL OR @conversationState IN ('CD', 'DI', 'ER') BEGIN DELETE FROM ServiceBrokerConversations WHERE Handle = @handle SET @handle = NULL; END END
This seems like it will solve my issue: If the conversation is in a state that will fail sending create a new conversation and send the message on that. My question is: Is querying the sys.conversation_endpoints from within a trigger and other "business critical" code dangerous or bad practice? Are there any "gotchas"? I checked the performance and it seems like a fast, low cost, light read query. I just want to make sure that that there are no known issues with using it in high volume production code that will come back to bite me.
EDIT: For reference, the full sending process would look like:
CREATE PROCEDURE [dbo].[usp_SendMessage] ( @fromService SYSNAME, @toService SYSNAME, @onContract SYSNAME, @messageType SYSNAME, @conversationTimeout INT, @xmlPayload XML=NULL ) AS BEGIN SET NOCOUNT ON DECLARE @SBDialog UNIQUEIDENTIFIER DECLARE @Message XML DECLARE @counter INT DECLARE @error INT DECLARE @conversationCreateDate DATETIME2 DECLARE @handle UNIQUEIDENTIFIER; SET @counter = 1 WHILE (1=1) BEGIN SET @handle = NULL -- Seek an eligible conversation in [ServiceBrokerConversations] -- We will hold an UPDLOCK on the composite primary key SELECT @handle = Handle , @conversationCreateDate = CreateDate FROM [ServiceBrokerConversations] WITH (UPDLOCK) WHERE ProcessId = @@SPID AND FromService = @fromService AND ToService = @toService AND OnContract = @onContract; -- If the conversation is expired then remove it and start a new one IF DATEADD(MINUTE, @conversationTimeout, @conversationCreateDate) < SYSDATETIME() BEGIN EXEC dbo.usp_RemoveConversation @handle SET @handle = NULL END -- Check the current state of the conversation to make sure it -- is valid for sending messages on. IF @handle IS NOT NULL BEGIN DECLARE @conversationState CHAR(2) SELECT @conversationState = [state] FROM sys.conversation_endpoints AS ce WITH(NOLOCK) WHERE ce.[conversation_handle] = @handle -- If NULL then no conversation exists. -- If error the queue will receive a 'http://schemas.microsoft.com/SQL/ServiceBroker/Error' message and it should handle it by ending the conversation. -- Either way we will just DELETE the handle from the table since this should be handled IF @conversationState IS NULL OR @conversationState IN ('CD', 'DI', 'ER') BEGIN DELETE FROM ServiceBrokerConversations WHERE Handle = @handle SET @handle = NULL; END END IF @handle IS NULL BEGIN -- Need to start a new conversation for the current @Id BEGIN DIALOG CONVERSATION @handle FROM SERVICE @fromService TO SERVICE @toService ON CONTRACT @onContract WITH ENCRYPTION = OFF; INSERT INTO [ServiceBrokerConversations] (Identifier, IdentifierType, FromService, ToService, OnContract, Handle) VALUES (@identifier, @identifierType, @fromService, @toService, @onContract, @handle); END; ;SEND ON CONVERSATION @handle MESSAGE TYPE @messageType (@xmlPayload); SELECT @error = @@ERROR; IF @error = 0 BEGIN -- Successful send, just exit the loop BREAK; END SELECT @counter = @counter+1; IF @counter > 10 BEGIN -- We failed 10 times in a row, something must be broken RAISERROR ( N'Failed to SEND on a conversation for more than 10 times. Error %i.' , 16, 1, @error) WITH LOG; BREAK; END -- Delete the associated conversation from the table and try again DELETE FROM [ServiceBrokerConversations] WHERE Handle = @handle; SET @handle = NULL; END END
Thanks,
Ryan