Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Advanced CORBA Programming wit C++ - M. Henning, S. Vinoski.pdf
Скачиваний:
65
Добавлен:
24.05.2014
Размер:
5 Mб
Скачать

IT-SC book: Advanced CORBA® Programming with C++

the generic factory interface, can be attributed to the service's age. Still others, such as the coarse interface granularity, reflect a lack of design foresight.

For applications that require a weak type model, using the Life Cycle Service can be appropriate. However, for the majority of CORBA applications, the interfaces are too weakly typed and too general to be useful, so it is better to provide equivalent functionality with strongly typed operations on individual interfaces.

A telling point is that no CORBA specification (except for the CORBA-COM Interworking specification) uses the Life Cycle Service interfaces, typically because objects do not have copy or move semantics. Instead of inheriting from LifeCycleObject, the OMG services define a destroy operation on the relevant inter-faces that takes the role of remove.

Whether you find the Life Cycle Service useful is determined by your requirements. The main reason we discuss it in detail here is that it makes an interesting case study of the trade-offs involved in specification design. It illustrates the relative advantages and disadvantages of weakly typed and strongly typed interface models.

In general, CORBA encourages strong type models and provides type any as an escape hatch to allow us to relax the type system without complete loss of type safety. Whether to use a weakly typed or a strongly typed design depends on your application. However, we recommend that you use strongly typed designs wherever possible and that you accept a weakly typed model only if the trade-off is repaid by a substantial gain in flexibility. See [17] for an excellent discussion of these and other design issues.

12.6 The Evictor Pattern

The main motivation for using servant managers is that they allow us to instantiate servants on demand when an invocation for an object arrives instead of having to keep all servants in memory continuously. For example, when the CCS server is first started, it reads a list of asset numbers from secondary storage and uses this list to initialize the m_assets set, but it does not instantiate any servants at all. As client invocations arrive for the various devices, the servant manager's preinvoke or incarnate operation is invoked by the POA, and the servant manager instantiates a new servant for each device as needed.

There is a potential problem here. Assume that the CCS server runs for very long periods, possibly weeks or months, without ever shutting down. We use a servant activator to create servants on demand, and this means that our POA adds each servant to its Active Object Map as soon as the servant activator creates it. Chances are that sooner or later, some client or other will touch every object provided by the server. This means that although the server initially starts up without any instantiated servants, ongoing activity causes all servants to be faulted into memory eventually. The memory consumed for all these servants may be more than we can tolerate, so the server does not scale and we will need to shut it down periodically to reclaim memory.

499

IT-SC book: Advanced CORBA® Programming with C++

To solve this problem, we must be able not only to bring servants into memory on demand but also to evict them if memory runs out or servants have been idle for some time. In that way, we can place an upper bound on the number of instantiated servants and therefore on the memory consumption of the server. We could use a servant locator instead of a servant activator and thereby avoid having the POA keep an Active Object Map. However, using a servant locator requires that we either create and destroy a servant for each request—a practice that is inefficient—or that we maintain our own pool of servants and reuse one for each new request. Maintaining such a pool is essentially just another way to keep an upper bound on the number of servants in memory at any point in time.

The Evictor pattern describes a general strategy for limiting memory consumption. The basic idea is that we use a servant manager to instantiate servants on demand. However, instead of blindly instantiating a new servant every time it is called, the servant manager checks the number of instantiated servants. If the number of servants reaches a specified limit, the servant manager evicts an instantiated servant and then instantiates a servant for the current request.

12.6.1 Basic Eviction Strategy

One of the more interesting issues of the Evictor pattern is how to choose which servant to evict. There are many possible strategies, such as least recently used (LRU), least frequently used (LFU), evicting the servant with the highest memory consumption, or using a weighted function that chooses a servant for eviction based on a combination of factors. Usually, a simple LRU algorithm is effective and incurs low run-time overhead, so we show an LRU eviction implementation.

Note that you can use the Evictor pattern either with a servant locator or with a servant activator. We first show how to implement it using a servant locator and then discuss the changes required to use it with a servant activator.

Recall from Section 11.7.3 that a servant locator implies the NON_RETAIN policy. With this policy, the POA does not maintain an Active Object Map. Instead, the POA invokes the preinvoke and postinvoke operations on the servant locator on every request. The job of preinvoke is to return a pointer to the servant that should handle the request, whereas postinvoke has the job of cleaning up after the operation completes. In our implementation, preinvoke does all the work and postinvoke is empty.

