Quantcast
Channel: SQL Service Broker forum
Viewing all articles
Browse latest Browse all 461

Using sys.conversation_endpoints to validate a conversation before sending

$
0
0

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


Viewing all articles
Browse latest Browse all 461

Trending Articles