Welcome to MSDN Blogs Sign in | Join | Help

Recycling Conversations

In my previous post http://blogs.msdn.com/remusrusanu/archive/2007/04/24/reusing-conversations.aspx I promised I’ll follow up with a solution to the question about how to end the conversations that are reused for the data-push scenario (logging, auditing, ETL for DW etc).

First of all, why end them in the first place? The answer is to minimize exposure in case of problems. If a problem happens and a dialog has to be destroyed (i.e. errored), then we want to minimize the potential number of messages that we have to re-process or that are potential lost. But wait a minute, how come we’re talking about message loss, doesn’t Service Broker guarantee delivery? The truth is that no system can ever guarantee delivery under all conditions. Human error can happen resulting in misconfiguration, applications have bugs that can send invalid messages (e.g. fail XML validation); hardware can be catastrophically lost w/o any possibility to restore a database/host and so on and so forth. Reusing a single dialog to send all your messages (or one per @@spid) is like putting all your eggs in one basket. If that dialog was trailing millions of messages behind, all those messages have to be reprocessed or are lost.

Another reason to recycle the dialogs is to allow RETENTION on queues. Enabling RETENTION keeps a copy of every message sent to/from that queue for the duration of the dialog. If a dialog is kept open for unlimited duration, the queue will continuously grow.

Also we want to recycle dialogs to deal with load balancing scenarios, since a dialog is sticky to one service instance we would like to periodically start new dialogs so they ‘stick’ to new instances of from the available target service instances.

So how should we end this dialogs? My recommendation is to end them based on time. Choose a time for how long a dialog should be used, depending on the expected number of messages traffic. Then you can use the Service Broker conversation timers to end the dialog when it’s time is done. I also believe that the timer should be handled by the initiator, but the conversation should be ended by the target. Given that the pattern we’re talking about (one way data push) involves sending only messages from initiator to target, the initiator is not allowed to end the conversation because that will create a fire-and-forget message pattern, a very undesirable message exchange pattern, see http://blogs.msdn.com/remusrusanu/archive/2006/04/06/570578.aspx. My recommendation is to have a special message in the contract used by the two services that informs that target that the initiator is done sending on this dialog. Lets call this message [EndOfStream]. The target would respond to this message by ending its side of the conversation, thus sending an EndDialog message to the initiator. The initiator responds to this message by ending its side (issuing the END CONVERSATION verb).

To implement what I described above we would need to modify the original example from http://blogs.msdn.com/remusrusanu/archive/2007/04/24/reusing-conversations.aspx to create a conversation timer when the dialog is created and to add an activated procedure that handles the timer message, as well as the EndDialog and Error messages. One interesting issue is how to handle the timer message. After sending the [EndOfStream] message, the initiator should not send any more messages on that dialog. This is easily achieved by removing the dialog handle from the [SessionConversations] table. The next invocation of usp_Send will create a new one, just as desired. But there is a race condition there, an usp_Send procedure might be executing already and trying to send a message on that conversation. Even though the dialog was removed from the [SessionConversations] table, the concurrent usp_Send will successfully send on the dialog that just sent an [EndOfStream] message. Since the target responds to the [EndOfstream] message by ending the conversation, the message sent is most likely lost as the target will ignore it! To solve this situation, the usp_Send procedure must guarantee the stability of the conversation it looked up in the [SessionConversations] table. So the SELECT from the [SessionConversations] table has to be REPEATABLE, and this can be achieved with an appropriate query hint (HOLDLOCK).

But by doing this we introduce a new problem: the activated procedure handling the timer message and the usp_Send procedure might deadlock each other! One will hold a lock on the conversation and try to update the [SessionConversations] table, while the other will hold the lock on the [SessionConversations] table and try to acquire the lock on the conversation: guaranteed deadlock. To solve this problem, will solve it as most deadlock problems are solved: acquire the locks in the same order in both procedures. Since the usp_Send must acquire the locks in the order row-in-[SessionConversations]-table-first-conversation-next, the activated procedure must do the same. That is: delete the row from [SessionConversations] first, send the [EndOfStream] message next.

 

So here is the code, the modified usp_Send and the initiator side activated procedur. I highlighted the changes in usp_Send.

 

 

-- This table associates the current connection (@@SPID)

-- with a conversation. The association key also

-- contains the conversation parameteres:

-- from service, to service, contract used

--

CREATE TABLE [SessionConversations] (

      SPID INT NOT NULL,

      FromService SYSNAME NOT NULL,

      ToService SYSNAME NOT NULL,

      OnContract SYSNAME NOT NULL,

      Handle UNIQUEIDENTIFIER NOT NULL,

      PRIMARY KEY (SPID, FromService, ToService, OnContract),

      UNIQUE (Handle));

GO

 

-- SEND procedure. Will lookup to reuse an existing conversation

-- or start a new in case no conversation exists or the conversation

-- cannot be used

--