We need two data structures to support our Evictor pattern. The first data structure is an STL map that maps object IDs to C++ servant pointers and acts as our own active object map.[1] An STL map provides O(log n) performance on insert and erase operations, and that is sufficient for our purposes. A truly high-performance implementation of our active object map would probably use a hash table. (See [39] for how to implement a hash table that works as a drop-in replacement for an STL map.)

[1] To distinguish our private object map from that of a RETAIN POA, we refer to our active object map using lowercase words and the POA's Active Object Map using capitalized words.

500

IT-SC book: Advanced CORBA® Programming with C++

The second data structure we need to implement LRU eviction is a simple queue. Each item on the queue represents a servant in memory. For example, we could store a C++ pointer to a servant in the queue items, or we could store the servant's object ID instead. The main point is that we can uniquely identify each instantiated servant with the information in each queued item.

Initially, when the server starts up, the evictor queue is empty. Whenever a client request arrives, the servant locator's preinvoke operation is called, and it first looks in our STL map for the required servant. If the servant is already in memory, preinvoke returns a pointer to the servant. If the servant is not in memory, preinvoke instantiates it, adds an entry for the servant to our private active object map, and adds a new item for the servant to the tail of the queue. Figure 12.2 shows the evictor queue after preinvoke has been called for the first five objects used by clients after server start-up. The order of items in the queue indicates the order of instantiation. The item corresponding to the servant that was instantiated first appears rightmost in the queue— that is, as the oldest item.

Figure 12.2 An evictor queue after instantiating five servants.

Here is the sequence of events for instantiating a new servant as shown in Figure 12.2. A client invokes an operation.

The POA calls preinvoke on the servant locator. The servant locator instantiates the servant.

The servant locator adds an item for the servant at the tail of the queue.

Note that the arrows from the queue items to the servants do not necessarily indicate pointers. As pointed out earlier, we could store a C++ pointer in each queue item, but we also could store an asset number or the servant's object ID.

Let us assume that our queue is limited to holding only five items and that the client sends a request for object ID 6, which is not yet in memory. Again, when the request arrives, the POA calls the preinvoke operation on the servant locator. However, the implementation of preinvoke now realizes that the queue is full. As a result, preinvoke removes the oldest servant's item from the head of the queue. It then deletes

501

IT-SC book: Advanced CORBA® Programming with C++

this oldest servant before instantiating a new servant and adding the new servant's item to the tail of the queue. The entire process is illustrated in Figure 12.3.

Figure 12.3 Eviction of servant 1 from the queue.

The sequence of events in Figure 12.3 is as follows. A client invokes an operation on the object with ID 6. The POA calls preinvoke on the servant locator.

The servant locator's preinvoke realizes that the evictor queue is full and dequeues the item at the head (object 1).

preinvoke either deletes the servant immediately or, in a multithreaded server, calls _remove_ref to decrement the servant's reference count.

preinvoke instantiates the servant for object 6.

preinvoke adds an item for object 6 to the tail of the queue and returns control to the POA.

The POA dispatches the request to the new servant and then later invokes postinvoke on the servant locator (which does nothing in our implementation).

The net effect of these events is that we start with five servants and we finish with five servants because we have evicted the oldest servant from memory to make room for the newest servant.

12.6.2 Maintaining LRU Order

The remaining question is how to maintain the queue in LRU order. Conceptually, we want to ensure that every operation that is dispatched to a servant causes that servant to be dequeued from its current queue position and to be moved to the tail of the queue. Achieving this goal is simple in our implementation because the preinvoke operation is called on every request whether or not the servant is in memory.

If preinvoke finds a servant in memory, it moves the servant's item to the tail of the queue.

502

IT-SC book: Advanced CORBA® Programming with C++

If preinvoke does not find a servant in memory, it instantiates the servant and adds it to the tail of the queue.

Either way, each request that is dispatched causes its servant to move to the tail of the queue. With this strategy, after we have located the correct servant for a request, we must be able to efficiently remove the servant from the current queue position and enqueue it at the tail. By storing the queue position of each servant in our active object map, we can locate the servant on the queue as a constant-time operation.

12.6.3 Implementing the Evictor Pattern Using a Servant

