Abstract
Building a multi-tier distributed systems that can manage many
client requests concurrently has always been a challenge for the COM developers.
COM+ has introduced the idea of "object-per-client" and its JITA extension
-"method-per-client" models . The framework provides a highly efficient runtime
environment for implementing extremely scalable enterprise solutions. The main
idea behind these mechanisms is to suggest an architecture for implementing
thread-safe classes that allow concurrent access to their methods and
attributes along with providing a means for simultaneous method invocation
required by the performance and scalability requirements. To simplify the
development of distributed systems, COM+ run-time environment provides a
framework that offers set of services. In my opinion, certainly the most
important one is the transaction management service. In the theory a
transaction is defined as a single entity of work done on behalf of a
particular client initiator. On the other hand, taking the advantage of
declarative transactions requires unavoidably use of JITA - "Just In Time
Activation" feature of COM+ [EWA-2001]. This service is related to the object
lifetime management. An object that uses JITA feature has the option of
detaching from its stub and being released from its context before its client
actually frees it. When next time the client invokes a method of this object,
COM+ environment attaches to the stub new instance.
Problem
If you are implementing scalable server-side COM+ components, designed to
manage relatively large number of clients, more likely you would make them
stateless. The issue here is that often such systems require maintaining client
specific information that must be kept between successive COM+ object method
calls.
Solution
COM+ provides a number of techniques for preserving the state between
consecutive client requests (method calls). The solution I will present is
based on the COM+ internal architecture and demonstrates how to design and
implement complex stateless transactional components that can easily maintain
client-specific information. Having an insight into COM+ context internals we
can see that actually it is just a well-defined territory within an executable.
Each context is responsible for providing particular pre-defined set of
services to the objects executing inside it. Each COM+ object has only one
context. COM+ is in charge of the context generation as well as associations
between objects and contexts. The framework ensures that the objects run in
appropriate context and if two objects have the same service requirements they
will be associated with the same context. However, most configured classes
default attributes are set up in a way that each new COM object instance has to
be placed in a new context on its own. Moreover, if the COM component supports
declarative transactions and JITA service then COM+ forces the objects to live
in context on their own [EWA-2001]. Turning on the attribute
EventTrackingEnabled
(which controls COM+ statistics feature) requires each new
COM+ object instance to be associated with instance-dedicated context as well
[EWA-2001]. A detailed analysis of consecutive method calls shows that if the
conditions above are met - COM+ maps an unique pair of object's stub and COM+
context ID. Each proxy pointer kept by the client code is "related" to exactly
one unique context ID. Therefore we can keep client-specific information on the
server-side mapping it to a context ID.
Design and implementation
JITA service allows an object to deactivates itself before the client releases
it. COM+ interception layer inspects "done" bit at the end of each COM call.
That's why it is important to set bit "done" using IObjectContext::SetComplete()
or IObjectContext::SetAbort()
in order to inform COM+ environment
that the object has to be deactivated at the end of each method call. If the
class supports transactions and a DTC transaction has been initiated, setting
the "done" bit could be very crucial. It in fact determines the outcome of the
distributed transaction. We can override the default behavior and force object
to be deactivated automatically when a call completes though setting the AutoComplete
attribute to TRUE
. Each time when a client makes a method call and
IObjectControl::Activate()
is called, the context ID is retrieved
using IObjectContextInfo::GetContextId()
. This ID will be used as
key for storing and locating client-specific information in a map container. A
COM+ component that keeps the state of transactional COM+ objects can store the
information in a global thread-safe STL map in order to achieve better
performance and efficiency. An important matter we should take under
consideration is that the associative container should be designed as a
thread-safe one in order to provide a proper access to information it
maintains. Follows the interface of base CSafeMap template class:
template <class BOOL bThreadSafe>
class CSafeMap: private map<tstring, T*, CNocaseCmp>
{
public:
BOOL AddOrUpdate(
TCHAR* pszKey,
T* pObject
);
void Remove(TCHAR* pszKey);
BOOL GetObjectCopyByKey(TCHAR* pszKey, T& copyObject);
};
In order to minimize coupling and not overuse inheritance in CSafeMap
I apply
private inheritance. Nonpublic inheritance expresses
"IS-IMPLEMENTED-IN-TERMS-OF". Despite that CSafeMap
doesn't override any of
map's virtual functions I compromised for the sake of simplicity and decided to
make use of "IS-IMPLEMENTED-IN-TERMS-OF" instead of "HAS-A" (i.e. composition)
which is preferable considering all functional requirements of CSafeMap
. For
more details see [SUT-2000] Item 24.
Actually CSafeMap
is simple associative map container using
context ID as key and object pointers (e.g. CCompObjectState
) for
their corresponding values. Associative containers require that their elements
can be ordered. By default operator "std::less"
is used to define
the order. To change the default comparison I designed CNocaseCmp
class.
It implements the operator bool operator()(const tstring& x, const
tstring& y)
for case-insensitive comparison of strings. More
attention in regards to the CSafeMap
implementation deserves AddOrUpdate()
method. It is based on a perfect Scott Meyers's realization. Briefly, in
order to achieve better performance, this method uses map::insert()
method for adding and updating elements to the map. For more details see
[MEY-2001] Item 24. Follows a snippet with part of AddOrUpdate()
implementation:
BOOL AddOrUpdate(
TCHAR* pszKey,
T* pObject
)
{
CLockMgr<CCSWrapper> lockMgr(m_Mutex, bThreadSafe);
BOOL bResult;
CSafeMap::iterator lb = lower_bound(pszKey);
if ( ( lb != end() ) && !( key_comp()(pszKey, lb->first) ) )
{
if ( m_bOwnsObjects )
{
T* pLastObj = lb->second;
delete pLastObj;
}
lb->second = pObject;
bResult = FALSE;
}
else
{
insert( lb, value_type(pszKey, pObject) );
bResult = TRUE;
}
return bResult;
}
Another important feature of CSafeMap
is that it provides an
option for owning stored objects through setting m_bOwnsObjects
attribute.
It allows CSafeMap
to free objects when they are removed from the
map or the map is destroyed. By default the value of m_bOwnsObjects
is set to TRUE
. However it can be altered using Set_OwnsObjects()
accessor method.
CSafeMap
is a thread-safe class when you instantiate it with a
parameter bThreadSafe = TRUE
, that is - it is designed to allow
multiple threads to access it, store and get information to/from it. The
container class aggregates an instance of CCSWrapper
, which is a
simple CRITICAL_SECTION
wrapper. However the implementation of CSafeMap
doesn't use directly CCSWrapper
- it does it by means of the
template class CLockMgr
. Rather than call to Enter()
and
Leave()
methods - the implementation of CSafeMap
instantiate
a CLockMgr
on the stack, thus when a thread-safe method of CSafeMap
enters, CLockMgr
's constructor invokes CCSWrapper::Enter()
to acquire the lock and when the method leaves, CLockMgr
goes out
of the scope, which forces a call to its destructor, where the release of the
CCSWrapper's
lock takes place. The advantage of this approach is
that we don't have to worry about exception handling that should release the
lock in case of thrown exception inside CSafeMap's
method.
Last but not least, I implemented a class CStateMgr
that inherits
from CSafeMap
and uses CCompObjectState
as template
parameter. This class encapsulates implementation of singelton pattern and
model "IS-A" CSafeMap
class. CStateMgr
implementation
is straightforward and combines "The double-checking locking pattern" explained
by Andrei Alexandrescu's "Modern C++ Design" and Meyers singelton described in
"More Effective C++" item 26. Follows the implementation of GetInstance()
static method.
CStateMgr& CStateMgr::GetInstance()
{
if (!sm_pInstance)
{
CLockMgr<CCSWrapper> guard(g_SingeltonLock, TRUE);
if (!sm_pInstance)
{
static CStateMgr instance;
sm_pInstance = &instance;
}
}
return *sm_pInstance;
}
In order to complete the entire picture we should declare two additional
interface methods of our COM object CTrickyObject
- AssignState()
and RemoveState()
. Their purpose is to register and unregister
interest in preserving state for a particular object. I defined them in a
separated interface ICasheState
, but it is up to the implementer
to decide whether they should be part of any other COM interfaces implemented
by the same COM object. As long as the implementation provides two interface
methods for assigning state and releasing allocated resources - it will work.
Follows the snippet of the COM class interface:
class ATL_NO_VTABLE CTrickyObject:
public CComObjectRootEx<...>,
public CComCoClass<CTrickyObject...>,
public IDispatchImpl<ITrickyObject...>,
public IObjectControl,
public ICasheState
{
public:
STDMETHOD(AssignState)( BSTR bstrParam);
STDMETHOD(RemoveState)();
};
Here is the implementation of AssignState()
method. Notice that if
m_guidContextId
was not initialized (i.e. is GUID_NULL
)-
the result code returned by this method is CO_E_NOT_SUPPORTED
.
STDMETHODIMP CTrickyObject::AssignState(BSTR bstrParam)
{
HRESULT hr = S_OK;
CStateMgr& stateMgr = CStateMgr::GetInstance();
if (m_guidContextId != GUID_NULL)
{
_bstr_t bstrProperty(bstrParam);
CCompObjectState* pState = new CCompObjectState();
pState->Set_Param( (TCHAR*)bstrProperty );
stateMgr.AddOrUpdate(
_bstr_t( CComBSTR( m_guidContextId ) ),
pState
);
}
else
{
hr = CO_E_NOT_SUPPORTED;
}
return hr;
}
Finally we can have a look at the client side. It is a simple console
application that implements two demo functions - StatelessCalls()
and
StatefulCalls()
. StatelessCalls()
demonstrates
regular COM+ object manipulations. StatefulCalls()
uses provided
mechanism for storing information per COM+ object. It shows how the component
maintains information among consecutive COM+ method calls.
Compiling and debugging TrickyComp.DLL using VC++ 6.0
- Make sure you turn on "Exception handling"
Use multithreaded debug version of VC RTL.
-
If you would like to debug it - set active configuration to "Debug"
-
Set "Executable for debug session"
-
Compile TrickyComp.DLL
-
Compile the client project
-
Register TrickyComp.DLL using Component Services MMS snap-in.
Creating a new COM+ component using presented framework
-
Create new ATL COM project and select DLL with MTS support
-
Add following files to your project:
-
Common.h
-
CompObjectState.h
-
CompObjectState.cpp
-
LockMgr.h
-
LockMgr.cpp
-
StateMgr.h
-
StateMgr.cpp
-
SafeMap.h
- Declare
m_guidContextId
attribute in your COM object. It will keep the value
of context ID retrieved by IObjectContextInfo::GetContextId
in
IObjectControl::Activate()
implementation. Make sure that you set it up to
GUID_NULL
at the beginning of the Activate()
method.
- Declare and implement
AssignState()
and RemoveState()
methods. For more
details see the sample code.
Summary
Managing state in a stateless environment could be handled by using many
different approaches. Presented mechanism is quite efficient and applicable for
various scenarios. However you should consider very carefully, analyze the
exact system requirements and then make the decision whether or not you really
need to turn transactional COM+ components to ones that can maintain
client-specific information.
References
[EWA-2001] Tim Ewald "Transactional COM+", 2001
[SUT-2000] Herb Sutter "Exceptional C++: 47 Engineering Puzzles, Programming
Problems, and Solutions", 2000
[MEY-2001] Scott Meyers "Effective STL: 50 Specific Ways to Improve Your Use of
the Standard Template Library" 2001
[ALE-2001] Andrei Alexandrescu "Modern C++ Design: Generic Programming and
Design Patterns Applied"
[MEY-1995] Scott Meyers "More Effective C++: 35 New Ways to Improve Your
Programs and Designs"
[TAP-2000] Pradeep Tapadiya "COM+ Programming: A Practical Guide Using Visual
C++ and ATL"
Platform SDK, COM+ Component Services