CREATE PROCEDURE [usp_Send] (

      @fromService SYSNAME,

      @toService SYSNAME,

      @onContract SYSNAME,

      @messageType SYSNAME,

      @messageBody VARBINARY(MAX))

AS

BEGIN

      SET NOCOUNT ON;

      DECLARE @handle UNIQUEIDENTIFIER;

      DECLARE @counter INT;

      DECLARE @error INT;

 

      SELECT @counter = 1;

     

      BEGIN TRANSACTION;

      -- Will need a loop to retry in case the conversation is

      -- in a state that does not allow transmission

      --

      WHILE (1=1)

      BEGIN

            -- Seek an eligible conversation in [SessionConversations]

            --

            SELECT @handle = Handle

                  FROM [SessionConversations] WITH (HOLDLOCK)

                  WHERE SPID = @@SPID

                        AND FromService = @fromService

                        AND ToService = @toService

                        AND OnContract = @OnContract;

            IF @handle IS NULL

            BEGIN

                  -- Need to start a new conversation for the current @@spid

                  --

                  BEGIN DIALOG CONVERSATION @handle

                        FROM SERVICE @fromService

                        TO SERVICE @toService

                        ON CONTRACT @onContract

                        WITH ENCRYPTION = OFF;

                  -- Set an one hour timer on the conversation

                  --

                  BEGIN CONVERSATION TIMER (@handle) TIMEOUT = 3600;

                  INSERT INTO [SessionConversations]

                        (SPID, FromService, ToService, OnContract, Handle)

                        VALUES

                        (@@SPID, @fromService, @toService, @onContract, @handle);

            END;

            -- Attempt to SEND on the associated conversation

            --

            SEND ON CONVERSATION @handle

                  MESSAGE TYPE @messageType

                  (@messageBody);

            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 [SessionConversations]

                  WHERE Handle = @handle;

            SELECT @handle = NULL;

      END  

      COMMIT;

END

GO

 

CREATE PROCEDURE usp_SenderActivation

AS

BEGIN

DECLARE @handle UNIQUEIDENTIFIER;

DECLARE @messageTypeName SYSNAME;

DECLARE @messageBody VARBINARY(MAX);

BEGIN TRANSACTION;

      RECEIVE TOP(1)

            @handle = conversation_handle,

            @messageTypeName = message_type_name,

            @messageBody = message_body

      FROM [sender_queue];

      IF @handle IS NOT NULL

      BEGIN

            -- Delete the message from the [SessionConversations] table

            -- before sending the [EndOfStream] message. The order is

            -- important to avoid deadlocks. Strictly speaking, we should

            -- only delete if the message type is timer or error, but is

            -- simpler and safer to just delete always

            --

            DELETE FROM [SessionConversations]

                  WHERE [Handle] = @handle;

 

            IF @messageTypeName = N'http://schemas.microsoft.com/SQL/ServiceBroker/DialogTimer'

            BEGIN

                  SEND ON CONVERSATION @handle MESSAGE TYPE [EndOfStream];

            END

            ELSE IF @messageTypeName = N'http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog'

            BEGIN

                  END CONVERSATION @handle;

            END

            ELSE IF @messageTypeName = N'http://schemas.microsoft.com/SQL/ServiceBroker/Error'

            BEGIN

                  END CONVERSATION @handle;

                  -- Insert your error handling here. Could send a notification or

                  -- store the error in a table for further inspection

                  -- We're gonna log the error into the ERRORLOG and Eventvwr.exe

                  --

                  DECLARE @error INT;

                  DECLARE @description NVARCHAR(4000);

                  WITH XMLNAMESPACES ('http://schemas.microsoft.com/SQL/ServiceBroker/Error' AS ssb)

                  SELECT

                        @error = CAST(@messageBody AS XML).value('(//ssb:Error/ssb:Code)[1]', 'INT'),

                        @description = CAST(@messageBody AS XML).value('(//ssb:Error/ssb:Description)[1]', 'NVARCHAR(4000)')

                  RAISERROR(N'Received error Code:%i Description:''%s''', 16, 1, @error, @description) WITH LOG;

            END;

      END  

COMMIT;

END

GO

 

-- attach the uspSenderActivation procedure with the sender's queue

--

ALTER QUEUE [sender_queue]

      WITH ACTIVATION (

            STATUS = ON,

            MAX_QUEUE_READERS = 1,

            PROCEDURE_NAME = [usp_SenderActivation],

            EXECUTE AS OWNER);

GO

Published Wednesday, May 02, 2007 11:32 PM by RemusRusanu
Filed under:

Comments

# re: Recycling Conversations

Hi,

Great article! I am using SQL Service Broker since a couple of weeks and I am already loving it :) ... Your article makes it even more lovable! But:

Correct me if I'm wrong, it seems you only handle the messages from the conversation itself (error, end, timer) in the example above (should another IF block work out?).