Locator

We need two supporting data structures for the Evictor pattern. Both of them are private data members of our servant locator. The first data structure is our evictor queue:

typedef list<Thermometer_impl *> EvictorQueue;

The evictor queue simply stores pointers to servants. As you will see shortly, preinvoke maintains that queue in LRU order.

Our active object map provides the mapping from asset numbers to the queue position of the corresponding servant:

typedef map< CCS::AssetType,

EvictorQueue::iterator > ActiveObjectMap;

The next step is to provide an implementation of the servant locator. Here is the class definition:

class DeviceLocator_impl :

public virtual POA_PortableServer::ServantLocator { public:

DeviceLocator_impl(Controller_impl * ctrl );

virtual PortableServer::Servant preinvoke(

const PortableServer::ObjectId & oid,

PortableServer::POA_ptr

poa,

const char *

operation,

void * &

cookie

) throw(

 

CORBA::SystemException,

 

PortableServer::ForwardRequest

 

);

 

virtual void

 

postinvoke(

 

const PortableServer::ObjectId & oid, PortableServer::POA_ptr poa,

503

IT-SC book: Advanced CORBA® Programming with C++

const char *

operation,

void *

cookie,

PortableServer::Servant

servant

) throw(CORBA::SystemException) {}

private:

m_ctrl;

Controller_impl *

typedef list<Thermometer_impl *>

EvictorQueue;

typedef map<CCS::AssetType, EvictorQueue::iterator>

 

ActiveObjectMap;

static const unsigned int

MAX_EQ_SIZE = 100;

EvictorQueue

m_eq;

ActiveObjectMap

m_aom;

};

 

Note that the postinvoke member has an empty inline definition because we do not use it. We have also added a few private data members to the class: m_ctrl, m_eq, and m_aom. The m_ctrl member is initialized by the constructor and stores a pointer to the controller servant so that we can access the controller's asset set. The m_eq and m_aom members store the evictor queue and our active object map, and MAX_EQ_SIZE is the maximum number of servants we are willing to hold in memory simultaneously.

All the action for the Evictor pattern happens in preinvoke:

PortableServer::Servant DeviceLocator_impl:: preinvoke(

const PortableServer::ObjectId & oid,

PortableServer::POA_ptr

poa,

const char *

operation,

void * &

cookie

) throw(CORBA::SystemException, PortableServer::ForwardRequest)

{

//Convert object id into asset number. CORBA::String_var oid_string;

try {

oid_string = PortableServer::ObjectId_to_string(oid); } catch (const CORBA::BAD_PARAM &) {

throw CORBA::OBJECT_NOT_EXIST();

}

if (strcmp(oid_string.in(), Controller_oid) == 0) return m_ctrl;

istrstream istr(oid_string.in()); CCS::AssetType anum;

istr >> anum; if (istr.fail())

throw CORBA::OBJECT_NOT_EXIST();

//Check whether the device is known.

if (!m_ctrl->exists(anum))

throw CORBA::OBJECT_NOT_EXIST();

504

IT-SC book: Advanced CORBA® Programming with C++

//Look at the object map to find out whether

//we have a servant in memory.

Thermometer_impl * servant;

ActiveObjectMap::iterator servant_pos = m_aom.find(anum); if (servant_pos == m_aom.end()) {

//No servant in memory. If evictor queue is full,

//evict servant at head of queue.

if (m_eq.size() == MAX_EQ_SIZE) { servant = m_eq.back(); m_aom.erase(servant->m_anum); m_eq.pop_back();

delete servant;

}

//Instantiate correct type of servant. char buf[32];

if (ICP_get(anum, "model", buf, sizeof(buf))!= 0) abort();

if (strcmp(buf, "Sens-A-Temp") == 0) servant = new Thermometer_impl(anum);

else

servant = new Thermostat_impl(anum);

}else {

//Servant already in memory.

servant = *(servant_pos->second); // Remember servant m_eq.erase(servant_pos->second); // Remove from queue

//If operation is "remove", also remove entry from

//active object map -- the object is about to be deleted. if (strcmp(operation, "remove") == 0)

m_aom.erase(servant_pos);

}

//We found a servant, or just instantiated it.

//If the operation is not a remove, move

//the servant to the tail of the evictor queue

//and update its queue position in the map. if (strcmp(operation, "remove") != 0) {

m_eq.push_front(servant); m_aom[anum] = m_eq.begin();

}

