Sunday, October 18, 2009

Elements of Interception


3 4



Elements of Interception



COM+ performs all its transparent magic through a process known as interception. Calls from clients to objects are intercepted by an entity that implements the same interfaces as the object but adds services before passing the call to the object. It is easy to see how such a technique might regulate concurrency through locking: the interceptor could acquire a critical section before passing on the call. In fact, you might have used such a technique years ago while synchronizing access to a library that was not thread safe. We will examine interception implementation in more detail in a moment. For now, suffice it to say that COM+ always implements concurrency management through some form of interception. Because interception is such a central concept in COM+, we will step outside the bounds of examining concurrency management in this section to take a broader look at how interception is used in COM+ middleware.



Concurrency vs. Reentrancy



Do not confuse an object's tolerance for being accessed by multiple threads—perhaps simultaneously—with its ability to handle calls back to the object by a logical thread that was used to make a call from the object. If object A can handle a call by object B, which object A is currently in the process of calling (either directly or indirectly), object A is said to be reentrant.



It is not the place of object middleware to regulate reentrancy. Only your object design can ensure that your object will not be called back while waiting for an outgoing call to return, in the event it cannot handle such a call. Your design might need to provide this assurance because blocking a reentering call would guarantee deadlock. In fact, COM+ puts some effort into ensuring that callbacks are always serviced and therefore never result in deadlock. This implementation is more challenging than you might think, since a callback can occur on threads other than the one waiting for the return of the method invocation. We will look at this issue more closely when we examine locking (in its own section) later in this chapter.



Interception Implementation



The concept of interception is quite simple: an arbitrary object is wrapped so that some amount of work can be done before and after calling a method in an interface the object supports. The most familiar example of an interceptor is COM's venerable proxy. A proxy (or more precisely, proxy manager) acts like the object it represents, but its job is to marshal all call parameters for transmission through the channel to the stub, which will unmarshal the parameters, reassemble the stack frame, and then make the call to the actual object. Yet the traditional proxy (as opposed to the newer, stubless proxy) is not a perfect example of an interceptor, since it is not generic. The MIDL compiler generates functions that mirror each method of the interface the proxy wraps.



The generality of a true interceptor presents a challenge during implementation. A generic interceptor does not know the shape of the interface it will wrap once it is compiled. This lack of information at compile time creates a very interesting set of difficulties.



The Language Problem



Procedural high-level languages, including C and C++, generally are not capable of calling a function with an unknown parameter list. By using the ellipsis and va_ set of functions, you can implement a function that does not know what parameters it will be called with at compile time. However, you cannot tell the compiler to make a call to a function and simply pass the parameters that were passed to the function making the call.



This problem can be overcome only by using a piece of assembly language to make the call from the interceptor to the wrapped object. Essentially this assembly code must make the call to the target function in the wrapped object while leaving the stack frame unchanged. However, the C compiler will already have altered the stack frame with a standard function prologue segment, which lets you access local variables. Microsoft Visual C++ offers the __declspec(naked) storage class attribute, which will prevent function prologue and epilogue generation. Obviously, implementing naked functions is difficult and, along with the necessary assembly segment, requires a thorough understanding of the processor architecture for which you compile your code.



The Failure Problem



COM+ interface methods always use the __stdcall calling convention. This convention has the callee, not the caller, clean up the stack before returning from the function. This is no problem if you actually can make the call to the object for which you are intercepting, but what if your interception task fails? What if you would rather not make the call under a certain set of circumstances, or what if the object you want to dispatch the call to is unavailable? Now you are responsible for removing parameters from a stack frame whose shape you don't even know.



Of course, there are ways to get around this problem. For example, you might require the caller to tell you the combined size of all parameters. However, this approach is somewhat awkward and makes your interception hardly transparent. Or you might try to derive the combined parameter size by querying the ITypeInfo interface of the target object. Of course, the object might not support this interface, in which case you could attempt to create a stub for the interface you want to wrap, and interpret its CInterfaceStubVtbl structure, defined in RpcProxy.h. And your interceptor must create a stub and interpret the structure before your wrapper function is called, since determining stack frame size inside the wrapper cannot tolerate failure. By now you've probably guessed that doing this will require significant effort.



The Post-Processing Problem