Besides, you always delete the conversation from the conversations table. Why do you do that? It seems to me there is no reusing when you do so? Is it not possible to keep the conversation alive unless (!) there is one of the handled situations?

I hope to hear and learn more from you soon!

Greetings,

Bert Loedeman (The Netherlands)

Thursday, July 05, 2007 10:03 AM by aa.loedeman

# re: Recycling Conversations

Because this is a one-way data push scenario, there is no message ever coming back from the target to the initiator. This is why I don't need to deal with anything other than the system messages.

The removal from the table is done always. Conversations are reused until one of the following happens:

- the timer fires

- conversation hits an error

- conversation is ended intentionally by peer (this would be a bug on peer's behavior)

for any of these three events there will be a message triggering the initiator's side activated procedure.

Thursday, July 05, 2007 12:45 PM by RemusRusanu

# re: Recycling Conversations

Thanks for your answer. Okay. I think I understand. Just for certainty:

Does the activated procedure in the code above get all messages or only the three handled messages? In our company's situation it's a little confusing, because we only have one service yet and one queue.

In other words: where are the real messages to push going? Is there another queue with an activated procedure? If so, does that one not send an 'END CONVERSATION' command?

Greetings, Bert

Friday, July 06, 2007 4:31 AM by aa.loedeman

# re: Recycling Conversations

You shold split the conversations into two services on two queues. While is true that is possible that yo can make it work on only one queue, you don't have any advantage and only disadvantages (like more complex code)

Friday, July 06, 2007 12:14 PM by RemusRusanu

# re: Recycling Conversations

Okay! Thanks for your answer. I'm late, but had a wonderful vacation on Tenerife (yes, the island currently burning).

I worked it out like you mentioned above and it works fine. Thanks a lot for your efforts showing the rest of the world how to use SQL Service Broker the real performing way :) ...

Greetings, Bert

Wednesday, August 01, 2007 10:27 AM by aa.loedeman

# re: Recycling Conversations

I'm creating a service broker app using your recycling conversations example but have encountered a problem with it.  Assume conversation 1 (on SPID 1) is flooded with a large number of messages just before the conversation timer expires. The DialogTimer then expires before the target queue is drained, so new messages with the same SPID are sent on a new conversation 2.  In this case the new messages with the same SPID on conversation 2 could be processed on the target queue prior to the conversation 1 messages.  I am attempting to resolve this issue with your idea around conversation groups, posted here (bottom of thread): http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=182646&SiteID=1, but wanted to know if you had any other suggestions.  Thanks!  Terryc

Saturday, September 15, 2007 3:48 PM by terryc_ms

# re: Recycling Conversations

Hi Terry,

The only way SSB can *guarantee* order is using the same conversation. I understand this cannot be always achieved, but w/o knowing the specific details of your case I cannot comment on your problem. I suggest you post this question on the SSB forum at http://forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=91&SiteID=1, I find the forum is a much more appropiate format for discussions than the blog comments.

Monday, September 17, 2007 4:10 AM by RemusRusanu

# re: Recycling Conversations

Thanks - I've posted my questions to the following thread: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2152201&SiteID=1

Terryc

Tuesday, September 25, 2007 11:48 PM by terryc_ms

# re: Recycling Conversations

Hi Remus,

Your blog is great. I tried to make simple, one way, audit service broker. I followed your instructions to End conversation from both sides. But every time the endpoint for the conversation remains active even if conversation is closed.

Why is that happen?

Thanks in advance

Nikola

Tuesday, October 16, 2007 9:48 AM by hotmnikola

# Nikola

Hi,

Thanks for reading my blog! About your question, I'm not sure I udnerstand what you mean by 'conversation remains active even if conversation is closed'. Does a record in sys.conversation_endpoints still remains in the database? What state is it in? Please note that target conversation endpoints will stay around in 'CLOSED' state for up to 30 minutes for security reasons (to prevent a replay attack)

Tuesday, October 16, 2007 10:05 AM by RemusRusanu

# re: Recycling Conversations

Thanks for quick response,

Yes, record remains in sys.conversation_endpoints and its state is 'CLOSED'.

What confuses me is that only target-to-initiator endpoint (column is_initiator = 0) of conversation remains with lifetme column till 2075-11-03.

Initiator-to-target endpoint (column is_initiator = 1) is gone as I wanted.

Is that expected behavior as you said?

Thanks again

Nikola

Tuesday, October 16, 2007 10:25 AM by hotmnikola

# CLOSED conversations

Yes, the CLOSED target endpoints will be removed after a while. The relevant column is 'security_timestamp' that contains the datetime (UTC) when the endpoint should be deleted.

Tuesday, October 16, 2007 10:35 AM by RemusRusanu

# This blog has moved

If you wish to pos a comment on this entry you ca do it at it's new address at http://rusanu.com/2007/05/03/recycling-conversations/

Monday, October 29, 2007 8:43 AM by RemusRusanu
Anonymous comments are disabled
 
Page view tracker