Features of Serviced Components

I l @ ve RuBoard

Features of Serviced Components

Now that we've covered the bare essentials of implementing a serviced component, it's worth taking a look at the other features surrounding COM+ development.

Synchronization, Activities, and Context

As you might recall from Chapter 8, multithreaded objects can use synchronization to prevent two concurrent threads from accessing a shared resource simultaneously , possibly corrupting data. This is achieved by controlling, and possibly prohibiting, concurrent calls to methods of the same object. The same feature is available to serviced components, through System.EnterpriseServices.SynchronizationAttribute (not to be confused with System.Run ­time.Remoting.Contexts.SynchronizationAttribute , which offers similar functionality but does not operate with COM+). The situation in COM+ is more complex than the cases examined in Chapter 8, however, because multiple threads can execute across multiple computers. (COM+ components can be remote.) If synchronizing multiple threads is hard, synchronizing multiple distributed threads is 10 times as hard. Fortunately, COM+ and serviced components provide a mechanism to ease the burden on the poor developer.

In COM+ parlance, a set of objects performing work on behalf of the same client is referred to as an activity . Synchronization is used to control how objects that are part of the same activity interleave their execution.

Note

A client can have several concurrent activities, but components do not have to execute in the context of an activity. (See Table 14-2 for an explanation.)


The SynchronizationAttribute class uses values from the System.Enterprise ­Services.SynchronizationOption enumeration. SynchronizationAttribute is applied to a component, as shown here:

 /** @attribute SynchronizationAttribute(SynchronizationOption.Required) */ public class MyComponent extends ServicedComponent implements IMyInterface {     } 

Synchronization is accomplished by the COM+ runtime through locks. Each activity has a lock, and when a call is made into an activity from outside, the calling thread might make an attempt to obtain this lock, depending on how synchronization is configured. If the lock is currently held by another thread, the calling thread might block until the lock becomes available. When the lock is released, a blocked thread can acquire it and continue processing. You should note that there are no features such as timeouts ”once a thread requests the activity lock, it will block indefinitely until the lock is available.

Table 14-2 summarizes the SynchronizationOption values available and how they affect the concurrency and context hosting a serviced component. A context belongs to at most one activity, but an activity can contain multiple contexts. As with transactions, if a serviced component is instantiated in a context whose synchronization settings are incompatible with the component's requirements, a new context will be created to hold the serviced component.

Table 14-2. Synchronization Options for Serviced Components

Option

Description

SynchronizationOption.Disabled

No synchronization is used. Threads executing methods in the component will not acquire the activity lock. The component will operate in the current activity regardless of the synchronization settings used by other components already existing in this context. The current context will also be used, unless the TransactionAttribute setting is incompatible with the current context when a new context will be created.

SynchronizationOption.NotSupported

No synchronization is used, and the component will not form part of any activity. Threads executing methods of the component will not acquire the activity lock. You can use this setting only if the component is not transactional and does not use COM+ JIT activation (see later in this chapter).

SynchronizationOption.Required

If a synchronized activity already exists, it will be used. (A new context might need to be created in this activity, depending on the transactional attributes of the class.) If not, a new activity containing a new context will be created. Method calls to the component will acquire the activity lock.

SynchronizationOption.RequiresNew

A new synchronized activity and context will always be created. Method calls to the component will acquire the activity lock.

SynchronizationOption.Supported

If an activity already exists, it will be used and method calls will acquire the activity lock. If not, method calls to the component will proceed without synchronization. (You might need to implement your own locking scheme if you want to protect data encapsulated within the component!)

An example using synchronization is supplied in the Stock class (in the file StockControl.jsl in the project COMPlusStockControl). The Stock class available in this project is a variation on the Stock class used in the examples in Chapter 8, but it is implemented as a serviced component that implements the IStock interface defined in the same project. A Stock object keeps track of how many items of a particular type are available in a supplier's warehouse. The IStock interface defines methods for increasing the number in stock, reducing the number in stock, or just querying the number in stock:

 // IStock interface - models stock items in a warehouse /** @attribute ComVisibleAttribute(true) */ public interface IStock {    /** @property */    public int get_numInStock();    public int addToNumInStock(int howMany);    public int reduceNumInStock(int byHowMany) throws System.Exception; } 