Your interception task might require work before making the call to the wrapped object, as well as afterward. This means that after the wrapped function is complete, it must return to your wrapper rather than that wrapper's caller. Therefore, you need to change the return address on the stack so that it points within the wrapper function. But how do you remember the address of the caller to which you must return after you finish post processing? After all, there is no place on the stack to store this information.



You can save the final return address in thread local storage (TLS). But allocating a new TLS slot can be expensive if interceptor calls nest on the same thread, and you might find yourself running out of slots. Therefore, you should manage a stack of return addresses via a single slot, instead of allocating a new slot for each function invocation.



Make no mistake: implementing generic interception is very challenging and is nonportable. Even if you never need to implement an interceptor in your own software,2 understanding the issues of the task gives you a better grasp of what is happening inside the COM+ middleware, if not sympathy for the developers who created it.



The Apartment



The COM+ apartment model lets objects make a statement regarding their thread affinity. An in-process server makes this statement declaratively by setting the ThreadingModel named value under the InprocServer32 key under the class ID key in the registry, generally at registration time. Before the MTS COM era, the apartment defined an object's innermost execution context—that is, the COM run-time environment would never inject itself between objects that resided in the same apartment. COM+ allows each object to choose from one of the following apartment types:




  • The single-threaded apartment (STA). An object created in this apartment is entered only by the unique thread that comprises the apartment. A ThreadingModel value of Apartment indicates that an object requires instantiation within an STA. A user thread can create such an apartment by calling CoInitialize or CoInitializeEx with COINIT_APARTMENTTHREADED. Calls into the apartment are received by the channel via window messages; therefore, each user thread that creates this apartment type must service a message loop until no objects remain in the apartment. Otherwise, calls to objects in the apartment cannot be serviced and will block. Since STA objects can be entered only by their creating thread, no concurrency can exist within them. Microsoft Windows NT and Windows 2000 will place a new STA object in the system STA—unless the caller resides in an STA itself, in which case the new object will be co-located in the caller's apartment. The system STA is an apartment
    owned by a thread created by the COM/COM+ library. The library arranges for this thread to service a message loop for the lifetime of the process. At most, one system STA will be created per process. The system STA is the only STA that can be created in a process by COM preceding the MTS era.


  • The main single-threaded apartment. By omitting the ThreadingModel named value or setting it to Single, an in-process server's object indicates that the object requires instantiation within the unique main STA of a process. This main STA is formed by the first user thread that creates an STA. If no STA exists within a process yet, the system STA will become the main STA.

    Legacy in-process servers sometimes use this setting because their objects were written under the assumption that they could share global in-process server data without requiring locking. An ActiveX DLL created by Microsoft Visual Basic also supports this setting.3 The setting is not recommended for new projects because it can lead to contention among all the COM objects that are forced to share the same thread.



  • The multithreaded apartment (MTA). Objects with ThreadingModel set to Free are created in this apartment. There is only one such apartment per process, and user threads can join it by calling CoInitializeEx with COINIT_MULTITHREADED. Such threads need not service a message loop and can terminate at any time. Objects in the apartment receive calls on arbitrary threads created by the Remote Procedure Call (RPC) run-time library. This apartment type does not imply synchronization, and objects running under COM prior to MTS as well as unconfigured COM+ objects must prepare for concurrent entry by callers. Visual Basic 6 objects cannot use this setting.


  • The thread-neutral apartment (TNA). This apartment type is new in COM+. Its objects are entered directly by the caller's thread, whether it is an STA thread or belongs to the MTA. Threads cannot belong to this apartment; they merely enter it for the duration of a call sequence. Like the MTA, this apartment type does not imply synchronization. Unconfigured COM+ objects must prepare for concurrency. Visual Basic 6 does not support this setting.





An object can also declare ThreadingModel equal to Both, in which case it will be created in the apartment of its caller. The value Both is used for historical reasons: it originated at a time when COM supported only two apartment types. An unconfigured component using this setting might experience concurrency, as its creator might be an MTA thread or a TNA object. The primary motivation for using this setting is to eliminate an apartment boundary between an object and its instantiator.



Table 4-1 illustrates which apartment COM and COM+ will choose for instantiation of a new unconfigured object, given that object's ThreadingModel and the instantiating thread's apartment membership. (Of course, the TNA row and columns are relevant to COM+ only.)



