September 1999
A publisher is any program that makes the COM calls that initiate events, and a subscriber is a COM+ component that receives the COM calls representing events from a publisher. The subscriber implements an interface as a COM server; the publisher makes calls on it as a COM client. |
This article assumes you're familiar with COM and C++ |
Code for this article: COM+ Events.exe (92KB)
David Platt is president and founder of Rolling Thunder Computing Inc. (http://www.rollthunder.com), and teaches COM and COM+. This article is based on a chapter from his book Understanding COM+ (Microsoft Press, 1999). |
Publishing and Subscribing
Notifying interested parties of changes to data is a classic problem of distributed computing. One program detects a change in the world that it thinks other programs want to know about: a stock ticker program notes a change in price, a weather monitoring program notes changes in barometric readings from remote sensors, or a medical monitoring program notes that a patient's blood pressure has exceeded the acceptable range. Somewhere else in the world are other programs that would like to hear about these changes: a portfolio program that buys a stock when it hits a certain price, an alarm program that tells fishing boats to return to port, a patient monitoring program that signals the nurses' station that a patient requires medication.
The meanings of client and server become murky in discussions of scenarios like these, so I will introduce a new nomenclature. Programs that provide notifications to other programs, such as the stock ticker program, I will call "publishers." Applications such as the portfolio program, which receive data from publishers and act on it, I will call "subscribers." A publisher detects a change that the subscriber cares about, but how does the subscriber find out from the publisher when a change takes place? The simplest approach is for the subscriber to poll the publisher every so often, analogous to you telephoning your stockbroker periodically to ask for the latest price of a stock. In the terminology of COM+, the publisher would provide the subscriber with an interface, and the subscriber would periodically call a method on that interface to see if any changes had taken place, as shown in Figure 1.
Figure 1 Subscriber Polling |
This strategy is simple to code, but it's a terrible idea for several reasons. First, the subscriber wastes an enormous amount of time and energy asking, "Are there any changes?" The publisher also wastes time and effort replying, "No, there aren't." You might get away with polling in a desktop application that spends most of its time idling, waiting for user input, but it's deadly in an enterprise application where many clients connect to a single server. Mutual fund giant Fidelity just announced restrictions on their account holders who did this too
often. And anyone who has ever screamed at their child's tenth repetition of "Are we almost there yet?" in as many minutes will understand the fundamental wrongness of this approach.
Second, polling involves some inevitable amount of latency between the time the change occurs and the time the subscriber polls for it. On average, this latency is equal to half the polling interval. As you lengthen the polling interval to waste fewer CPU cycles, the latency increases. Not only is this latency bad in and of itself, but the fact that it is nondeterministic (it varies from one call to another) is also a problem in designing systems. Polling is evil in an enterprise application, so don't do it. You really want the publisher to initiate the notification process when it detects interesting changes in the world. Instead of you having to call your stockbroker periodically to find out the latest prices, you would save yourselves a lot of headaches if you gave the broker your phone number and asked him to call when something changed. In COM terms, the subscriber provides the publisher with an interface, and the publisher calls a method on it when something interesting happens. This is the approach that ActiveX® controls use to fire events to their containers, as shown in Figure 2. Here, the control is the publisher and the container is the subscriber. |
Figure 2 ActiveX Callbacks |
This is called a tightly coupled event. The subscriber knows exactly which publisher to request notifications from (the container knows the CLSID, or class identifier, of the control) and the mechanism for connecting to it (the IConnectionPointContainer and IConnectionPoint interfaces exposed by the control). A tightly coupled event works reasonably well on a single desktop, but it has a number of drawbacks when used at the scale of an enterprise system. For the event mechanism to function, a tightly coupled event requires both the publisher and the subscriber to be running at all times. Both sides have to be running when the subscriber (the container) provides the publisher (the control) with its callback interface, and both sides have to be running when the publisher calls the method on the subscriber's interface. ActiveX controls are mostly used for desktop user interface elements, which have no reason not to match the lifetimes of their containers, so this restriction is not usually a problem. Requiring lifetimes to overlap can be a big problem in enterprise applications, as I demonstrated in my previous article on Queued Components, "Building a Store-and-Forward Mechanism with Windows 2000 Queued Components" (MSJ, June 1999). You'd like the subscriber to be able to make subscription requests while the publisher isn't running, and the publisher to be able either to launch the subscriber just in time to fire an event or to use Queued Components instead of direct connections for sending event notifications. Another problem with tightly coupled events is that the subscriber needs to know the exact mechanism a particular publisher requires for establishing subscriptions, and this mechanism can vary radically from one publisher to another. For example, ActiveX controls use the IConnectionPoint mechanism for hooking up the callback circuit to deliver notifications of their events. An OLE server uses the method Advise on the IOleObject interface to hook up a callback circuit to deliver notifications of embedding-related events. It would be great to standardize on one connection mechanism for publishers and subscribers, and to be able to use this standard connection mechanism administratively instead of having to write code to access it. The third problem of the classic event mechanism is that it contains no mechanism for filtering or interception. When I tell my broker that I care about changes to stock prices, he doesn't call me with the change of any stock price anywhere in the world. Instead, I tell him which stocks I care about, usually those that I own or am thinking about buying. I might even tell him that I care only about certain movementsfor example, if the price of my favorite stock tops $150. You'd like to have some mechanism whereby a subscriber could specify that it wanted to receive calls only if their parameters had certain valuesfor example, if the parameter designating the stock symbol matched one that you cared about, or if the parameter designating the price was greater than a certain value. And ideally, you'd like to be able to specify this information administratively as well. One solution to these problems would be to store the information about matching publishers with subscribers externally instead of inside the programs themselves. The publisher would maintain an external database containing a list of the different events for which it knows how to send notifications. Subscribers could read this list and pick the events that they want to hear about. The publisher would also maintain some sort of subscription database, conceptually similar to a mailing list, of the CLSIDs of subscribers that want to hear about each event. Administrative tools or the subscriber programs themselves would know how to make entries in this subscription database. When the publisher wants to fire an event, it looks in this database, finds the CLSIDs of all the interested subscribers, creates a new object of each interested class, and calls a method on that object. I would call such a system loosely coupled rather than tightly coupled because the information about which subscriber wanted to hear from which publisher would be maintained in a central database instead of being bound to the programs themselves. This promising design approach has two snags. First, you'd have to develop and maintain the events and subscriptions database and write all the code for the publisher-side event firing mechanism and the administrative tools. You'd have to sell a lot of units before this approach became cost-effective, and enterprise applications generally have relatively low unit volume compared to desktop applications. Your time and money would be much better spent on your business logic instead of an event infrastructurecustomers aren't paying you for infrastructure. Second, even if you did develop all this infrastructure, your subscription process would still be different from every other vendor's. Subscribers would have to know not only the specific techniques required to subscribe to your events, but also the different mechanisms required for every other publisher they ever want to hear from. What you'd really like is to inherit such a mechanism from the operating system. This way you would have to write very little code, and every vendor's subscription process would be the same. Event Services
|
Figure 3 COM+ Event Service Architecture |
Using this terminology, an event represents a single call to a method on a COM interface, originated by a publisher and delivered by the event service to the correct subscriber or subscribers. A publisher is any program that makes the COM calls that initiate events, and a subscriber is a COM+ component that receives the COM calls representing events from a publisher. The subscriber implements an interface as a COM server; the publisher makes calls on it as a COM client. The only change from classic COM is the event service in the middle, which keeps track of which subscribers want to receive the calls and directs the calls to those subscribers without requiring any specific knowledge of the publisher.
Making a single event call is known as firing the event. You may also see the term "publishing" used in the documentation as a synonym for firing. This term suffers from ambiguity between its general meaningto make information availableand its specific meaningin this sense, to initiate a single event. Since the term "firing" is unambiguous, it is used in the names of the interfaces and methods relating to the event system, and is the term that the many users of ActiveX controls are accustomed to. I will continue using it here. The connection between a publisher and a subscriber is represented by an event class. An event class is a COM+ component, synthesized by the event system, that contains the interfaces and methods a publisher will call to fire events and that a subscriber needs to implement if it wants to receive events. The interfaces and methods provided by an event class are called event interfaces and event methods. You tell COM+ which interfaces and methods you want an event class to contain by providing COM+ with a type library. Event classes are stored in the COM+ catalog, placed there either by the publishers themselves or by administrative tools. A subscriber indicates its desire to receive events from a publisher by registering a subscription with the COM+ event service. A subscription is a data structure that provides the event service with information about the recipient of an event. It specifies which event class, and which interface or method within that event class, the subscriber wants to receive calls from. Subscriptions are stored in the COM+ catalog, placed there either by the subscribers themselves or by administrative tools. Persistent subscriptions survive a restart of the operating system; transient subscriptions do not. When a publisher wants to fire an event, it uses the standard object creation functions, such as CoCreateInstance or CreateObject, to create an object of the desired event class. This object, known as an event object, contains the event system's implementation of the requested interface. The publisher then calls the event method that it wants to fire to the subscribers. Inside the event system's synthesized implementation of the interface, the event system looks in the COM+ catalog and finds all the subscribers that have registered subscriptions to that interface and method. Once that task is completed, the event system then connects to each subscriber (using any combination of direct creation, monikers, or queued components) and calls the specified method. Because frequently more than one subscriber wants notification for each event, event methods may not use any type of output parameters and must return only success or failure HRESULTsthe same restrictions as for Queued Component interface methods. In this way, essentially any COM client can become a publisher, and essentially any COM+ component can become a subscriber. Neither has to know anything about the intervening gyrations performed by the event service to make the connection. The current implementation of the event system has some limitations. First, the subscription mechanism is not itself distributed. There is currently no enterprise-wide repository of all the publishers and subscribers. You could certainly construct such a thing yourself by placing all the event classes and subscriptions on a central machine. Publishers would then create event objects on the central machine, and subscribers would receive their notifications from the event system on that central machine. This approach would be easy to set up, but it would cost you one extra network hop per event fired from the publisher (for the call to travel from the publisher's machine to the central machine). The central machine would also represent a potential single point of failure, so you'd have to make sure you backed it up with a hot spare using Microsoft Clustering Services. Second, delivery of events in the current system occurs either by DCOM or Queued Components, which are one-to-one communication mechanisms. This means that the delivery time and effort increase linearly with the number of subscribers, which in turn means that the current system is not well suited to firing events to many subscribers. The exact number of simultaneous subscribers that can be supported will obviously vary widely depending on system workload and hardware capacity, and in any event will have to await the final performance tuning of the product. For ballpark numbers at the time of this writing, figure that dozens of subscribers are probably fine (unless you call them every second and pass them the Encyclopedia Britannica), and thousands of subscribers are probably not fine (unless you only call them once or twice a day, pass zero information, and don't care when they process it). The current event system does not support broadcast datagrams, which would be the best way of reaching very large numbers of subscribers. If you need to do that today, you can write one subscriber that receives events via DCOM or Queued Components and then resends the incoming data to many users via a datagram. The Simplest Event Example
|
Figure 4 Publisher Sample |
The first thing you need to do is register the event class. To do this, you create a new COM+ application and start installing a component by using the Component Services snap-inclicking the "Install new event class(es)" button, as shown in Figure 5. |
Figure 5 Installing a Component |
You need to provide COM+ with a type library describing the event interfaces and methods so that it will know how to synthesize the event class on your behalf. To work with the event system, the type library needs to reside in or be accompanied by a self-registering DLL. The user interface used to make these settings is shown in Figure 6. |
Figure 6 Installing an Event Class |
I've registered the event class in the sample using the ATL COM AppWizard to create a dummy component called StockEventCls (the name of the component and project can't be the same in ATL), which you will find in the StockEventClass folder. It contains the definition of the interface that the subscriber will implement and that the publisher will call. In this case, the interface is named IStockEventCls and contains the methods StockPriceChanged and NewStockListed. The event system uses the type library and the self-registration code from this component. Even though I had to add implementations of the methods to the event class component because of the internal structure of the ATL COM AppWizard, the methods on this component will never be called by the event system. It was simply the fastest way to produce the type library and self-registration that the event system requires.
Entering the path to the event class component DLL and clicking OK tells the Component Services snap-in to synthesize an event class and install it in the COM+ catalog. The snap-in does this internally by going to the COM+ catalog and calling the method ICOMAdminCatalog::InstallEventClass. The component will look very much like any other component in the snap-in. The only discernible difference at this level is the entries on the Advanced tab of the component's property sheet, as shown in Figure 7. In this example, I add a Publisher ID string that will show up in the snap-in when I add the subscriptions to this event class later. This is all I need to do for a subscriber to find the event class and subscribe to it, and for a publisher to create an object of the event class and fire it. |
Figure 7 Event Class Properties |
Next, add a subscriber, which must be a configured component in a COM+ application. You will find the subscriber component in the StockSubscriber folder. You can install them both in the same COM+ application for convenience if you want. After you create an application and install the component as usual, you'll notice that each component shown in the snap-in contains a Subscriptions folder just beneath its Interfaces folder, as shown in Figure 8. Right-clicking on the Subscriptions folder will bring up a wizard allowing you to enter a new subscription. The wizard will offer you a choice of all the interfaces that have been added as event classes, as shown in Figure 9. |
Figure 8 Snap-in Subscriptions |
Figure 9 Creating a New Subscription |
You can subscribe to a single method or to all methods on the entire interface. If you want to receive calls to more than one method, but not every method, you must add a subscription for each desired method. The wizard searches the COM+ catalog for registered event classes that support the specified interface and offers you the choice to subscribe, as shown in Figure 10. Choose the publisher that you want your subscriber to hear from. |
Figure 10 Subscribing to a Method |
One additional step in the wizard allows you to enter a name for the subscription and, more importantly, enable the subscription (see Figure 11). Subscriptions may be enabled or disabled. The latter receive no event notifications. That's all you have to do. |
Figure 11 Subscription Options |
When the Publisher Demo application wants to fire an event, it simply creates an object of the event class and calls the method on it. The code to do this is identical to what the publisher would do if it was making calls on a single object in classic COM. The only difference is that the publisher creates an object of the COM+ synthesized event class instead of the subscriber class. When the publisher calls the method on the event class, the event system looks through the COM+ catalog to find all the relevant subscribers. It creates the subscriber object in the manner specified by the subscribereither directly, queued, or with a monikerand passes on to each of them the method call that the publisher originally made.
A sample listing demonstrating this process is shown in Figure 12. The subscriber code is also quite similar to what would be used to call directly by the publisher instead of the event mechanism, as shown in Figure 13. That's all you need to tap into the COM+ event system. More on Subscriptions
More on Firing Events
Subscriber Parameter Filtering of Incoming Calls
|
Figure 16 Subscriber Filtering |
Publisher Filtering of Outgoing Calls
The example shown earlier demonstrated the simplest way of getting the job done. Using this approach makes it extremely easy to write and deploy your applications, but it may not always be as flexible as you need. In particular, the system as demonstrated offers no possibility of controlling the order in which subscribers receive their events, nor does it offer any chance for the publisher to refuse to deliver an event to a specific subscriber. These types of decisions are often part of a publisher's business logic. For example, a publisher might want to check its current financial records to see if a subscriber has paid the requisite fees before actually firing an event to that subscriber. Or the publisher might want to fire events to subscribers in a particular order; perhaps some subscribers have paid extra for priority notification. The default functionality of the event system does not handle these situations. For maximum flexibility, you need a way for the publisher to inject itself into the event system's logic at event firing time. The publisher can exercise fine-grained control over the event firing process by means of the IEventControl interface, whose methods are listed in Figure 17. Of particular interest is the method GetSubscriptions, which returns an enumerator object that the publisher can use to examine the subscribers for the event, determine whether to fire it, and determine the order in which subscribers should receive notification. The collection is self-updating; every time you walk it you get the current list of subscribers, with additions and removals being taken into account. A publisher can access the IEventControl interface by querying the event object. However, accessing it at this point raises several problems. First, it won't work if the event class itself is a Queued Component. This is an entirely reasonable design, as I'll explain later. The interface isn't queueable because it contains output parameters. Second, this strategy works only if it is compiled into the publisher program. It won't help you control a publisher for which you don't have the source code. If you use this approach, you won't be able to change your filter behavior without rebuilding your entire publisher program. The publisher control mechanism should work with Queued Components and without requiring modification of the publisher code. Fortunately, the COM+ event system provides a publisher filter mechanism that allows you to handle these cases. A publisher filter is a COM+ component that is installed downstream from the event object. Putting publisher modification code in a publisher filter means that it will work with Queued Components and third-party software. You tell COM+ which publisher filter to use for an event class by setting the event class's PublisherFilterCLSID property in the COM+ catalog. The Component Services snap-in contains no user interface for this; you will have to write your own administrative application to do it. When the publisher creates the event object, the event system reads the CLSID of the filter specified for that event class and creates an object of the filter class. If the filter object cannot be created, the creation of the event object fails. The filter class must implement the IPublisherFilter interface, whose methods are shown in Figure 18. If the event class supports more than one interface, the publisher filter must support the IMultiInterfacePublisherFilter interface instead. The IMultiInterfaceEventControl and IMultiInterfacePublisherFilter interfaces supersede IEventControl and IPublisherFilter and really should be used instead since they are no more complex. You are encouraged to implement IMultiInterfaceEventControl and IMultiInterfacePublisherFilter for composition with Queued Components and handling multiple interfaces on an event class. Here, however, I will discuss only the IPublisherFilter interface for simplicity. After creating the filter object, the event system will call the IPublisherFilter::Initialize method, passing the filter's IDispatch interface (which you query for the IEventControl interface or IMultiInterfaceEventControl) if filtering more than one interface. This interface is only passed once, in the Initialize method. Use this interface to obtain needed information (such as Enum interface on the collection of subscriptions), then release it before returning from the Initialize method. This is to avoid a circular reference between the filter object and the synthesized event system "agent" object, thus preventing the destructor of each from executing. When the publisher calls a method on the event interface exposed by the event object, the event system will call the filter's IPublisherFilter::PrepareToFire method. The first parameter specifies the name of the method to be fired, and the second is an interface pointer of type IFiringControl. This interface has one method named FireSubscription. You use it to tell the event system to deliver events to one subscriber. Calling this method invokes all of the event system's standard delivery mechanisms, which include the parameter filtering described earlier. At this point, you have two choices. If all your filter wants to do is use its business logic to swallow calls to certain recipients, your filter need not support the actual event interface that the publisher is calling; it only has to support IPublisherFilter. Your filter will include some mechanism for checking which subscribers are supposed to receive the event and which are not. Within the PrepareToFire method, call IFiringControl::FireSubscription for every subscription in the list that you want to deliver. You might, however, want your filter to perform more sophisticated logic based not just on the subscriber list, but also on the parameters passed by the publisher to this individual method. For example, you might want to deliver a stock price change event only if the new stock price is at least $5 above the cost of current holdings of that stock, which the publisher knows as part of its business logic. To perform filtering at this level, your filter class must support the event interface that it is filtering, in addition to IPublisherFilter. After calling PrepareToFire, the event system will query for this interface. If it finds the interface, the event system will then call the actual event method on your filter object. Within this method, you will look at the parameters, decide which subscribers you want to receive the event, and call FireSubscription for each subscriber. Using Events with Queued Components
Using Events with Transactions
Event System Security
Conclusion
|
For related information see: Com+Events at: http://msdn.microsoft.com/library/psdk/cossdk/pgservices_events_2y9f.htm. Also check http://msdn.microsoft.com for daily updates on developer programs, resources and events. |
From the September 1999 issue of Microsoft Systems Journal.
|