The Stock class itself uses a private instance variable called numberInStock to hold the volume of the stock item available in the warehouse. (In a production application, this information would be held in a database rather than being maintained as internal state in the Stock class ”the reasons for this will be explained later.) The addToNumInStock and reduceNumInStock methods manipulate this variable:

 public class Stock extends ServicedComponent implements IStock {    private int numberInStock = 0;        // Restock the warehouse    public int addToNumInStock(int howMany)    {       this.numberInStock += howMany;       return this.numberInStock;    }    // Remove stock from warehouse    public int reduceNumInStock(int byHowMany) throws System.Exception    {       if (this.numberInStock >= byHowMany)       {          this.numberInStock -= byHowMany;          return this.numberInStock;       }       else       {          throw new System.Exception            ("Insufficient goods in stock - please reorder");       }    }     } 

If the addNumInStock and reduceNumInStock methods were allowed to execute concurrently over the same instance of the Stock class, the numberInStock variable could become corrupted. (Remember that the += and - = operators comprise several MSIL instructions and are not atomic.) This can also occur if reduceNumInStock is executed concurrently by two separate threads, and the same is true for the addNumInStock method. The Stock class is therefore tagged with SynchronizationAttribute , using the value SynchronizationOption.RequiresNew . This setting creates a new synchronized context for each instance of the Stock component. Concurrent calls to the same instance of the object will be serialized, but calls to other instances (and to other objects) can proceed in parallel:

 /** @attribute SynchronizationAttribute(SynchronizationOption.RequiresNew) */ /** @attribute ClassInterfaceAttribute(ClassInterfaceType.None) */ public class Stock extends ServicedComponent implements IStock {     } 

The StockController class, in the same project, is a test harness that creates a Stock object and then spawns two threads that manipulate the Stock object. (A COM+ application called Stock Control Demo, which contains the Stock serviced component, will be created automatically if you build and execute the project.)

Static Methods

When we looked at the .NET Remoting architecture in Chapter 11, you learned that the static methods of an object are never executed remotely ”instead, they're run locally, in the client. The same is true of static methods exposed by serviced components. To be honest, you should really avoid using them if possible. The reason is that they're not available through COM+ (they don't appear in the Component Services console) or through a type library and cannot be exposed through an interface ”they can therefore be invoked only by managed clients because you need a reference to the managed assembly to see them.

Serviced Component Activation

One good reason to build serviced components is the inherent support they provide for scalability. Serviced components can be JIT-activated and pooled. Well-designed serviced components are highly reusable objects.

JIT Activation

You should not confuse JIT activation of a serviced component with JIT compilation used by the common language runtime ”they are distinct concepts that both happen to be performed "just in time." JIT activation is used to conserve resources required by a serviced component on a computer that can potentially host thousands of instances of the same component.

A typical client application can instantiate a serviced component and invoke its methods. There might be a significant period of time between method calls, and keeping a serviced component active in the intervening time can be expensive in terms of the memory required. The alternative is to force client applications to instantiate serviced components as required and then destroy them after each method call, creating and destroying further instances as each method call is made. This approach might save memory on the host computer, but at the cost of increased client complexity and the overhead of continually creating and destroying objects (not to mention the volume of extra network traffic generated).

JIT activation permits an inactive serviced component to be destroyed but transparently re-created if the client invokes one of its methods. This act of deception is achieved through the proxies and stubs used by COM+. When a client instantiates a serviced component, the COM+ runtime creates a proxy that is handed to the client and a stub that executes on the server. Method calls issued by the client pass through the proxy and are marshaled (possibly over the network if the serviced component is remote) to the stub. The stub unmarshals the method call and invokes the real serviced component. Return values and output parameters are sent back from the serviced component, through the stub and proxy, and eventually end up in the client.

So far, this is not much different from the generic RPC architecture employed by COM, CORBA, RMI, and even .NET Remoting. The trick with JIT activation comes when the method call completes. If the serviced component has finished its work and does not need to maintain any state information, the COM+ runtime might destroy the serviced component. (If and when this occurs is actually up to the runtime.) However, the server-side stub and the client-side proxy will remain intact. If the client invokes another method on the serviced component, the method call will pass through the proxy to the stub, which will create a new instance of the serviced component if the previously used instance was destroyed ” otherwise the existing instance will be used, and then call the requested method. When the method call completes, the serviced component instance might be destroyed again. The client will not be aware of any of this chicanery.

Note

The COM+ server-side stub also includes context information; the context is retained between method calls on the server-side even though the objects contained by the context can disappear.


You can indicate that a serviced component supports JIT activation by tagging it with System.EnterpriseServices.JustInTimeActivationAttribute , as shown in the Widget class below:

 /** @attribute JustInTimeActivationAttribute(true) */ public class Widget extends ServicedComponent {     } 
State Management

JIT activation sounds great, but there's one question that we need to answer: How does the COM+ runtime know that a serviced component has completed its work and has no state that needs to be preserved? A serviced component might define methods that populate and query data held inside the component, and a client might rightfully expect that if it stores a value in an object, that value will still be there when the object is next accessed.

To solve this problem, each serviced component contains a flag called the done bit . You can arrange for this flag to be set when you've finished with an object and are happy for it to be discarded, or you can leave the flag clear if you want the object to remain active. Typically, you either set or clear the done bit during each method for a JIT activated component.

The done bit is not directly accessible, however. You can manipulate and query the done bit by using the static Boolean DeactivateOnReturn property ( get / set_DeactivateOnReturn in J#) of the ContextUtil class:

 ContextUtil.set_DeactivateOnReturn(true); // Indicate "doneness" 

Note

Serviced components that use transactions are automatically JIT-activated. The static SetComplete and SetAbort methods of the ContextUtil class set the done bit and will deactivate a JIT-activated object when the calling method completes. Tagging a method with AutoCompleteAttribute will also set the done bit.


The Widget class (in Widget.jsl in the JITWidget project) shows an example of a JIT activated serviced component. A widget is a highly prized piece of mock-Medieval earthenware, produced only by the skilled craftsmen of Trottiscliffe Moor in West Lumockshire. During the manufacturing process, the craftsmen take a lump of grey Lumockshire clay and model it into a widget of a specified length and breadth. The widget is then painted , and optionally varnished, before being delivered to the customer.

The Widget class contains methods ( SetWidgetDimensions , PaintWidget , and VarnishWidget ) that model the manufacturing process. A Widget object has some internal state (length, breadth, color , varnished flag) that must be maintained between method calls as the widget is created. These methods set the done bit to false . (This is actually the default state, but it does not hurt to be explicit.) Only when the widget has been completed and dispatched to the customer can the state information be discarded. (The PrintWidget method is used to create the dispatch note; it also sets the done bit to true .)

The completed Widget class is shown here:

 /** @attribute ComVisibleAttribute(true) */ public interface IWidget {    public void SetWidgetDimensions(int length, int breadth);    public void PaintWidget(String color);    public void VarnishWidget(boolean varnished);    public String PrintWidget(); } /** @attribute ClassInterfaceAttribute(ClassInterfaceType.None) */ /** @attribute JustInTimeActivationAttribute(true) */ public class Widget extends ServicedComponent implements IWidget {    private int length, breadth;    private String color;    private boolean varnished;    public void SetWidgetDimensions(int length, int breadth)    {       this.length = length;       this.breadth = breadth;       ContextUtil.set_DeactivateOnReturn(false);    }    public void PaintWidget(String color)    {       this.color = color;       ContextUtil.set_DeactivateOnReturn(false);    }    public void VarnishWidget(boolean varnished)    {       this.varnished = varnished;       ContextUtil.set_DeactivateOnReturn(false);    }    public String PrintWidget()    {       ContextUtil.set_DeactivateOnReturn(true);       return this.color + " widget, size " + this.length +           " x " + this.breadth + " ready for delivery";    } } 

The WidgetClient class is a test harness that creates a Widget object, invokes the methods needed to manufacture a widget, and then calls the PrintWidget method to display the details. Because the WidgetClient and Widget classes are part of the same project, you don't need to deploy the Widget class to the GAC to build and run the test harness.

An interesting exercise is to invoke PrintWidget twice. The first time, you'll see a message indicating the state of the widget. The second time, the state will have been discarded and you'll see the default values for the widget's state instead, as shown in Figure 14-15.

Figure 14-15. The output of the WidgetClient class

Object Pooling

By default, when a serviced component is deactivated, it is destroyed. If another instance of the same component is required, it must be constructed from scratch. Object construction can be an expensive process ”the common language runtime has to obtain a chunk of memory from its heap, locate the assembly that defines the class to be instantiated, load the assembly, create the new object, execute its default constructor, and perform any other initialization required.

A more scalable solution is to use object pooling. A pool of objects can be created by the COM+ application, and objects from this pool can be handed to clients as needed. When an object is deactivated, rather than being destroyed it can be returned to the pool. You can specify that a serviced component should support pooling, as well as control the size of the pool, by using System.EnterpriseServices.ObjectPoolingAttribute . For example, to indicate that the Widget component should be pooled, you can use the following code. (A pooled version of the Widget class is available in the PooledWidget project.)

 /** @attribute ObjectPoolingAttribute(true, MinPoolSize=100,  MaxPoolSize=200) */ public class Widget extends ServicedComponent implements IWidget {     } 

An initial pool of 100 Widget objects will be created. Additional objects will be created and added to the pool on demand, up to a limit of 200. When the upper limit is reached, requests for Widget objects will be queued and widgets will be served up to clients as they are deactivated by other clients. You can specify an optional timeout parameter by using the ObjectPoolingAttribute to indicate how long a client can be queued waiting for an object before giving up and throwing an exception; the default value is 6000 milliseconds (1 minute). As objects are deactivated and clients become quiescent, the number of objects in the pool will gradually drop down to 100.

For pooling to function, you must also implement an instance method called CanBePooled . (If you're familiar with COM+ development, you'll recognize CanBePooled as part of the IObjectControl interface.) If this method returns true , the object will be returned to the pool when it is deactivated; if it returns false , the object will be discarded. A default implementation of CanBePooled that returns false is supplied as part of the ServicedComponent class. A minimal version of CanBePooled for the Widget component is shown here:

 public class Widget extends ServicedComponent implements IWidget {        protected boolean CanBePooled()    {       return true;    }     } 

If an object contains state variables, you might want to initialize them before handing the object to the client when the object is first activated. The usual place to perform initialization is the object's constructor. However, the constructor for a pooled object is executed when the object is created and added to the pool. If an object is returned to the pool, its state variables will be left intact. Another client receiving this same object from the pool will find the object populated with the data from its previous use. (This feature might be useful under some circumstances ”see the sidebar titled "Stateless vs. Stateful Objects.") For this reason, the ServicedComponent class defines two additional methods called Activate and Deactivate . (Again, for those of you who are familiar with COM+, these are also part of the IObjectControl interface.) The Activate method is executed when an object is handed to a client, and Deactivate is executed when the client has finished with the object. If you implement CanBePooled , you should always override Activate and Deactivate as well. Place any client-specific initialization code you require in the Activate method, and use the Deactivate method to tidy up and remove any sensitive information. The following code shows implementations of Activate and Deactivate for the pooled Widget component:

 public class Widget extends ServicedComponent implements IWidget {    private int length, breadth;    private String color;    private boolean varnished;        protected void Activate()    {       this.length = 1;       this.breadth = 1;       this.color = "puce";       this.varnished = false;    }    protected void Deactivate()    {       this.length = 0;       this.breadth = 0;       this.color = null;       this.varnished = false;    }     } 

The CanBePooled , Activate , and Deactivate methods are called on behalf of the client by the runtime at the appropriate junctures during a pooled object's lifecycle. A client should not be able to call these methods directly. (You should make the methods protected, as shown in the examples above, or expose the serviced component through an interface, as all the examples have done so far, and ensure that CanBePooled , Activate , and Deactivate are not part of the interface so they'll be hidden from the client.) However, under some circumstances, a client might have good cause to want to actively destroy a serviced component and ensure that it is not returned to the pool. You can achieve this using the Dispose method. The ServicedComponent class exposes Dispose , and a client can call it to destroy an object. If a serviced component needs to perform any additional cleaning up, you should override the Dispose method and place the cleanup code there. Do not put cleanup code in a finalizer because you'll have no control over when it will be called. The Dispose method will be available through the IDisposable interface. (This interface is registered automatically when the serviced component is installed into its host COM+ application.)

Stateless vs. Stateful Objects

Developers who build distributed systems have an ongoing debate about the pros and cons of stateless and stateful objects. Most developers argue that holding state for any length of time can affect the scalability of a system that's based on serviced components ”and that you should therefore minimize the amount of state an object needs to hold between method calls. This argument is valid much of the time, but there will always be exceptions, and occasionally using a component that maintains state can make sense.

In fact, the key issue is not holding state per se, but holding state on behalf of a client. If a pooled component needs a bundle of non-client-specific information before it can service any client, it makes sense for the component to obtain this when it is instantiated and to retain it during its time in the pool (as long as this state is not too volatile).

Furthermore, holding state does not in itself affect scalability. Holding resources always affects scalability. By storing state, you can cause the object to stay in memory and thus use up a resource (one of the objects in the pool). Holding state in memory affects your ability to load balance between servers because your client becomes linked to the particular server/component instance. This can frequently affect your ability to scale because some servers become unduly loaded due to state-based client affinity.

The viability of state holding will always depend on the volatility and consistency requirements of the data. If you have writable data that needs a high level of consistency, don't store it in an object between calls. For read-mostly data that does not have critical consistency, store away!

To summarize: Stateless objects are the preferred model for many implementations, but stateful objects have their uses.

Caching Shared State

You should think carefully about the type and amount of internal state held by any serviced components you write. However, sometimes you'll need to share state information between objects. You can hold shared state in a variety of ways ”in a file on disk, using a Windows service such as the ASP.NET State Service (see Chapter 15 for more information about interacting with Windows Services from J#), or through a resource manager such as a DBMS. (The Northwind Traders database is really just an example of persistent shared state.) However, schemes such as these might be impractical or too heavyweight if the amount of shared information to be held is small or does not need to persist indefinitely. COM+ provides the Shared Property Manager (SPM) for occasions such as this.

The SPM acts as a middle- tier cache, allowing you to store and retrieve data by name through user -defined shared properties. Data is stored in memory on the COM+ server on a per-process basis ”all serviced components executing in the same COM+ host process can share property values. Shared properties are transient, however; when the host process shuts down, all shared properties in that process will be lost.

Note

When you're considering whether to use the SPM, you should bear two things in mind. First, the SPM is not transactional ”any changes you make to data held in the SPM will not be rolled back if a transaction aborts, for example. Second, using the SPM ties a client to a particular instance of a COM+ application and makes load-balancing more difficult.


To create a shared property, you must first create a shared property group . You do this by instantiating a SharedPropertyGroupManager object and invoking its CreatePropertyGroup method. The SharedPropertyGroupManager class is located in the System.EnterpriseServices namespace. The following code fragment, which can be executed as part of a method in a serviced component, creates a new shared property group called GroupName:

 SharedPropertyGroupManager spgm = new SharedPropertyGroupManager(); boolean exists = false; PropertyLockMode lock = PropertyLockMode.SetGet; PropertyReleaseMode release = PropertyReleaseMode.Process; SharedPropertyGroup spg = spgm.CreatePropertyGroup("GroupName", lock,    release, exists); 

The parameters of CreatePropertyGroup are the name of the new group, a lock mode, a release mode, and a Boolean flag. The lock mode specifies the degree of concurrency supported by the property group. The value PropertyLockMode.SetGet will lock a property as it is accessed, but concurrent threads in this and other components that execute in the same host process will be able to access other properties. You can specify an alternative value, PropertyLockMode.Method , which will lock all properties in the property group. You should use this value if there are interdependencies between properties. In this case, the lock on the property group will be retained until the method that's acquiring the lock completes.

The release mode parameter indicates the lifetime of the shared property group. The value PropertyReleaseMode.Process ties the lifetime of a shared property group to the lifetime of the host process, and the shared property group will not be destroyed until the host process terminates. You can also specify PropertyReleaseMode.Standard , which causes the shared property group to disappear when the last reference to the group is released.

You execute CreatePropertyGroup both to create a property group and to obtain a handle on an existing property group. The Boolean flag parameter ( exists in the example) will be filled in with a value indicating whether the call to CreatePropertyGroup created a new group ( false ) or returned a reference to an existing group ( true ).

Note

Although the Boolean exists parameter is a primitive type and would normally be passed by value to a regular Java method, you should notice that CreatePropertyGroup is a .NET Framework method that expects an output parameter. The J# compiler will generate code that passes the Boolean variable by reference. If you were writing this code in C#, you wouldn't need to initialize the exists variable before calling CreatePropertyGroup either!


Having established the property group, you can use it to create and access properties. The CreateProperty method does this. You pass the name of the property to be created or accessed, and a Boolean flag. A reference to the named property will be returned, and the Boolean flag will indicate whether the property has just been created ( false ) or previously existed ( true ):

 SharedProperty myProperty = spg.CreateProperty("PropertyName", exists); 

Finally, you can read and write the property using get_Value and set_Value . The property data is held as an Object , so you must cast it to the appropriate type when you retrieve the data using get_Value :

 String someData = ...; myProperty.set_Value(someData);   // write the property someData = (String)myProperty.get_Value(); // read the property 

Serviced Components, COM+, and Remoting

The Enterprise Services infrastructure that .NET uses as the front for COM+ is actually heavily based on the .NET Remoting architecture. (See Chapter 11 for details about Remoting.) Serviced components can be hosted in process (library components) or out of process (server components).

When a managed client instantiates an in-process serviced component, .NET Enterprise Services uses a modified version of the .NET Remoting object activation chain and invokes the COM CoCreateInstance method through a managed wrapper (written in managed C++). This allows COM+ to initialize the COM+ context for the serviced component. As you might recall, the Windows Registry InprocServer32 key for a serviced component actually refers to MSCOREE.DLL, so CoCreateInstance calls on the common language runtime to actually create the serviced component (the class name and assembly are also supplied as keys in the Windows Registry) in the same application domain as the managed client. The common language runtime creates a transparent proxy and a customized real proxy to provide access to the serviced component from the client. The customized real proxy is known as a serviced component proxy (SCP). The SCP contains information about the COM+ context created for the serviced component. The transparent proxy and the SCP are marshaled through COM+ back to the managed client. This infrastructure is shown in Figure 14-16.

Figure 14-16. The infrastructure created for an in-process serviced component

The managed client and the serviced component execute in the same application domain. The client executes methods through the transparent proxy and the SCP. When a method call is made, the SCP examines the current COM+ context (the context ID is cached in the SCP) and compares it to the context hosting the serviced component. If they are the same, the SCP simply forwards the method call to the serviced component. If the contexts are different, the SCP calls the COM+ runtime to perform a context switch.

The SCP supplies a callback function for the COM+ runtime that refers back to the SCP. After COM+ has switched contexts, it invokes the callback which returns control to the SCP. The SCP executes the serviced component using the new context (and it also caches the ID of the new context, replacing the previously held value). Notice that COM+ is used only to provide the context and COM+ services; the serviced component itself is still executed by the common language runtime. This means that parameters passed from a managed client to a serviced component do not have to be marshaled as unmanaged COM types and back again.

Server-based serviced components execute out of process, and an extended form of the in-process infrastructure is created. When a managed client instantiates an out-of-process serviced component, the remoting activation chain initially operates as before, using a wrapper to call CoCreateInstance . This time, however, the common language runtime creates the serviced component in a new process. An SCP and a transparent proxy are created as before, but communication with the client is achieved through DCOM. (The common language runtime executing the server process does not know whether the client is managed or unmanaged.) Therefore, a CCW is also created to marshal method calls from DCOM into the transparent proxy. If the client is managed, it receives a transparent proxy and a specialized version of the SCP called a remote serviced component proxy (RSCP) that communicates with DCOM through an RCW. An unmanaged client will communicate with the serviced component through DCOM as if it were an ordinary COM+ component. Figure 14-17 shows the infrastructure created when a managed client invokes a serviced component.

Figure 14-17. The infrastructure created for an out-of-process serviced component

More About Transactions

We discussed some aspects of transactions earlier in this chapter, when we described the use of TransactionAttribute . However, it's worth taking a closer look at how transactions interact with serviced components and the runtime environment.

Note

Chapter 7 showed you how to use explicit transactions with ADO.NET through SqlTransaction objects. These are referred to as imperative transactions because they're defined and managed by the program code written by the developer. Serviced components use declarative transactions , which are implemented using TransactionAttribute . The COM+ runtime manages declarative transactions in conjunction with the Microsoft Distributed Transaction Coordinator (MS DTC). You should not mix imperative and declarative transactions in the same code.


You use transactions to define the atomicity of a series of operations performed against resource managers. When the transaction commits, the results of the operations are made permanent. If the transaction aborts, the results are rolled back. The power of transactions managed by COM+ is that they can span multiple resource managers. The COM+ runtime uses the facilities of the MS DTC to propagate the outcome of a transaction to every resource manager participating in the transaction, using the Two-Phase Commit (2PC) protocol to ensure consistency everywhere. (The details of the MS DTC and 2PC are beyond the scope of this book.) A resource manager interacts with the MS DTC through a series of standard interfaces implemented by the resource manager.

Bear in mind that a resource manager might be accessed through a number of different applications, not just serviced components, and these applications might need to be protected from one another. For example, SQL Server (a resource manager that provides access to a database) can be accessed through tools such as SQL Server Enterprise Manager. Concurrent applications must be protected from one another. Otherwise, you can just imagine what could happen if two applications (COM+ and otherwise) try to manipulate the same data at the same time.

Specifying an Appropriate Isolation Level

In the following discussion, a resource manager is anything that provides controlled access to a shared, persistent resource ”not just a DBMS. For example, MSMQ is a nondatabase resource manager; the protected resources are message queues.

When you specify the transactional requirements of a serviced component, you can indicate the degree of transactional concurrency (also known as the isolation level ) you want to allow by using the Isolation property of TransactionAttribute . Isolation is typically achieved by a resource manager through a series of locks. Two types of lock are used ” shared and exclusive . A shared lock is often acquired when data is read, and other concurrent applications and threads might also acquire a shared lock over the same data at the same time. An exclusive lock is required when an application is modifying, inserting, or deleting data. Other applications cannot access the data until the lock is released. This is for consistency ”if the transaction in which the data is modified, inserted, or deleted is rolled back, the changes will be undone, so it might not be safe for another application to have access to the modified data until any changes are made permanent.

The following code fragment shows how to specify the isolation level for a serviced component. You specify a value from the System.EnterpriseServices.TransactionIsolationLevel enumeration. Table 14-3 describes the available isolation options.

Caution

Note that the isolation level describes only the degree and duration of shared locking that occurs when a serviced component reads data. When data is modified, it the locking used is always exclusive (although the serviced component might be blocked if other applications hold a shared lock on that data), and the exclusive lock is always held until the transaction completes (commits or aborts). Generally speaking, the more data that is locked (with either type of lock), the greater the effect on the concurrency of other applications. You should seek to minimize the volume of data you lock while balancing this requirement against the need to maintain a consistent view of the data.


 /** @attribute TransactionAttribute(TransactionOption.Required,    Isolation=TransactionIsolationLevel.Serializable) */ public class MyComponent extends ServicedComponent implements ... {     } 
Table 14-3. Isolation Levels for Serviced Components

Option

Description

TransactionIsolationLevel.ReadCommitted

The serviced component acquires a shared lock over data as it is read. If another application already holds an exclusive lock over the same data, the serviced component will be blocked until the exclusive lock is released. Once a data item has been retrieved, the shared lock is released (which often is before the transaction completes ). Another application can then update this data. If the serviced component reads the same data again as part of the same transaction, it might therefore obtain different values.

TransactionIsolationLevel.ReadUncommitted

This isolation level is similar to ReadCommitted , except that if another application holds an exclusive lock over data when a serviced component attempts to read it, the exclusive lock will be ignored and the data will be retrieved. This prevents a thread from being blocked, but this situation is potentially dangerous and provides no guarantees about the consistency of data!

TransactionIsolationLevel.RepeatableRead

This isolation level is similar to ReadCommitted , except that shared locks are retained until the transaction completes. Another application cannot modify the locked data, so if the same data is read again, it will have the same values.

TransactionIsolationLevel.Serializable

This isolation level is an extended form of RepeatableRead . The entire resource being accessed (a table in a relational database, for example) is share-locked, not just the data (rows) being read. This prevents any modifications from being made to any other data in the resources until the transaction completes. The rationale behind this approach is that if the query that identifies the data is executed again as part of the same transaction, it should return exactly the same data items ”with no new "phantom" additions.

TransactionIsolationLevel.Any

If a serviced component is invoked by another serviced component, the calling serviced components isolation level will be used. If the calling serviced component is not transactional, the Serializable isolation level will be used.

The default isolation level is Serializable . This is the safest in terms of consistency, but you might find that it decreases concurrency and therefore performance. For a component that only writes data, you might find ReadCommitted (or even ReadUncommitted ) more useful. Similarly, if a component does not repeatedly read the same data in the same transaction, ReadCommitted should be perfectly acceptable. Use the RepeatableRead or Serializable isolation levels only if you must perform repeated reads in the same transaction and guarantee that the same data will be fetched each time.

Transaction Duration

The isolation level determines the circumstances under which data is locked. How long the data remains locked can depend on the duration of the transaction. Remember that some shared locks, and all exclusive locks, that are acquired during a transaction will be retained until the transaction completes. Concurrent requests by other components might be blocked until the transaction finishes. Therefore, to avoid lengthy waits and to maximize throughput, you must design serviced components to keep transactions as short as possible.

In our discussion of JIT activation, we drew your attention to the done bit , which indicates that a serviced component has completed its work and can be deactivated. Serviced components also have a consistent bit (often called the happy bit ), which indicates whether the serviced component wants to commit or abort the current transaction. The done bit and happy bit are used together at the end of each method call to determine the ultimate outcome of the transaction, as follows . (This is a simplified version of what happens ”if a new transaction is created while an existing transaction is active, the algorithm gets more involved!)

  • If the done bit is set and the happy bit is set, the transaction can be committed and the serviced component deactivated.

  • If the done bit is set and the happy bit is clear, the transaction will be aborted and the serviced component deactivated.

  • If the done bit is clear, the component will still be active (it will not be deactivated) and so the transaction outcome can't yet be determined.

You can set the happy bit using the ContextUtil.SetComplete method, and you can unset it using the ContextUtil.SetAbort method. These two methods also set the done bit, as you saw earlier. If you want to set or clear the happy bit without changing the done bit, you can invoke ContextUtil.EnableCommit or ContextUtil.DisableCommit . Remember, however, that until a method completes with the done bit set, the transaction outcome will not be determined. Therefore, a method that invokes DisableCommit can be overridden if a subsequent method call in the same context executes EnableCommit .

You can also access the happy bit using the static MyTransactionVote property of the ContextUtil class. You can query the current state of the happy bit and change it. The values you use ( Commit , Abort ) are specified in the System.EnterpriseServices.TransactionVote enumeration:

 ContextUtil.set_MyTransactionVote(TransactionVote.Abort); 

Of course, the other way to complete a transaction is to tag methods with AutoCompleteAttribute . This attribute sets the done bit (again, as you saw earlier) and sets the happy bit if the method finishes successfully, or it clears the happy bit if the method terminates due to an unhandled exception.

I l @ ve RuBoard


Microsoft Visual J# .NET (Core Reference)
Microsoft Visual J# .NET (Core Reference) (Pro-Developer)
ISBN: 0735615500
EAN: 2147483647
Year: 2002
Pages: 128

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