Table 4-1 Instantiation Apartment Selection




















InstantiatorSingleApartmentFreeNeutralBoth
Main STA Main STA Main STA MTATNAMain STA
Secondary STAMain STA Caller's STAMTATNACaller's STA
MTAMain STASystem STAMTATNAMTA
On loan to TNAMain STASystem STAMTATNATNA





Whenever a thread invokes a method on an object across an apartment boundary, the invocation is intercepted by the object's proxy, routed via the channel, and then delegated to the object by the stub on the other side of the channel. The COM+ middleware performs three important functions when intercepting method calls across apartments:




  • Since apartment switches that do not involve the TNA imply thread switches, the proxy and stub are responsible for packaging the stack frame and reassembling it on the object's thread.


  • Notifying the target apartment about incoming COM+ traffic can involve sending window messages or some other interprocess communication (IPC) mechanism. This notification is the channel's job. Crossing an apartment boundary that necessitates a thread switch imposes significant overhead. A ThreadingModel value of Both will eliminate this overhead between instantiator and object, and the TNA will eliminate the overhead in all cases for subsequent callers from other apartments and for the instantiator.


  • Object references from the originating apartment are converted to proxies in the target apartment. This prevents a thread in the target apartment from crossing into the object's apartment without interception. The new proxy is always directly connected to its object's apartment and does not detour through the caller's apartment unless the object resides in the caller's apartment.





Making a call to an object in the TNA within the same process never requires switching to a different thread. Only the last item in the previous list needs to be performed by an interceptor guarding access to the in-process TNA. Such an interceptor is sometimes called a lightweight proxy. Compared to the overhead of a thread switch, a lightweight proxy is very fast. But the lightweight proxy still needs to perform object reference conversion, as shown in the graphic at the top of the next page. For this reason, a TNA interceptor needs access to the proxy/stub DLL of the interface it is encapsulating, or in the case of a type library-marshaled interface, to its type library. Such access is also necessary for the interceptor to handle the failure problem inherent in general interception.



One of the consequences of a user thread's inability to join the TNA is that the thread always incurs the overhead of at least a lightweight proxy when creating or calling an object within the thread-neutral apartment. This is not necessarily true for creating or accessing an object in one of the other apartment types. If a user thread performing a watchdog activity or other periodic task needs frequent access to COM+ objects, placing these objects in the TNA could impede performance. However, such situations are relatively rare and in most architectures are confined to "system" type objects rather than objects containing business logic.








Managing STA Concurrency



Since only a unique thread can enter an object created in a single-threaded apartment, an STA object naturally avoids concurrency. However, method invocations are serialized not only for an individual object in the apartment, but also for all objects created in the apartment, since they all share the same thread. Therefore, it can be important to actively control which STA will be chosen for an object instantiation; otherwise, you might end up with large groups of objects blocking one another from executing, even though such concurrency in separate objects would be perfectly all right. Often the only thing preventing two objects from executing concurrently is their need to access shared global data. Such data access frequently is better controlled by using explicit locking strategies (described later in this chapter), rather than using the somewhat heavy-handed approach COM+ has for invocation serialization. Given that a group of objects needs to reside in an STA, the question becomes how many objects
should share one apartment for optimal concurrency. The pressures to be balanced include each individual object's responsiveness as well as the amount of threads the system can handle before the thread scheduler's overhead becomes too significant.



Under normal circumstances, the instantiator of an object implicitly selects the object's apartment. But it is typical to see a client take control of in-process server concurrency by first creating a single-threaded apartment and then issuing the instantiation call from this new apartment. This approach can be effective in situations where the client process ultimately is aware of how server objects are being used, and how their concurrency can best be exploited.



MTA-Bound Call Interception

Since the introduction of the multithreaded apartment on Windows NT, COM developers frequently have asked why a thread executing within an STA object cannot call directly into the multithreaded apartment. With the addition of the thread-neutral apartment under COM+, the question becomes why a thread that originally created a single-threaded apartment cannot enter the multithreaded apartment, whether executing in its own apartment at the time of the call or on loan to the thread-neutral apartment when the call is dispatched. The question generally acknowledges that you would still need lightweight interception to prevent MTA threads from crossing over into the calling apartment when accessing object references passed as interface method arguments, but such interception should be feasible without incurring the expensive thread switch. After all, MTA objects are written to be entered by arbitrary threads and therefore it doesn't matter whether a calling thread actually belonged to a single-threaded
apartment.