return servant;

}

There is a lot happening here.

The code converts the passed object ID to an asset number and tests whether this device is known. If the conversion fails or the asset number is not known, preinvoke throws OBJECT_NOT_EXIST, which is propagated back to the client.

Note that the code explicitly checks whether the request is for the controller object and, if it is, returns a pointer to the controller servant. This step is necessary because we assume that the controller and all devices share a single POA. We use a single POA because with separate POAs, invocations for the controller and a device may be processed in parallel

505

IT-SC book: Advanced CORBA® Programming with C++

even with the SINGLE_THREAD_MODEL policy on all POAs. However, in this example, we are not dealing with issues of thread safety; we cover these in Chapter 21.

If the device is real, we must locate its servant. The code uses the find member function on our active object map to check whether we have a servant for this device in memory. If the servant is not in memory, the evictor queue may already be at its maximum size (MAX_EQ_SIZE). If it is, the code retrieves the servant pointer in the element at the head of the evictor queue, removes the servant's entry from our active object map, removes the servant from the head of the queue, and deletes the servant. This action evicts the least recently accessed servant from memory. (Note that we have changed the servant's m_anum member variable to be public so that preinvoke can access it. This is safe because m_anum is a const member.)

Now there is room for a new servant, so the code instantiates a servant for the current request, enqueues the servant's pointer at the tail of the evictor queue, and updates our active object map with the servant's asset number and queue position.

If the servant for the request is already in memory, the code simply moves the servant's element from its current position to the tail of the evictor queue and updates our active object map with the new queue position.

The preceding steps work for all operations except remove, for which we must take special steps.

If a remove causes a servant to be brought into memory, there is no point in placing that servant in our active object map or at the tail of the evictor queue because the servant is about to be destroyed.

If a remove finds that a servant is already in memory, that servant is immediately removed from our active object map, again because it is about to be destroyed.

This logic ensures that our active object map accurately keeps track of which servants are in memory.

