Microsoft Component Services

[Previous] [Next]

One of the most common complaints about COM during the mid-1990s was that many of the services already available through the Common Object Request Broker Architecture (CORBA) were unavailable through COM. Nobody had coded these services to make distributed computing using COM a reality.

Microsoft Transaction Server (MTS) finally emerged from Redmond in 1997. Although people had been writing transaction systems for years, no viable, standard distributed transaction system had been built on COM. To play in the enterprise arena, Microsoft needed something like MTS, which is now called Microsoft Component Services in Windows 2000. In this section, we'll cover how transactions work in general and then how Microsoft Component Services uses interception and context to make writing transaction software easier as well as how Windows 2000 handles issues such as concurrency.

Transactions

Moving from an understanding of stand-alone systems (which is what many Windows developers have been working with for the last 10 years) to an understanding of distributed transaction systems requires some new thinking. When a simple client application talked to a single database (or some other data source), transactions were simple—either they worked or they didn't. Now that developers are creating distributed systems, transactions have become fairly complex.

A transaction is a unit of work done on behalf of a client. It is initiated by some sort of transaction application and can have multiple participants that determine its overall success or failure. For example, consider the process of ordering a product from Domain.com. Lots of disparate pieces of the process have to function for the entire operation to be successful—a client's credit must be good, the product has to be available, and the request to ship the product has to be received. Otherwise, the operation isn't completed. Transactions always end in only one of two outcomes: success or failure.