The justification for switching threads involves an STA thread's need to service a message loop somewhat frequently, and with the expectation of the MTA object developer. This justification is not so much connected to the mechanism the channel uses when making an outbound call from the MTA: normally this mechanism involves blocking the calling thread until the method invocation returns, but the channel has the power to discover a thread's native apartment membership and can enter a message loop waiting for call return if the calling thread is an STA thread, even when making a call from an MTA object. The real problem is that the MTA programming model allows the object developer to unconditionally block threads and to do so for arbitrary periods of time—usually for synchronization purposes or when waiting to access a resource. Therefore, within MTA object implementations, you frequently will find calls to EnterCriticalSection, WaitForSingleObject, and
WaitForMultipleObjects
that have long time-outs. The STA thread must protect itself from running into code like this; it does so by waiting in a message loop at the apartment boundary and having an MTA thread block in the synchronization APIs instead.





In other situations, the server is best equipped for arranging object-to-apartment distribution. This includes cases where server objects do not run in the client's process space, making the client unable to affect target apartment selection. At first it might appear that the server is unable to influence this apartment selection, since COM+ makes all the choices. This impression might have been furthered by my choice of words: to simplify the discussion, I always speak of the apartment in which the object will be created. In fact, the object's class factory—not the object itself—will be instantiated in the apartment by the COM+ library. Therefore, it is up to the class factory to create the actual object. Normally the class factory creates the object inside its own apartment, but it does not have to. The indirection of the class factory in the object creation process gives servers the ability to take control over an STA object's target apartment.



Visual Basic allows developers to multiplex objects, created externally or internally by means other than New, across a thread pool of a fixed size on a per-project basis. Alternatively, developers can specify that each object created in this manner should be located in a new apartment. These options are available to local servers only. Of course, C++ developers implementing their factories manually can do whatever they like to control target apartment selection or creation. But when using the Active Template Library (ATL), you can achieve multiplexing across a pool by using the macro DECLARE_CLASSFACTORY_AUTO_THREAD. By default, the size of the thread pool used by this mechanism will be four times the number of processors of the system on which your code executes. This dynamic way of determining pool size makes more sense than the Visual Basic approach, because a pool that is too large will degrade performance as much as a pool that is too small will cause insufficient object concurrency.



The Context



With MTS, COM started taking on additional services that needed to be handled at the level of the interceptor, including transaction support and role-based security. Such services have expanded under COM+ while becoming more configurable. We will take a survey of these interception services in a moment.



Before the days of MTS, COM interception was married to the concept of the apartment, and apartments are about threads. But uniting the new services with the thread infrastructure did not make sense, so a tighter execution scope for COM objects was needed. This new innermost execution scope is called the context. An object now resides within a context, and an apartment bounds a context. Extended COM+ run-time services are performed by interceptors, which must exist between any two objects that do not reside in the same context. As long as the interception occurs between contexts in the same apartment or on a TNA-bound call, these interceptors generally are as efficient as any lightweight proxy that does not require a thread switch. But recall that the interceptor still needs access to your proxy/stub DLL or type library, for the same reason a lightweight proxy requires this access. Two objects with similar configurations and the same interception needs may share the same context, but certain services require that an object be created in an entirely new context. Threading model setting permitting, an unconfigured object, which, by its very nature does not ask for and is unaware of extended services, is always co-located in its instantiator's context.



Of particular interest is a COM+ service specifically dedicated to managing concurrency in an object. I have to admit that I was initially quite confused by this service. Having worked with the apartment model for years, the concepts of thread affinity and synchronization had become indistinguishable in my mind. But upon later reflection, I realized that while a relationship between the concepts exists, the concepts for the most part are independent. There is no reason, after all, to not serialize access to either a multithreaded or a thread-neutral object. In Essential COM (Addison-Wesley, 1998), Don Box speculated about a new apartment type he called the rental-threaded apartment (RTA). This apartment type would have behaved just like the thread-neutral apartment of COM+, but with synchronization built in. This is just the type of idea that was bound to emerge from a mindset that identifies apartments with concurrency management. Yet the decoupling of the COM thread management construct (the apartment) from the synchronization construct (contextual concurrency management) feels conceptually pure and gives us more flexibility: we now can build an object that any thread can enter (as the RTA would have allowed), but we can choose whether to synchronize method invocations (which the RTA would not have allowed).