The remainder of the source code is trivial, so we do not show it here. (It creates a POA with the NON_RETAIN and USE_SERVANT_MANAGER policies, creates a DeviceLocator_impl instance, and calls set_servant_manager to inform the POA of the servant locator's existence.)

Before we go on, be warned that the preceding code takes advantage of a guarantee provided by the STL list container: insertion and removal of an element do not invalidate iterators to other elements. This property is unique to the list container. You cannot replace the list implementation of the evictor queue with a deque because a deque does not guarantee that iterators to other items in the container remain valid if any part of the container is modified.

506

IT-SC book: Advanced CORBA® Programming with C++

12.6.4 Evaluating the Evictor Pattern with Servant Locators

Looking at the preceding code, you can see that it is remarkably easy to implement the Evictor pattern. Ignoring the class header and a few type definitions, it takes barely 30 lines of code to implement sophisticated functionality. Much of the credit for this goes to the Standard Template Library (STL),[2] which supplies us with the requisite data structures and algorithms. But even ignoring STL, something else remarkable is happening here: to add the Evictor pattern to our code, we did not have to touch a single line of object implementation code. The servants are completely unaware that we have added a new memory management strategy to the server, and they do not have to cooperate in any way.

[2] If you are not familiar with STL, we cannot overemphasize its importance and utility. We strongly recommend that you acquaint yourself with this library as soon as you can. See [14] for an excellent tutorial and reference.

Being able to make such modifications without disturbing existing code is a strong indicator of clean and modular design. Moreover, it shows that the POA achieves correct separation of concerns. Object activation is independent of the application semantics, and the servant locator design reflects this.

The most valuable feature of the Evictor pattern is that it provides us with precise control over the memory consumption and performance trade-off for the CCS server. A longer evictor queue permits more servants to be active in memory and results in better performance; a shorter queue reduces performance but also reduces the memory requirements of the server.

You must be aware, however, of one potential pitfall: if the evictor queue is too small, performance will fall off dramatically. This happens if there are more objects being used by clients on a regular basis than the server can hold in memory. In that case, most operation invocations from clients cause one servant to be evicted and another servant to be instantiated, and that is expensive. The problem is similar to that of thrashing in a demand-paged operating system if the working set of a process does not fit in memory [13]; if the "working set of objects" does not fit into the evictor queue, the server spends much of its time evicting and instantiating servants instead of servicing requests.

The Evictor pattern is an important tool that can help servers achieve high performance without consuming massive amounts of memory. Object systems exhibit locality of reference (see [13]) just as ordinary processes do; it is rare for clients to be uniformly interested in all or almost all of the objects implemented by a server. Instead, client activity is typically focused on a group of objects for quite some time and then shifts to a new group of objects. The caching nature of the Evictor pattern makes it well suited to this behavior.

Another important way of achieving high performance is to use the USE_DEFAULT_SERVANT policy value, which allows you to handle invocations for many different CORBA objects with a single servant (see Section 11.7.4). Default

507

IT-SC book: Advanced CORBA® Programming with C++

servants go a step beyond the Evictor pattern in reducing memory requirements because they eliminate both the Active Object Map and the one-to-one mapping from object references to servants. The price of the default servant technique is that unless the server uses an aggressive threading strategy, object invocations are serialized on the default servant, so invocation throughput will drop. However, default servants make it possible to create lightweight implementations that allow a server to scale to millions of objects while keeping memory consumption very low.

12.6.5 Implementing the Evictor Pattern Using a Servant

Activator

In Section 12.6.1, we mentioned that you can use the Evictor pattern with servant activators as well as servant locators. To use the pattern with servant activators, the POA for the servants must use the RETAIN policy. This in turn implies that the POA maintains its own Active Object Map, and you have no direct control over its contents. This raises the question of where to store the position of each servant in the evictor queue. In the servant locator case we store the queue position in our own active object map, but for servant activators we cannot do this.

The solution to this problem is to store the queue position in each individual servant. This creates an evictor queue as shown in Figure 12.4.

Figure 12.4 Implementing the Evictor pattern using a servant activator.

The back pointers from the servants to the evictor queue record each servant's queue position, so we can efficiently locate a servant's item on the queue to evict it or move it to the tail of the queue. The sequence of steps during activation is similar to those of the servant locator case, but for the servant activator, the incarnate operation takes responsibility for instantiating and evicting servants.

A client invokes an operation.

The POA looks for the servant in its Active Object Map. If it cannot find the servant, it calls incarnate on the servant activator.

The servant activator instantiates the new servant, possibly deactivating the object incarnated by the servant at the head of the queue.

508

IT-SC book: Advanced CORBA® Programming with C++

The servant activator adds the new servant to the tail of the queue. If a servant was evicted, the activator deactivates its associated object and removes the servant's entry from the evictor queue.

Note that this design is not quite as clean as the one using a servant locator. Because we cannot control the Active Object Map, we must modify the implementation of each individual servant to store its queue position. In other words, servants must be aware of the fact that they are kept on a queue. We could instead store this information in an external data structure, such as a hash table, to keep our servants ignorant. However, a hash table has other drawbacks, which we explore in the next section.

To evict a servant, incarnate must take different actions than preinvoke does in

Section 12.6.3. To evict a servant, incarnate calls deactivate_object on the POA for the servant at the head of the queue and then removes the evicted servant's entry from the queue. The deactivate_object call causes the POA to remove the servant's entry from its Active Object Map and eventually results in a call from the POA to etherealize, which destroys the servant. If incarnate does not need to evict a servant, it simply creates the new servant and places it at the tail of the queue.

Maintaining the evictor queue in LRU order also requires changes. For the servant locator approach, we took advantage of the fact that preinvoke is called by the POA for all object invocations. However, incarnate on the servant activator is called only if the servant is not in memory. (If the servant is already in memory, the POA finds it in its Active Object Map and dispatches directly to it.) This means that we must change our strategy for moving servants to the tail of the evictor queue whenever a request is processed: on entry to every operation, the servant must move itself to the tail of the queue. The only exception is the remove operation; instead of moving the servant to the tail of the queue, remove erases it from the queue.

Armed with these ideas, we can fill in the source code. The evictor queue holds object IDs instead of servant pointers:

typedef list<PortableServer::ObjectId> EvictorQueue;

We use object IDs because that is the parameter expected by deactivate_object. The servant activator class definition is similar to the one for the servant locator, so we do not show it here.

The incarnate member function has little work to do because it will be called by the POA only if no servant is in memory. This means that incarnate need only check that the device exists and then check whether the queue is full. If the queue is full, incarnate must evict the least recently used servant before instantiating the servant for the current request:

PortableServer::Servant

DeviceActivator_impl::

509

IT-SC book: Advanced CORBA® Programming with C++

incarnate(

const PortableServer::ObjectId & oid, PortableServer::POA_ptr poa

) throw(CORBA::SystemException, PortableServer::ForwardRequest)

{

//Convert OID to asset number (not shown). CCS::AssetType anum = ...;

//Check whether the device is known.

if (!m_ctrl->exists(anum))

throw CORBA::OBJECT_NOT_EXIST();

//If queue is full, evict servant at head of queue. if (eq.size() == MAX_EQ_SIZE) {

poa->deactivate_object(eq.back()); eq.pop_back();

}

//Instantiate new servant. PortableServer::ServantBase * servant; char buf[32];

if (ICP_get(anum, "model", buf, sizeof(buf)) != 0) abort();

if (strcmp(buf, "Sens-A-Temp") == 0) servant = new Thermometer_impl(anum);

else

servant = new Thermostat_impl(anum);

//Add new servant to tail of queue.

eq.push_front(oid);

return servant;

}

