If you want to support delivery methods other than email, you need a custom delivery protocol. This section describes how to build a custom delivery protocol and integrate it into your SQL-NS application. As mentioned in the early part of this chapter, several software vendors produce reusable custom delivery protocols that you can leverage. If you obtain a custom delivery protocol from one of these vendors, you can use the configuration techniques described in the later parts of this section to deploy it in your SQL-NS instance and use it in your applications. The Custom Delivery Protocol InterfaceLike other custom components in SQL-NS, custom delivery protocols are written as classes that implement a specific SQL-NS interface. The interface for delivery protocols is called IDeliveryProtocol and is shown in Listing 10.6. Listing 10.6. The IDeliveryProtocol Interface
The interface defines the following four methods:
Figure 10.6 shows the operation of the IDeliveryProtocol methods graphically. Figure 10.6. Operation of custom delivery protocol methods.When the distributor starts processing a work item, it calls the delivery protocol's Initialize() method, passing it the following items:
In the Initialize() method, a protocol typically saves the multicast flag and the callback delegate in private member variables and then processes the delivery channel arguments. The delivery channel arguments usually contain the information needed by the protocol to connect to the delivery endpoint that the channel represents. When implementing a custom delivery protocol, you decide the arguments that it requires. The distributor calls the DeliverNotification() method for each formatted notification that needs to be delivered. It passes two arguments to DeliverNotification():
To understand the contents of the notification headers array, think about how notifications are represented in the notifications table. The table contains a row for each generated notification that specifies the notification data, as well as information about the intended recipient (such as the subscriber ID and device name). You can think of each row as representing a separate delivery instruction saying, "send this notification data to this subscriber on this device." When multicast delivery is used, the distributor groups notifications that have common notification data and formats that data just once. But it still has to carry out the delivery instructions represented by each individual notification row. The notification headers array passed to the DeliverNotification() method essentially contains all the individual delivery instructions that apply to the given formatted notification message. The headers array contains a NotificationHeaders object for every notification row in a multicast group. If multicast delivery is not used, the headers array always contains just one element. Each NotificationHeaders object in the array contains the following information:
Using the information passed in, the DeliverNotification() method attempts to deliver the notification message. Notice that the DeliverNotification() method does not return a value indicating whether the delivery succeeded or failed. Instead, delivery status is reported by means of the NotificationStatusCallback delegate (originally passed to the Initialize() method). This delegate is defined with the following signature: public delegate void NotificationStatusCallback(params NotificationStatus[] status); When its delivery attempts complete, the delivery protocol invokes the delegate, passing in the delivery status of one or more notifications. The delegate takes a variable number of NotificationStatus objects, each of which represents the delivery status of one notification. The delivery protocol must call the notification status callback delegate with one NotificationStatus object for each NotificationHeaders object it received in DeliverNotification(). Each NotificationStatus object has the following properties (which the delivery protocol sets appropriately):
The delivery protocol can report the status of notifications one at a time or in sets. There is no requirement that the status of notifications be reported in order, or that the status of all the notifications passed to one DeliverNotification() invocation be reported together. Caution Although there are no restrictions on the grouping or ordering of notification status reports through the notification status callback, the delivery protocol must eventually report the status of every notification it handles. In other words, the total number of NotificationStatus objects passed in all the invocations of the callback must equal the total number of NotificationHeaders objects passed to the delivery protocol in all the invocations of DeliverNotification(). The use of the callback mechanism for reporting notification status allows the delivery protocol to implement asynchronous delivery. Instead of performing the delivery operation immediately when DeliverNotification() is called, the delivery protocol can queue a delivery request and return from DeliverNotification quickly. It can then service the queued delivery requests asynchronously and report their delivery status through the call-back. Delivery protocols implemented asynchronously usually perform much better than their synchronous counterparts because they do not block the distributor while waiting for a delivery to complete. As soon as DeliverNotification() returns, the distributor can start processing the next notification in the distributor work item. This can be an advantage if the delivery operations involve network latency, as they often do. The distributor calls the delivery protocol's Flush() method at the end of a distributor work item. In the Flush() method, the protocol should either complete or abort any pending notification delivery attempts and report their status through the notification status callback. After Flush() returns, there should be no more outstanding notification requests. It is illegal for the delivery protocol to call the notification status callback after Flush() returns. The Flush() method is usually important only if the protocol implements asynchronous delivery. In these delivery protocols, the Flush() should ensure that asynchronous delivery requests either complete or are canceled. In synchronous delivery protocols, each delivery operation completes by the end of the DeliverNotification() method, so Flush() usually doesn't do anything. After Flush() returns and the distributor performs the cleanup it needs to do to finish processing the work item, it calls the protocol's Close() method. Close() should clean up any resources held by the protocol during its operation. The distributor may reuse an instance of a delivery protocol class across the processing of several distributor work items. However, the distributor makes the following guarantees:
Note If you need to debug a custom delivery protocol, you can either attach a debugger to the SQL-NS Windows service process, or you can start the service as a console application from within a debugger. The "Debugging a Custom Component" topic in the SQL-NS Books Online provides instructions for debugging custom components that run in the SQL-NS Windows service. Implementing the Custom Delivery ProtocolIn this section, we implement the code for a custom delivery protocol that uses Message Queuing to deliver notifications to a message queue. Open the code for this custom delivery protocol using the following instructions:
The delivery protocol is implemented in the MQDeliveryProtocol.cs file. Listing 10.7 shows the code in this file. Listing 10.7. Implementation of the MQDeliveryProtocol
The first part of the source file defines a class, MQNotification, that represents the structure of the messages that will be delivered to the message queue. The delivery protocol creates an instance of this class for each notification it is asked to deliver and sends this instance to the message queue. The MQNotification class defines properties for the various aspects of a notification delivered via the MQDeliveryProtocol, including message ID, subscriber ID, target address, and notification body. This is a delivery protocol made up purely for the purposes of illustration, and these fields were chosen somewhat arbitrarily as examples of the headers that typical delivery protocols might use. Notice that the message structure is not specific to the music store application. The delivery protocol described here could be used with any application that wants to deliver to a message queue. The delivery protocol operations are implemented in the MQDeliveryProtocol class, which implements the IDeliveryProtocol interface. The Initialize() method saves the multicast flag and the notification status callback, and then looks in the channel arguments dictionary for an argument called QueueName. This is a required argument that tells the protocol the name of the queue to connect to. If the argument is not specified, Initialize() throws an exception. Because the queue name is not hard-coded, but instead obtained dynamically from a delivery channel argument, the MQDeliveryProtocol can be used with any message queue. You can create a delivery channel for the message queue you want to use, configure it to use the MQDeliveryProtocol, and specify the queue name through the QueueName argument. This is an example of how a delivery channel is used to represent a delivery endpoint (the specific message queue). The channel arguments tell the delivery protocol how to connect to that endpoint. After reading the channel arguments, Initialize() creates a MessageQueue object to represent the message queue. If the queue already exists, the protocol just creates a reference to it. If the queue does not exist, the protocol actually creates it. The DeliverNotification() method determines the number of notification headers passed in and creates two arrays with this number of elements. The first array is used to store the MQNotification objects that it sends to the message queue. The second array stores the NotificationStatus objects that it will pass to the notification status callback to report the status of each notification. For each element in the headers array, the protocol creates an MQNotification object and sets the properties using the value of a protocol field, the recipient information, and the formatted notification body. In this case, the protocol uses a protocol field called MessageId that provides a unique identifier for the message. When we look at the protocol declaration in the ADF, you will see this protocol field defined. After creating the MQNotification object, the protocol attempts to deliver the notification by calling the Send() method on the message queue object. If this method does not throw an exception, the code assumes that the delivery succeeded and creates a notification status object indicating success. If an exception is thrown, the code catches it and creates a notification status object indicating failure. The StatusInfo property of the status object is set to the value of the exception message. Note All the information returned by the delivery protocol through the notification status objects can be viewed in the notification distribution views, as described in the section, "The Notification Distribution Views" (p. 360). After the delivery protocol attempts to deliver all the notifications, it reports their status using the notification status callback. It passes the entire array of NotificationStatus objects to the callback delegate. Because this is a synchronous delivery protocol, the Flush() method has no work to do; all delivery attempts complete before DeliverNotification() returns. The Close() method closes the message queue object and then returns. This is the only operation required to clean up the delivery protocol state. Declaring the Custom Delivery Protocol in the ICFTo use the custom delivery protocol, we need to declare it in the ICF. Listing 10.8 shows this declaration. Listing 10.8. Declaration of the Custom Delivery Protocol
The declaration appears in a <Protocol> element, inside the <Protocols> element. It specifies a protocol name, which can then be used in delivery channel declarations and notification class declarations in the ADF to identify the custom delivery protocol. The declaration also specifies the class and assembly names that the distributor will use to load the custom delivery protocol at runtime. As with all other SQL-NS custom components, the class name must be provided in fully qualified form, with the namespace prefix, and the assembly name must include a full path. The assembly name given here refers to the delivery protocol assembly that will be built when we compile the delivery protocol source code, as described in the section, "Testing the Custom Delivery Protocol," (p. 386). Creating a Delivery Channel for the Custom Delivery ProtocolAfter the custom delivery protocol has been declared, we can create a delivery channel that uses it, as shown in Listing 10.9. Listing 10.9. Declaration of a Delivery Channel Using the Custom Delivery Protocol
The delivery channel declaration is added below the other delivery channel declarations in the <DeliveryChannels> element in the ICF. It specifies a delivery channel name and references the protocol name declared in Listing 10.8. It also defines the one required delivery channel argument, the queue name. Supporting the Custom Delivery Protocol in the Notification ClassFinally, to deliver NewSong notifications with the custom delivery protocol, we need to add a <Protocol> declaration for it to the NewSong notification class, as shown in Listing 10.10. Listing 10.10. Declaring Support for the Custom Delivery Protocol in the NewSong Notification Class
The new <Protocol> declaration appears after the other protocol declarations, which remain valid. This means that notifications of this notification class can be delivered over multiple delivery protocols. The protocol declaration references the protocol name and declares the one protocol field that the protocol requires. This protocol field provides a message ID by calling the NEWID() SQL function to generate a new identifier. You saw the value of this protocol field accessed in the implementation of the delivery protocol's DeliverNotification() method, in Listing 10.7. For illustration purposes, Listing 10.10 also shows how a protocol declaration can specify a retry schedule for failed notifications. In this case, the retry schedule specifies two delay intervals: 5 minutes and 15 minutes. If one or more delivery failures occur in the processing of a work item when using the MQProtocol, the distributor will wait 5 minutes and then process the work item again, attempting to deliver those notifications that were not successfully delivered before. If there are still failures, the distributor will try once more after waiting 15 minutes. Because the retry schedule is specified within the <Protocol> declaration, you can specify different retry patterns for different protocols. Note The delays in a retry schedule are relative to one another, not relative to the first processing attempt on the work item. In the example shown in Listing 10.10, the first retry attempt will happen 5 minutes after the original attempt, and the second retry attempt will happen 15 minutes after the first retry attempt (20 minutes after the original attempt). Also for illustration, Listing 10.10 shows how the <ExpirationAge> element can be used to specify an expiration age for notifications of the notification class. In this example, the expiration age is set to 6 hours. Undelivered notifications older than 6 hours will never be delivered. Testing the Custom Delivery ProtocolUse the following steps to get the music store instance running with the custom delivery protocol:
To test the new delivery protocol, you need subscriber devices configured to use the delivery channel that references it (MQChannel). When you completed the instructions in the "Adding Subscriber Devices for New Delivery Channels" section (p. 365) earlier, you added devices that use only the SMTP delivery channel. Now that you've added the MQChannel to the instance, you must run the AddMQSubscriberDevicesAndSubscriptions.sql script (located in C:\SQL-NS\Chapters\10\Scripts). This will add a set of subscriber devices that specify MQChannel as their delivery channel name (and a corresponding set of subscriptions that reference these subscriber devices). Complete the following instructions:
For the purposes of seeing notifications delivered by the custom delivery protocol, we'll use a simple client program that listens for messages on the message queue. When it receives a message, this program displays the message contents on the console. We'll start this program, submit some events into the music store application that match the subscriptions referencing the new subscriber devices, and then wait to see the listener program receive and display the resulting notifications. Before you can test the custom delivery protocol, you need to build the client listener program using the following instructions:
Use the following instructions to exercise the custom delivery protocol and see the results:
The messages printed by the notification listener show the header values created by the delivery protocol, as well as the notification bodies. Because the subscriber devices associated with the custom delivery protocol's delivery channel are of the TextMessageDevice type, the content formatter produces smaller notification bodies than the ones in the email notifications. Note To quit this MQListener program, press Ctrl+C in the command prompt window in which it is running.
|