Figure 4-1 shows all possible synchronization settings for an object. The values are identical to those available for configuring transaction support. However, instead of being enlisted in or beginning a new transaction, an object with the value Supported, Required, or Requires New participates in or begins what is known in COM+ as a synchronization domain. Under MTS, synchronization domains were known as activities and were configurable only through the method a client chose to instantiate another object. Unfortunately, it therefore was up to the client to determine whether a new object could join its activity. Under COM+, this has become transparent to the instantiator and is controlled solely by the setting in the property sheet shown in Figure 4-1.








Figure 4-1. Concurrency tab of the property sheet of a configured thread-neutral object.




Like a transaction, a synchronization domain can include objects in different contexts, apartments, processes, and hosts.4 Also, a synchronization domain is formed through the creation of an object with the setting Required (made by a caller currently outside any synchronization domain) or Requires New (made by any caller); the synchronization domain then flows to any object with the setting Supported or Required at instantiation time. In a synchronization domain, only one physical thread can execute at a time, and each thread must execute as the result of either a direct or indirect synchronous method invocation from the thread that first entered the domain. Figure 4-2 shows a synchronization domain that spans contexts and hosts with several physical threads.









Figure 4-2. Thread and synchronization domain interaction.




Threading Model and Synchronization Interaction

I have championed the fact that synchronization support and thread affinity are independent concepts and now are treated as such by COM+. And it is rather easy to see how synchronization can be applied (or not applied) to objects in either multithreaded or thread-neutral apartments. But understanding how synchronization is applied with the single-threaded apartment is a bit more challenging.5 After all, being single threaded already implies a certain natural synchronization across the entire apartment.

The fact is that objects in the single-threaded apartment, which do participate in a synchronization domain, act quite differently from objects that do not participate in a synchronization domain. These differences include the following:




  • An object in a synchronization domain will flow domain membership to any object it creates that supports or requires synchronization. As a result, a group of MTA or TNA objects that support synchronization will not experience concurrency when created by an STA object in a synchronization domain. But if the STA object did not participate in a synchronization domain, this group of objects will experience concurrency.


  • Synchronized STA objects cannot be entered by calls coming from a causality other than the one of the call chain currently executing within the synchronization domain. Unsynchronized STA objects can.


  • STA objects that do not require synchronization will be created in the same apartment as the single-threaded instantiator object. But if the instantiator is not inside a synchronization domain and the STA object requires synchronization, the object actually will be created in a different single-threaded apartment. The reverse does not hold, however. If the caller is inside a synchronization domain, the object will be created in the same apartment—even if it does not support synchronization.


As you can see, for the most part COM+ synchronization layers its benefits on top of single-threaded synchronization. Understanding this relationship certainly will be useful in your own projects.






An object with the synchronization setting Disabled is unaware of synchronization services and behaves like an unconfigured component. Notice that this setting is not the same as Not Supported: the latter ensures that an MTA or TNA object can receive calls concurrently, while the former can result in the object being located in the caller's synchronized context. Disabled also is not the same as the Supported setting, which will force the object into a context that participates in the same synchronization domain as it would were the caller a member of a synchronization domain. But if the object requires a different context than that of the caller (for example, because the object has a different threading model, or because of other COM+ service configurations), the contexts must communicate to prevent concurrent execution. COM+ might achieve this communication by having the contexts share some type of lock. But when the setting is Disabled, the target context will not participate in such a locking
scheme. This is why the Disabled setting truly has a unique meaning.



The Required and Requires New settings mean that the object must run in a synchronization domain. Requires New ensures the object always will be the root of a new synchronization domain. Required creates a new domain only if the instantiator does not already participate in one.



Synchronization Implementation by Deadlock



The theory behind context-based synchronization is fantastic, and the sheer number of options now available to developers should tremendously simplify situations that previously required you to build your own plumbing to achieve just the right concurrency behavior. However, when I examine the current COM+ implementation of the synchronization services for single-threaded objects, my enthusiasm for the technology wanes.