To evict a servant if the queue is full, the function calls deactivate_object, which causes etherealize to be called after incarnate returns control to the POA. Note that we make the evictor queue eq a global variable here. This is because the same queue is now used by the servant activator as well as all servants. We could have made the evictor queue a static data member of the servant class, but that would do little to remedy the fact that global data is being shared among different classes (using a friend declaration would also do little to improve matters). For this simple example, we put up with the global variable. For a more realistic solution, we would use the Singleton pattern [4].

Next, let us look at the servant implementation. We require an additional private data member that records the servant's position on the queue, and we need a private member function called move_to_tail:

class Thermometer_impl : public virtual POA_CCS::Thermometer { public:

// As before...

private:

510

IT-SC book: Advanced CORBA® Programming with C++

const CCS::AssetType

m_anum;

bool

m_removed;

EvictorQueue::iterator

m_pos;

void

move_to_tail();

};

We show the move_to_tail member function in a moment. The constructor of the servant initializes the m_pos data member to point at the first element on the evictor queue:

Thermometer_impl:: Thermometer_impl(CCS::AssetType asset)

: m_anum(asset), m_removed(0), m_pos(eq.begin())

{

// Intentionally empty

}

This code records the servant's position in the queue for use by the destructor and the move_to_tail member function, whose job it is to move the servant's queue entry to the tail of the queue:

void Thermometer_impl:: move_to_tail()

{

EvictorQueue::value_type val = *m_pos; eq.erase(m_pos);

eq.push_front(val); m_pos = eq.begin();

}

To move the servant to the tail of the queue whenever an operation is invoked, we add a call to move_to_tail to every operation on the servant. For example:

CCS::AssetType

Thermometer_impl::asset_num() throw(CORBA::SystemException)

{

move_to_tail(); return m_anum;

}

CCS::AssetType

Thermometer_impl::temperature() throw(CORBA::SystemException)

{

move_to_tail();

return get_temperature(m_anum);

}

// etc...

511

IT-SC book: Advanced CORBA® Programming with C++

Our implementation of remove invokes deactivate_object, removes the object's asset number from the controller's map, erases the object's entry in the evictor queue, and sets the m_removed member to true.

void

Thermometer_impl::remove() throw(CORBA::SystemException)

{

EvictorQueue::value_type oid = *m_pos; deactivate_object(oid);

// Remove device from m_assets set. m_ctrl->remove_impl(m_anum); eq.erase(m_pos);

m_removed = true;

}

As usual, the etherealize implementation of our servant activator calls delete (or _remove_ref), which causes the destructor of the servant to run:

Thermometer_impl:: ~Thermometer_impl()

{

if (m_removed) {

// Inform network that the device is off-line. ICP_offline(m_anum);

}

}

If the destructor was called because the object was destroyed, m_removed is true, so the destructor marks the device as being off-line.

12.6.6 Evaluating the Evictor Pattern with Servant Activators

The Evictor pattern with servant activators works just as well as with servant locators as far as its advantages for performance and scalability are concerned. However, its implementation is not nearly as clean and simple as the one for servant locators. With servant activators, the Active Object Map is out of our control and we must explicitly call the move_to_tail member function in every operation, a practice that robs the implementation of much of its elegance.