As our example illustrates, a system that can be in a number of states is broken up into lots of little pieces. The state of a transaction system can exist simultaneously in the user tier, the middle tier, and the database, and might need to service many users at the same time. With so many parts to the system, drawing distinct lines between the state transitions is imperative—especially because there can be multiple points of failure. When performing transactions, the system must be able to maintain consistent states and introduce checkpoints along the way so that the users of the system always get accurate answers (that is, the product was shipped from Domain.com or it wasn't).

To maintain system integrity and ensure successful transactions, transaction systems introduce four properties for transactions, known collectively as the ACID properties. ACID stands for atomic, consistent, isolated, and durable:

  • Atomic The operation can't be interrupted.
  • Consistent The operation always leaves the system in a consistent state—that is, there are no dangling references to resources and no incomplete results.
  • Isolated The operation's outcome doesn't affect other operations.
  • Durable The operation keeps track of changes in the system so that the system can be brought back online immediately even after unusual failures (such as a power loss).

The Distributed Transaction Coordinator

At the heart of Microsoft Component Services lies the Microsoft Distributed Transaction Coordinator (DTC). One DTC per machine is involved in a transaction. The job of the DTC is to dispense transactions and coordinate work between the client and various resource managers. Resource managers are durable state managers. (Microsoft SQL Server is a good example.)

The DTC has its own API revealed through a number of COM interfaces. In fact, you can program transaction systems manually by using the interfaces exposed by the DTC. A client application programming to the DTC at a lower level typically enlists one or more resource managers on a new transaction. Then the client application submits requests to the resource managers. These state changes are "charged" against the current transaction. When the work is done, the client application closes the transaction through the transaction interface ITransaction, shown in the following code. ITransaction has two main functions, Commit and Abort.

 interface ITransaction : IUnknown {     HRESULT Commit(         [in] BOOL fRetaining, [in] DWORD grfTC,              [in] DWORD grfRM);      HRESULT Abort(          [in] BOID *pboidReason,          [in] BOOL fRetaining,            [in] BOOL fAsync);     HRESULT GetTransactionInfo([out] XACTTRANSINFO *pinfo); }; 

When a transaction closes, the result is either a new system state or the original composite state of the application and all the resource managers involved. Again, here we have the basis of transaction programming—either an operation succeeds in changing the state of the system or it doesn't.

Before moving on to how Microsoft Component Services works, let's take a look at a protocol designed to assure that operations are atomic.

Two-Phase Commit Strategy

To help ensure atomicity, a transaction system usually employs a two-phase commit strategy when changing the state of the system. The transaction initiator attempts to commit the transaction, initiating the two-phase commit protocol.

Phase one is the vote collection. The transaction manager polls each resource manager, and each votes to commit or abort based on work done within the transaction and whether the state of the system is still intact.

Once all votes have been collected, the transaction enters phase two, called the notification phase. During this phase, the transaction manager notifies all resource managers of the transaction outcome (success or failure), and the resource manager then commits or cancels the changes to keep the system intact.

Configured Components and Packages

As you learned earlier, the goal of interception and context-based development is to reduce the amount of code you have to write. We'll begin relying on the system to provide extended services, such as security and synchronization, by using declarative attributes. Classes that want extended services declare this desire as part of their registration process. These classes become configured components. Configured components are always DLLs, and Microsoft Component Services uses a surrogate to manage remote and local (out-of-process) activation.

As you know, Microsoft Component Services is layered on top of classic COM using interception and contexts. The capability to participate in a transaction, then, is layered onto existing COM objects. The transaction services provided by Microsoft Component Services live in the executive named MTXEX.DLL. When a Microsoft Component Services component registers itself, the component points its InProcServer32 key toward MTXEX.DLL. So when you call CoCreateInstance to retrieve a COM object, the system fires up MTXEX.DLL and the Microsoft Component Services DLL wraps your object.

The Microsoft Component Services catalog also separates classes into packages. Packages are analogous to COM applications. They're logical groupings of COM classes sharing activation and security settings. You can set up these logical groupings programmatically or by using the Microsoft Component Services Explorer. Packaging is a useful deployment tool—it makes it easier to get your application out there.

Once each component is configured within the catalog, Microsoft Component Services can create an interceptor (a proxy) between client and object based on information in the catalog. In this way, each Microsoft Component Services object is protected from concurrent access and has a say about participating in a transaction. Let's first look at how context protects an object from concurrent access and then at how contexts and transactions work in Microsoft Component Services.

Microsoft Component Services Contexts and Component Synchronization

In earlier chapters, we discussed COM's various apartment models. The original impetus behind apartments was to guard a component from concurrent access by multiple threads if the component wanted that protection. This story changes somewhat with Windows 2000. In Windows 2000, concurrency requirements are identified by a component's configuration and implemented by context and interception.

In Windows 2000, you can configure classes to use system-provided call serialization by using the Synchronization attribute. When a component is configured to require synchronization, COM puts the context in which the component lives into a synchronization boundary called an activity. An activity is just a group of one or more contexts in which no concurrent execution is desired. (An activity is similar to an apartment in Windows NT 4.0.) Each context belongs to at most one activity, and each new context belongs to the context creator's activity, a new activity, or no activity. Contexts that share activities share the lock with the creating context, whereas new activities maintain an independent lock. Contexts that reside outside an activity allow concurrency. Table 17-1 shows the effects of the various synchronization attributes.

Table 17-1. Windows 2000 Synchronization Attributes.

Synchronization Setting for the New Class Context in Activity Share Creator's Activity
NOT_SUPPORTED Never Never
SUPPORTED If creator in activity If creator in activity
REQUIRED Always If creator in activity
REQUIRES_NEW Always Never

Activities prevent concurrent access to a component in this way: The system maintains a set of activity data structures for each process—each activity maintains its own exclusive lock. Then each context maintains a reference to the activity it belongs to (if it belongs to an activity). When a call is made into the object, the interceptor tries to acquire the activity's lock before invoking the function. Then the interceptor releases the lock after the call finishes to allow other callers to enter the activity.

Microsoft Component Services Contexts and Transaction Services

When Microsoft Component Services (or Microsoft Transaction Server in Windows NT 4.0 and earlier) creates a transaction object, it inserts an interceptor between your client code and the object. The interceptor exists to establish a proper run-time environment, a context, based on class attributes in the catalog.

Remember that a context is a group of compatible objects within a process. All objects within a single context share a similar worldview—all objects in a single context have compatible attributes such as call synchronization and security settings. In fact, contexts are represented by real living, breathing objects that you can actually touch through interfaces (as we saw in the code for IObjectContext). The thread-local storage for a thread running within a Microsoft Component Services context has a pointer to a context object (which you can easily retrieve by calling GetObjectContext).

The following code shows how to use the context object to determine whether it's involved in a transaction, use it to do some work, and then let it know about the state of the transaction. The code shows how the transaction services are available through the object's context.

 STDMETHODIMP CSomeObject::DoSomeStuff(void) {     IObjectContext *pObjectContext = 0;     HRESULT hr = GetObjectContext(&pObjectContext);     if(SUCCEEDED(hr))     {         if(pObjectContext->IsInTransaction())          {             hr = this->DoMoreStuff();             if(SUCCEEDED(hr))             {                 pObjectContext->SetComplete();             } else             {                 pObjectContext->SetAbort();             }         } else         {             hr = E_UNEXPECTED;         }     pObjectContext->Release();     }     return hr; } 

Now let's take a closer look at how transaction objects and functions work. Then we'll look at how ATL and Microsoft Component Services fit together.

Transaction Objects and Functions

All the COM objects we've been running into until now have been regular old COM objects that don't do anything special in terms of transactions. By default, objects run outside transactions. Of course, you can configure a class to support instances running inside a transaction (by adding an attribute to your coclass statement in the IDL or through the Microsoft Component Services Explorer). Once an object is declared as a transaction object, the object can influence the outcome of a transaction.

Remember the ITransaction interface we looked at a bit earlier? Whenever you create a transaction COM object (which is wrapped in a transaction context), the context stores a pointer to ITransaction as a data member. The resource manager proxies use the ITransaction pointer to enlist resource managers in the transaction.

In some cases, a transaction might need to involve multiple transaction objects. To allow this, Microsoft Component Services includes the concept of a transaction stream. A transaction stream is a group of contexts that share a transaction. Transaction streams are important because they support the isolated property of ACID operations. The first transaction context created in a transaction stream is called the root of the stream. Microsoft Component Services starts a transaction when the first method is called on an object that is involved in a transaction stream, and it ends the transaction when the root object deactivates. The Microsoft Component Services run time tries to commit a transaction when the root object is deactivated.

Microsoft Component Services transactions are based on passive consent. Here's how they work: after an object enters a transaction, it goes about and does its thing. The transaction context maintains a flag that indicates whether or not the object is "happy" with the work the object has done. (The flag can be managed by the object.) When the run time wants to commit a transaction, it examines the flags within the context wrappers. If any of the objects has indicated an inconsistent or "unhappy" state, the run time rolls the transaction back. If and only if all the contexts are "happy" can the transaction roll forward.

In addition to indicating consistency, a flag included by the context wrapper indicates whether or not the object has completed its work. These two flags are controlled through the IObjectContext interface, shown here:

 interface IObjectContext : public IUnknown {      HRESULT SetComplete(void) = 0;     HRESULT SetAbort(void) = 0;     HRESULT EnableCommit(void) = 0;     HRESULT DisableCommit(void) = 0;  }; 

Each context has two flags, so four permutations are possible. Permutations are managed by the four functions SetComplete, SetAbort, EnableCommit, and DisableCommit:

  • SetComplete sets both flags to TRUE, meaning that everything is in a consistent state and the object's work is done. If the object is the root object, a setting of TRUE commits the transaction.
  • SetAbort sets the consistency flag to FALSE and the "done" bit to TRUE, causing the transaction to abort.
  • EnableCommit sets the consistency flag to TRUE and the "done" bit to FALSE. This setting means everything is fine within the object and that it would be OK to commit the transaction at that point.
  • DisableCommit sets both flags to FALSE. This tells the system to abort the transaction if the object doesn't get any more calls.

To review, in the Microsoft Component Services model, an object declares its intent to be involved in a transaction. It does so either at deployment time through the Microsoft Component Services Explorer or in the type library. When an object declares its need to be involved in a transaction, a transaction-enabled context joins the object when the object is created. As an object goes about its business, it can choose to set the "happy" and the "done" flags, which ultimately have an impact on the outcome of the transaction.

Creating Microsoft Component Services Objects Using ATL

As we saw in Chapter 9, the ATL Object Wizard lets you create all sorts of COM classes. One of those classes is an MS Transaction Server Component. An MS Transaction Server Component object is a standard ATL object that derives from the normal ATL COM implementations CComObjectRootEx and CComCoClass.

The default MS Transaction Server Component object created by the Object Wizard doesn't do anything special. It implements IUnknown using the regular ATL mechanism. If you choose to, you can create an object that implements the IObjectControl interface, which notifies you when the object is created and destroyed. You can also choose whether or not you want your object pooled when it implements IObjectControl.

Microsoft Component Services supports the notion of just-in-time activation (JITA), which helps objects maintain isolation and is a useful programming tool for postponing initialization until the first method is called. JITA objects implement IObjectControl to receive activation/deactivation notifications. To implement IObjectControl when generating an MS Transaction Server Component object, click the ATL Object Wizard's MTS tab and check the Support IObjectControl check box. The wizard yields an object with the IObjectControl interface turned on. The following code shows the basic object implementing IObjectControl.

 class ATL_NO_VTABLE CMTSObjectIObjectControl :      public CComObjectRootEx<CComSingleThreadModel>,     public CComCoClass<CMTSObjectIObjectControl,         &CLSID_MTSObjectIObjectControl>,     public IObjectControl,     public IDispatchImpl<IMTSObjectIObjectControl,         &IID_IMTSObjectIObjectControl,          &LIBID_MTSTESTLib> { public:     CMTSObjectIObjectControl()     {     } DECLARE_REGISTRY_RESOURCEID(IDR_MTSOBJECTIOBJECTCONTROL) DECLARE_PROTECT_FINAL_CONSTRUCT() DECLARE_NOT_AGGREGATABLE(CMTSObjectIObjectControl) BEGIN_COM_MAP(CMTSObjectIObjectControl)     COM_INTERFACE_ENTRY(IMTSObjectIObjectControl)     COM_INTERFACE_ENTRY(IObjectControl)     COM_INTERFACE_ENTRY(IDispatch) END_COM_MAP() // IObjectControl public:     STDMETHOD(Activate)()     {         HRESULT hr = GetObjectContext(&m_spObjectContext);         if(SUCCEEDED(hr))             return S_OK;         return hr;     }      STDMETHOD_(BOOL, CanBePooled)()     {         return TRUE;     }      STDMETHOD_(void, Deactivate)()     {         m_spObjectContext.Release();     }     CComPtr<IObjectContext> m_spObjectContext; // IMTSObjectIObjectControl public: }; 

Notice that the class returns TRUE for the CanBePooled method. Objects that return TRUE from CanBePooled will be recycled. Announcing that your object might be pooled lets you recycle objects, which is useful when initializing your object is expensive and generic. One caveat of object pooling is that the object must be context-neutral between activations, because the next activation could occur in any context, thread, transaction, or client in the process. Deactivating and activating your object is the moral equivalent of resetting your object.



Inside Atl
Inside ATL (Programming Languages/C)
ISBN: 1572318589
EAN: 2147483647
Year: 1998
Pages: 127

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net