A call into an executing synchronization domain from a caller not participating in that synchronization domain can cause deadlock. A deadlock occurs if the call is made through an object in the synchronization domain whose threading model is Apartment and if that object's thread is currently servicing a message loop (for example, because it is waiting for an Object RPC call return). The message loop does not need to be within the bounds of the object being accessed concurrently for the deadlock to occur. These are the deadlocked entities:




  • The caller making the call from outside the synchronization domain.


  • The entire apartment of the thread receiving the inbound call of a new causality via its message loop, as well as any upstream callers waiting for the return of method invocations that this thread might be executing on behalf of. Such callers will be deadlocked whether they were part of the synchronization domain or whether the caller representing the initial causality was taking ownership of that synchronization domain.


  • The entire synchronization domain of the apartment-threaded object, as well as any threads waiting to enter the synchronization domain.





All these callers now are stalled because the message loop thread attempts to gain access to a lock on behalf of the new inbound caller. This lock never will become available because releasing it would require returning this very thread from a method invocation that is now further up the stack, as shown in Figure�4-3. Hence, the thread never can return from the DispatchMessage call in the message loop, and the call chain becomes stalled from that level upward.








Figure 4-3. Example of a deadlock.




This issue is mitigated by the fact that concurrency often is regulated by a layer of technology in front of the COM+ object layers in modern COM+ architectures. The sharing of object references is discouraged in such highly scalable environments. (For more on this topic, see Chapter 13.) Nevertheless, the issue begs the question of why synchronization support is even an option for STA objects when enforcing it is guaranteed to result in deadlock. It is important to understand the situation precisely, since by its very nature it likely will cause sporadic bugs under just the right timing conditions, and often will be extremely hard to debug. Until Microsoft solves this problem, the only safe thing to do is religiously avoid sharing object references to synchronized STA objects. If you must share object references, be absolutely certain that synchronous calls cannot occur in your architecture. One warning: making concessions at that level of your architecture is likely to introduce brittleness.



The Message Filter



Restricting access for a single-threaded object to one causality at a time is common. Such objects often contain an internal state associated with the operation in progress, and receiving a call unrelated to this current operation can cause failure in these objects. As we have seen, using COM+ synchronization services unfortunately is not yet a solution for this kind of problem. But an ancient mechanism is designed to deal with this type of concurrency: the message filter, which stems from the 16-bit world of OLE and is associated with concurrency in user interface applications. Such applications often share single-threaded objects representing graphical entities among clients. A message filter is intended to prevent access to such single-threaded objects when they perform some internal manipulation (perhaps as the result of an inbound COM+ call), and would become confused by new requests if their internal call sequence had not yet completed.



There is both a server and a client aspect of the message filter mechanism. Server and client applications can install their own message filter by calling CoRegisterMessageFilter, passing an object that implements the IMessageFilter interface:





IMessageFilter�:�public�IUnknown
{
public:
����virtual�DWORD�STDMETHODCALLTYPE�HandleInComingCall(�
��������/*�[in]�*/�DWORD�dwCallType,
��������/*�[in]�*/�HTASK�htaskCaller,
��������/*�[in]�*/�DWORD�dwTickCount,
��������/*�[in]�*/�LPINTERFACEINFO�lpInterfaceInfo)�=�0;

����virtual�DWORD�STDMETHODCALLTYPE�RetryRejectedCall(�
��������/*�[in]�*/�HTASK�htaskCallee,
��������/*�[in]�*/�DWORD�dwTickCount,
��������/*�[in]�*/�DWORD�dwRejectType)�=�0;

����virtual�DWORD�STDMETHODCALLTYPE�MessagePending(�
��������/*�[in]�*/�HTASK�htaskCallee,
��������/*�[in]�*/�DWORD�dwTickCount,
��������/*�[in]�*/�DWORD�dwPendingType)�=�0;

};



On the server side, COM+ will call HandleInComingCall before dispatching an inbound call to an object. On the client side, COM+ will call RetryRejectedCall when the server does not dispatch the call but flat out rejects it or advises retrying it later. The code calls MessagePending when Windows messages are received by the client while waiting for a COM+ call to return. The dwCallType parameter of HandleInComingCall lets the server know whether an incoming call is from a new causality while the object's thread waits for an outgoing call to return (CALLTYPE_TOPLEVEL_CALLPENDING). Such calls therefore are deferred easily (return SERVERCALL_RETRYLATER).


