NOTE When a new conversation is created, in addition to being assigned a new conversation or dialog handle, that conversation is also joined to a new conversation group behind the scenes
Trang 1Planning Conversations Between Services
A conversation is a dialog between two services The purpose of this dialog is, of course, the
sending and receiving of messages, which ultimately leads to the completion of a task
A powerful feature of Service Broker messaging is that it guarantees exactly-once-in-order
(EOIO) messaging This means that messages are sent exactly once; there’s no chance that
a message can be sent twice because of a system issue, so the receiver doesn’t have to
check whether a message has already been processed It also means that messages are
always ordered in their queue in the same order in which they were sent (The
queuing_ordercolumn of the queue indicates this order.) Service Broker makes sure of
this, even in cases in which the send order somehow gets out of sync
Transactions are an integral part of Service Broker conversations When a message is sent
within the scope of a transaction, it is not actually moved to the destination queue unless
the transaction commits This has to do with the fact that before being placed in a queue,
messages are stored in internal tables called transmission queues (which are viewable via the
catalog view sys.transmission_queue) Similarly, a message is not deleted from a queue
after it is received unless the transaction commits (except in cases in which the RETENTION
flag for the queue is set to ON) This point is very important because it means that any
database operations as well as any messaging operations belong to the same transaction,
and they are controlled by the same transactional system This is a unique feature of
messaging with Service Broker and is part of the rationale for having messaging built in to
the database
TheBEGIN CONVERSATION DIALOGstatement is the cornerstone of the process of creating
conversations It specifies the services participating (TO SERVICEandFROM SERVICE) and
the contract to which they will be adhering during the dialog (ON CONTRACT) It also
enables the correlation of messages because it is the thread that relates them to each other
This relationship is achieved through the use of a conversation, or dialog, handle A dialog
handle is a variable of type uniqueidentifierthat identifies the dialog
You use the following syntax to start a dialog:
BEGIN DIALOG [ CONVERSATION ] @DialogHandle
FROM SERVICE InitiatingServiceName
TO SERVICE ‘TargetServiceName’
[ , { ‘service_broker_guid’ | ‘CURRENT DATABASE’ } ]
[ ON CONTRACT ContractName ]
[ WITH
[ { RELATED_CONVERSATION = RelatedDialogHandle |
RELATED_CONVERSATION_GROUP = RelatedConversationGroupId } ]
[ [ , ] LIFETIME = DialogLifetimeInSeconds ]
[ [ , ] ENCRYPTION = { ON | OFF } ] ]
[ ; ]
The items in the syntax are as follows:
Trang 2@DialogHandle—This is an output parameter of typeuniqueidentifierthat is
returned by the statement You use this option later in this chapter to relate
conversations
InitiatingServiceName—This is the name of the (local) service acting as the initiator
’TargetServiceName’—This is the name of the service acting as the target Note that
this is a case-sensitive string (technically of type nvarchar(256)), for purposes of
name resolution against non–SQL Server services (for later extensions); a byte-level
comparison is made for name resolution If this value is incorrectly provided,
messages remain in the transmission queue Note that
sys.transmission_queue.to_service_nameholds this value
A Service Broker globally unique identifier (GUID) may be optionally specified after
’TargetServiceName’, and it is required when you are doing inter-database
messag-ing (as a later example in this chapter illustrates) The ’CURRENT_DATABASE’string
indicates the current Service Broker GUID
ContractName—This is the name of the contract that the services use
WITH—This clause allows you to specify a related conversation group to which the
current conversation is related, either via a conversation handle or a conversation
group ID
NOTE
When a new conversation is created, in addition to being assigned a new conversation
(or dialog) handle, that conversation is also joined to a new conversation group behind
the scenes, unless the group ID of an existing conversation group is specified
Conversation groups are incredibly important because queues are locked at the
conver-sation group level A queue used by any services in a group of related converconver-sations is
locked on that group during receives, ensuring that messages are always received
seri-ally by all the services in the group BEGIN CONVERSATION DIALOGimplicitly locks the
conversation group it specifies (or the implied group it creates)
If locking did not work this way, a service program could receive a message lower in the
queue order before a second instance of the same service program finished receiving a
message higher in the order If that lower message needed data that was dependent
on the other uncommitted receive, you would end up with a referential integrity issue
It is thus a rather questionable practice to spread related and/or dependent data
across multiple messages or to do so without doing the appropriate checks in the code
The following options are available for the WITHclause:
RELATED_CONVERSATION—This option relates the current conversation to the
conversation group created for the specified conversation handle
Trang 3RELATED_CONVERSATION_GROUP—This option relates the current conversation to
the conversation group created for the specified conversation group ID (This
has the same effect as the RELATED_CONVERSATIONkeyword, with a different
parameter.) If the value provided for RelatedConversationGroupIdis invalid, a
new conversation group is created to which the dialog is related
LIFETIME—This option specifies the number of seconds for which the dialog
will remain open; it defaults to the maximum value of int, which is
approxi-mately 68 years If this option is specified, both services must call END DIALOG
CONVERSATIONbefore this time is up, or an error is raised
ENCRYPTION—This option specifies whether messages transmitted beyond the
current SQL Server instance (within the conversation) are encrypted It defaults
toON, meaning that message transmissions between databases on different
instances are encrypted by default Encryption requires the use of certificates,
discussed later in this chapter, in the section “Using Certificates for
Conversation Encryption.”
Creating the Conversation Initiator
It’s finally time to create the stored procedure that initiates the dialog between the
services Listing 49.4 contains the code to do this
CREATE PROCEDURE Production.ProductModelUpdate
GO
USE AdventureWorks2008
GO
DROP PROC Production.ProductModelUpdate
GO
CREATE PROCEDURE Production.ProductModelUpdate
(
@ProductId int,
@NewName Name
)
AS
DECLARE @DialogHandle UNIQUEIDENTIFIER
DECLARE @CatalogChangeXml xml (DOCUMENT dbo.CatalogChangeSchema)
DECLARE @RemoteSSBGuid uniqueidentifier
Get the SSB guid for the target service’s db
SELECT @RemoteSSBGuid = service_broker_guid
FROM sys.databases
WHERE name = ‘XCatMgmt’;
BEGIN TRAN;
Trang 4UPDATE Production.ProductModel
SET Name = pm.Name change this to @NewName to actually modify the data
FROM Production.ProductModel pm
JOIN Production.Product p on
p.ProductModelId = pm.ProductModelId
WHERE p.ProductId = @ProductId;
if @@ERROR != 0
BEGIN
ROLLBACK TRAN
RAISERROR(‘(Initiator) Error during table update’, 16, 1)
RETURN
END;
BEGIN TRY;
WITH XMLNAMESPACES
(
‘http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/ProductModelDescription’ as p1,
‘http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/ProductModelWarrAndMain’ as wm,
‘http://www.adventure-works.com/schemas/OtherFeatures’ as wf,
‘http://www.w3.org/1999/xhtml’ as html
)
SELECT
@CatalogChangeXml = CatalogDescription.query(‘
for $ContextNode in //p1:ProductDescription,
$SpecNode in $ContextNode/p1:Specifications,
$FeatureNode in $ContextNode/p1:Features
return
<CatalogChangeMessage
xmlns=”urn:www-samspublishing-com:examples:ssb:catalogchange”>
<CatalogChange
ChangeType=”2”
Price=”{sql:column(“p.ListPrice”)}”
ManufacturerId=”1”
Name=”{sql:column(“pm.Name”)}”
SourceProductId=”{sql:column(“p.ProductId”)}”>
<Summary>{$ContextNode/p1:Summary/html:p/text()}</Summary>
<Features>
Handlebars: {$FeatureNode/wf:handlebar/text()}
Wheels: {$FeatureNode/wf:wheel/text()}
BikeFrame: {$FeatureNode/wf:BikeFrame/html:i/text()}
</Features>
<Specifications>
Trang 5Material: {$SpecNode/Material/text()}
Color: {$SpecNode/Color/text()}
ProductLine: {$SpecNode/ProductLine/text()}
Style: {$SpecNode/Style/text()}
</Specifications>
</CatalogChange>
</CatalogChangeMessage>
‘)
FROM Production.ProductModel pm
JOIN Production.Product p
ON pm.ProductModelId = p.ProductModelId
WHERE p.ProductId = @ProductId
END TRY
BEGIN CATCH
ROLLBACK TRAN
RAISERROR(‘(Initiator) Error during XML production.’, 16, 1)
RETURN;
END CATCH
BEGIN DIALOG CONVERSATION @DialogHandle
FROM SERVICE
[//samspublishing.com/SS2008/SSB/Services/CatalogChangeInitiatorService]
TO SERVICE
‘//samspublishing.com/SS2008/SSB/Services/CatalogMaintenanceService’,
@RemoteSSBGuid
ON CONTRACT
[//samspublishing.com/SS2008/SSB/Contracts/BasicCatalogChangeContract]
WITH ENCRYPTION = OFF;
SEND ON CONVERSATION @DialogHandle
MESSAGE TYPE
[//samspublishing.com/SS2008/SSB/MessageTypes/CatalogChangeMessage]
(@CatalogChangeXml)
PRINT ‘(Initiator) Message Sent Successfully.’
COMMIT TRAN
ThisUPDATEprocedure exemplifies several key concepts A variable for the dialog handle is
declared for later storage during the call toBEGIN DIALOG After the actual database update,
a typed XML variable—that matches the same XML schema collection as the message type
of which it will become an instance—is populated, using anXQuerystatement
The call to CatalogDescription.query()transforms the original XML into XML that
matches the schema in CatalogChangeSchema This way, if there are any validation errors,
Trang 6you find out about them before the message is sent (implicitly terminating the open
trans-action) This makes it virtually impossible to send an invalid message
The new value forProductModel.Nameis inserted into the XML via the attribute constructor
Name=”{sql:column(“pm.Name”)}” The value of the attribute ChangeType=”2”corresponds
to the enumeration in the schema whereid=”Update” Because you use this value (as you’ll
soon see), the service forXCatMgmtknows what the sender intended by the message
In the new SENDstatement, the saved GUID for the Service Broker instance on XCatMgmtis
used to locate the target service when sending the message SENDhas the following syntax:
SEND ON CONVERSATION ConversationHandle
[ MESSAGE TYPE MessageTypeName ]
[ ( MessageBody ) ][ ; ]
As you can see, the SENDstatement requires ConversationHandlefor message correlation
The type specified inMessageTypeNamemust match the appropriate type specified in the
contract for the sending service.MessageBodymust be of a data type that can be converted
tovarbinary(max), such as xml
If any issues arise during the sending of a message, you can find the text of the reason
for the problems insys.transmission_queue.transmission_status This is a great place
to look for transmission-related information because the messages in it are reasonably
user-friendly
You also need to consider the use of the END CONVERSATIONstatement, which, predictably,
ends the conversation This is its syntax:
END CONVERSATION ConversationHandle
[
[ WITH ERROR = ErrorPositiveInt DESCRIPTION = ‘ErrorMsg’ ]
|
[ WITH CLEANUP ]
][ ; ]
If desired, you can specify an error message value in ErrorPositiveIntand an error
message of your choosing when ending the conversation Ending a conversation with an
error drops all the unsent messages currently in the transmission queue, and Service
Broker sends a message to the target service of typeError.
You can specify the WITH CLEANUPclause to clean up the transmission queue’s unsent
messages related to the conversation and to clear the queue owned by this service (in this
case,Production.CatalogChangeAckQueue).
Note that until both services in the conversation call END CONVERSATION, the conversation
is not complete When only one side calls END CONVERSATIONor when the LIFETIME
setting of the conversation has been met, the other endpoint can continue to use the
Trang 7invalid conversation handle until the two sides receive the EndDialogmessage (if messages
are sent after the EndDialogmessage has been received, a runtime error is raised)
Creating the Conversation Target
After you create the initiator, the next step is to create the service program in XCatMgmt
that is activated when messages arrive This step involves a bit more work because this
program needs to receive messages of at least three types (Error,EndDialog, and
CatalogChangeMessage), create and send acknowledgment messages, and perform local
database DML
The example in Listing 49.5 contains a stored procedure that receives correlated messages
from the initiator Note that this procedure is the same (empty) one you specified in
Listing 49.3
CONVERSA-TION in an Service Broker–Activated Stored Procedure
use XCatMgmt
GO
DROP PROC Publication.CatalogChangeQueueReader
GO
CREATE PROCEDURE Publication.CatalogChangeQueueReader
AS
DECLARE @e int, @r int, @MsgTypeName nvarchar(128), @desc varchar(255),
@MsgXml xml, @AckXml xml (DOCUMENT dbo.GenericAcknowledgementSchema),
@RemoteSSBGuid uniqueidentifier, @ErrNS varchar(150), @EndDlgNS varchar(150),
@CatChangeNS varchar(150), @TempXml xml, @NewName varchar(100),
@SourceProductId int, @ChangeType int, @ConversationGroupId uniqueidentifier,
@DialogHandle uniqueidentifier
SET @ErrNS = ‘http://schemas.microsoft.com/SQL/ServiceBroker/Error’
SET @EndDlgNS = ‘http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog’
SET @CatChangeNS = ‘//samspublishing.com/SS2008/SSB/MessageTypes/
CatalogChangeMessage’
Get the SSB guid for the initiator’s db
SELECT @RemoteSSBGuid = service_broker_guid
FROM sys.databases
WHERE name = ‘AdventureWorks2008’;
BEGIN TRAN;
WAITFOR(
GET CONVERSATION GROUP @ConversationGroupId
FROM Publication.CatalogChangeQueue
), TIMEOUT 1000
IF @ConversationGroupId IS NULL
Trang 8BEGIN
ROLLBACK TRAN
PRINT ‘(Target) ConversationGroupId not acquired in time.’
RETURN
END
ELSE
PRINT ‘(Target) ConversationGroupId acquired successfully.’;
RECEIVE TOP(1)
@MsgXml = CAST(message_body as xml),
@MsgTypeName = message_type_name,
@DialogHandle = conversation_handle
FROM Publication.CatalogChangeQueue
WHERE conversation_group_id = @ConversationGroupId
SELECT @e = @@ERROR, @r = @@ROWCOUNT
IF @r = 0
BEGIN
ROLLBACK TRAN
RETURN
END
IF @e != 0
BEGIN
ROLLBACK TRAN
PRINT ‘(Target) Error during receive.’
RETURN
END
ELSE
PRINT ‘(Target) Message received.’
if the msg is of type Error, end the conversation, stating the error
IF @MsgTypeName = @ErrNS
BEGIN
SELECT @desc = @MsgXml.value(‘
declare default element namespace
“http://schemas.microsoft.com/SQL/ServiceBroker/Error”;
(/Error/Description/text())[1]’, ‘varchar(255)’)
ROLLBACK TRAN
PRINT ‘(Target) Error message received.’
END CONVERSATION @DialogHandle
WITH ERROR = 808 DESCRIPTION = @desc
RETURN
END
Trang 9if the msg is of type EndDialog, end the conversation without error
IF @MsgTypeName = @EndDlgNS
BEGIN
PRINT ‘(Target) EndDialog message received.’;
END CONVERSATION @DialogHandle
RETURN
END
if the msg is of type CatalogChangeMessage, update appropriately
IF @MsgTypeName = @CatChangeNS
BEGIN
BEGIN TRY
what kind of change is requested?
(here we only deal with product name and xml changes)
;WITH XMLNAMESPACES
(
DEFAULT ‘urn:www-samspublishing-com:examples:ssb:catalogchange’
)
SELECT
@ChangeType = @MsgXml.value(‘
(/CatalogChangeMessage/CatalogChange/@ChangeType)[1]’, ‘int’),
@NewName = @MsgXml.value(‘
(/CatalogChangeMessage/CatalogChange/@Name)[1]’, ‘varchar(100)’),
@SourceProductId = @MsgXml.value(‘
(/CatalogChangeMessage/CatalogChange/@SourceProductId)[1]’, ‘int’)
IF @ChangeType IS NULL OR @NewName IS NULL OR @SourceProductId IS NULL
BEGIN
ROLLBACK TRAN
PRINT ‘(Target) An xml-selected value is NULL.’
RETURN
END
IF @ChangeType = 2 “Update”
BEGIN
UPDATE Publication.Product
SET
ProductName = @NewName,
ProductDetailXml = @MsgXml
WHERE @SourceProductId = SourceProductId
IF @@ERROR != 0 OR @@ROWCOUNT = 0
BEGIN
ROLLBACK TRAN
PRINT ‘(Target) Failure during table update.’
RETURN
END
Trang 10SET @AckXml = ‘
<Ack
xmlns=”urn:www-samspublishing-com:examples:ssb:genericack”
ResultCode=”1”>
<ResultMessage ContentId=”’ + CAST(@SourceProductId as varchar(10)) + ‘“
MsgType=”1”>Success!</ResultMessage></Ack>’;
SEND ON CONVERSATION @DialogHandle
MESSAGE TYPE
[//samspublishing.com/SS2008/SSB/MessageTypes/GenericAck]
(@AckXml)
PRINT ‘(Target) Message Sent Successfully.’
END
END TRY
BEGIN CATCH
ROLLBACK TRAN
SELECT @desc = ERROR_MESSAGE()
INSERT dbo.TargetErrs SELECT @desc simple error storage table
PRINT ‘(Target) Caught error:’ + @desc
END CONVERSATION @DialogHandle
WITH ERROR = 808 DESCRIPTION = @desc
RETURN
END CATCH
END
COMMIT TRAN
One issue you might notice when testing the code in Listing 49.5 is that the PRINT
state-ments in the activated procedure do not show up in the SSMS query window You need to
use SQL Profiler (which is discussed in Chapter 6, “SQL Server Profiler”) to aid in
debug-ging activated code because it always runs on background threads To help you out, a
Service Broker event group is available in SQL Profiler for tracing all the new Service
Broker events’ message transmission, activation, conversation beginning and ending, and
so on You can use this new event group along with the T-SQL and stored procedure event
groups to trace the code path Print statements are included in the code in Listing 49.5 to
make debugging easier
The code in Listing 49.5 introduces three new SQL statements: GET CONVERSATION DIALOG,
RECEIVE, and GET CONVERSATION GROUP.
The purpose ofGET CONVERSATION DIALOGis to lock the next available conversation group
associated with the messages inPublication.CatalogChangeQueue Conventional use of
GET CONVERSATION DIALOGrequires that theWAITFORstatement be used to make the
initi-ated program wait a specified number of milliseconds (or infinitely, as specified in
TIMEOUT) before continuing on in the program logic If the conversation group ID has been