In addition, the division of responsibility across different member functions is unpleasant. The incarnate function creates the servant and places it on the queue, the servant stores its queue position in a private member variable (and therefore must know about the queue), the servant destructor removes the servant's queue entry when the servant is destroyed, and, to top it all off, every operation must call move_to_tail to maintain the queue in LRU order. As a result, we have a design that is complex and difficult to modify.

512

IT-SC book: Advanced CORBA® Programming with C++

By deciding to store the queue position in each servant, we give explicit knowledge about the queue to each servant, introducing a major interdependency into the design. An alternative approach is to keep each servant's queue position in a separate data structure, such as a hash table that stores pairs of object ID and queue position. This approach makes it possible to remove a servant's queue entry as part of etherealize instead of removing the entry in the servant's destructor. Unfortunately, this approach still does not solve the problem that every operation invocation must somehow maintain the LRU order of the queue, so we still need support from the servant implementation. Worse, by storing pairs of object ID and queue position in the hash table, we are essentially duplicating the Active Object Map that is already maintained by the POA. This doubles the storage overhead for keeping track of servants. For servants that contain only a small amount of state, this approach may well be too expensive.

The conclusion of this discussion is that although we can use the Evictor pattern with servant activators, we must work hard to fit the pattern to the implementation. In turn, this suggests that it pays to plan ahead and to choose POA policies carefully. Turning source code that relies on servant activators into source code that uses servant locators (or vice versa) is not easy, so making the correct decision up front is worthwhile.

12.6.7 Interactions with Collection Manager Operations

The Evictor pattern offers a way to limit memory consumption in servers without compromising performance unduly. However, to some degree, it interferes with collection manager operations, such as list and find. For the implementation in Chapter 10, the implementation of list was trivial: we simply iterated over the list of servants and invoked the _this member function to create a list of references. However, as soon as we do not have all servants in memory, we cannot do this because to invoke _this, we need a servant.

The solution is to not rely on servants being in memory at all. Instead, we can use the make_dref function shown on page 546:

CCS::Controller::ThermometerSeq * Controller_impl::

list() throw(CORBA::SystemException)

{

//Create a new thermometer sequence. Because we know

//the number of elements we will put onto the sequence,

//we use the maximum constructor. CCS::Controller::ThermometerSeq_var listv

=new CCS::Controller::ThermometerSeq(m_assets.size()); listv->length(m_assets.size());

//Loop over the m_assets map and create a

//reference for each device.

CORBA::ULong count = 0; AssetMap::iterator i;

for (i = m_assets.begin(); i != m_assets.end(); i++) listv[count++] = make_dref(m_poa, i->first);

513

IT-SC book: Advanced CORBA® Programming with C++

return listv._retn();

}

This implementation avoids the need to instantiate a servant for each device just so that we can call _this. The version shown here applies to our servant locator implementation. For servant activators, we change only one line of code:

// ...

CORBA::ULong count = 0; AssetMap::iterator i;

for (i = m_assets.begin(); i != m_assets.end(); i++) listv[count++] = make_dref(m_poa, *i);

// ...

With a servant activator, the controller contains only a set of asset numbers instead of a map, so instead of passing i->first to make_dref, we pass *i. These implementations are similar to the examples we use to introduce servant managers in

Chapter 11.

The Evictor pattern also interferes with the find operation. The implementation in Chapter 10 iterates over the servants in memory to locate devices with matching attributes. Because not all servants may be in memory, this approach does not work. You may be tempted to write something like this instead:

//...

// Loop over input list and look up each device. CORBA::ULong listlen = slist.length(m_assets.size()); for (CORBA::ULong i = 0; i < listlen; i++) {

AssetMap::iterator where; // Iterator for asset map

// ...

CCS::Controller::SearchCriterion sc = slist[i].key._d(); if (sc == CCS::Controller::LOCATION) {

// Search for matching asset location. for (where = m_assets.begin();

where != m_assets.end(); where++) { Thermometer_var t = make_dref(m_poa, where->first); if (strcmp(t->location(), slist[i].key.loc()) == 0)

// Found a match...

}

// ...

}

// ...

}

// ...

The strategy here is to create an object reference for each device and then to ask the device for its location. This works, but it has a hideous flaw: this linear search causes

514