This all sounds marvelous, but there are some limitations. First, the message filter shows its user interface orientation by allowing only local servers, not DLL components, to register a message filter. This means no configured object can use this technique and therefore excludes today's most popular and flexible kind of COM+ component. Second, unless all clients can be guaranteed to be in process, rejecting a call with the retry flag can put an unacceptable burden on the caller. Unless the client's message filter specifies that such a rejected call should indeed be retried, an error message immediately will be returned to the caller. This caller might not be able to set its own message filter—for example, it might have been loaded from a DLL in another process or host, or it might have been implemented in a development system that does not permit setting a message filter. It is not reasonable to expect that all clients either have a message filter that retries or performs the retry at the
level of every single COM+ method invocation. A Visual Basic Standard EXE will install a message filter that does retry for some time and then uses OleUIBusy to bring up the infamous Server Busy dialog box, giving the user a chance to manipulate the user interface of the server application and thus resolve the impasse. But the default COM+ message filter does not do this, instead it propagates all retry rejections directly to the caller. Therefore, even COM+ objects implemented in Visual Basic are subject to this error when operating in a host process implemented with, say, Visual C++.


The bottom line is that message filters were designed in another era, for a problem more specific than regulating general STA object concurrency. If message filters happen to be applicable to your situation, great! By all means use them—they won't go away any time soon. But chances are that forcing message filters into a modern architecture will be like trying to fit square pegs into round holes.



Interception Services



Synchronization is only one service that COM+ performs at the level of the interceptor. COM+ configures lightweight proxies between contexts to perform the specific adjustments to the environment necessary for the crossover. For example, suppose context A has the same synchronization settings as context B but does not support transactions, while context B requires transactions. A lightweight proxy in context A representing an object in context B would create a new transaction but would not attempt to acquire the shared synchronization domain lock. This is part of the reason an object reference can be used only in the context in which it was created.



Now let's take a look at some of the other COM+ services performed by interceptors:




  • Figure 4-4 shows the transaction support configuration of COM+. All settings except Disabled are familiar from MTS. Disabled simulates the behavior of an unconfigured object, with respect to transactions. In addition, you now can set the transaction timeout on a per-object basis (rather than a per-machine basis). The following grid shows which combinations of instantiator context and transaction support settings will force a new object into a new context. Note that even if the transaction aspect does not force a new context, one still might be required as the result of other settings.













    Caller ContextDisabledNot SupportedSupportedRequiredRequires New
    Has transactionxxxx
    Does not have transactionxxx






  • Object security configuration is also familiar from MTS. If security is enabled for the application, only users who are members of the roles checked in Figure 4-5 or those roles granted access at the interface and method levels will be able to call the object. An object with security checks enabled always will be created in its own context.








    Figure 4-4. Transactions tab of the property sheet of a configured object.









    Figure 4-5. Security tab of the property sheet of a configured object.





  • Just-in-time activation was present but unconfigurable under MTS. It is now controlled by the similarly named check box shown in Figure�46. When this option is set, the object will be created in its own context because you need a unique interceptor to create the object instance when its first method is invoked.








    Figure 4-6. Activation tab of the property sheet of a configured thread-neutral object.




  • Selecting the pooling check box shown in Figure 4-6 enables object pooling. Object pooling can be described as the opposite of just-in-time activation: object instances are returned to a pool instead of being destroyed when an object is deactivated. Interfaces related to pooling were defined under MTS (an object could request to be pooled by implementing IObjectControl in a particular fashion), but the pooling mechanism itself had not yet been implemented. Be sure to review the documentation carefully before checking this box: the system makes very specific demands of the implementation of a pooled object. You also can violate the isolation property of transactions by carrying state in a pooled object. Pooling likely will be effective only in somewhat specialized circumstances.





An object's threading model can have an effect on what services will be available to it. For example, single-threaded objects cannot be pooled. Interdependency also exists among certain services. For instance, supporting or requiring transactions (including new ones) forces an object to use just-in-time activation. The specific settings of Supported and Required also force a setting of Required for synchronization support. The transaction setting Requires New implies the setting Required or Requires New for synchronization support. Apart from that, enabling just-in-time activation also demands that the object require an existing or new synchronization domain, regardless of the transaction setting.



No comments:

Post a Comment