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

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

// Set the nominal temperature.

if (ICP_set(anum, "nominal_temp", &temp) != 0) {

//If ICP_set() failed, read this thermostat's minimum

//and maximum so we can initialize the BadTemp exception. CCS::Thermostat::BtData btd;

ICP_get(

anum, "MIN_TEMP",

&btd.min_permitted, sizeof(btd.min_permitted)

); ICP_get(

anum, "MAX_TEMP",

&btd.max_permitted, sizeof(btd.max_permitted)

);

btd.requested = temp; btd.error_msg = CORBA::string_dup(

temp > btd.max_permitted ? "Too hot" : "Too cold"

); ICP_offline(anum);

throw CCS::Thermostat::BadTemp(btd);

}

//Add the new device to the m_assets map. add_impl(anum, 0);

//Create reference and narrow it. CORBA::Object_var obj = make_dref(m_poa, anum); return CCS::Thermostat::_narrow(obj);

}

We imply in this example that we have chosen to delay instantiation of a servant for a new device and to rely on a servant manager to create a servant when the first request arrives. Of course, we also could have instantiated the servant immediately. However, delayed instantiation is useful with the Evictor pattern. Section 12.6 discusses the Evictor pattern and shows implementations of both a servant locator and a servant activator.

12.4 Destroying, Copying, and Moving Objects

As opposed to object creation, the Life Cycle Service defines IDL interfaces to destroy, copy, and move objects. The IDL definition for the service is quite short, so we present it here in full and explain it as we discuss the relevant interfaces and operations.

//File: CosLifeCycle.idl #include <CosNaming.idl> #pragma prefix "omg.org"

module CosLifeCycle {

typedef CosNaming::Name Key; typedef Object Factory; typedef sequence<Factory> Factories;

typedef struct NVP { CosNaming::Istring name;

480

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

any

value;

 

} NameValuePair;

 

typedef sequence <NameValuePair> Criteria;

exception NoFactory

{ Key search_key; };

exception NotCopyable

{ string reason; };

exception NotMovable

{ string reason; };

exception NotRemovable

{ string reason; };

exception InvalidCriteria { Criteria invalid_criteria; }; exception CannotMeetCriteria { Criteria unmet_criteria; };

interface FactoryFinder {

 

 

Factories

find_factories(in Key factory_key)

};

raises(NoFactory);

 

 

 

 

interface LifeCycleObject {

 

 

LifeCycleObject copy(

 

there,

 

in FactoryFinder

 

in Criteria

the_criteria

) raises(

 

 

 

NoFactory, NotCopyable,

);

InvalidCriteria, CannotMeetCriteria

 

 

 

void

move(

 

there,

 

in FactoryFinder

 

in Criteria

the_criteria

) raises(

 

 

 

NoFactory, NotMovable,

);

InvalidCriteria, CannotMeetCriteria

 

 

 

void

remove() raises(NotRemovable);

};

 

 

 

interface GenericFactory { boolean supports(in Key k);

Object create_object(in Key k, in Criteria the_criteria) raises(

NoFactory, InvalidCriteria, CannotMeetCriteria

);

};

};

The important interface here is LifeCycleObject, which contains the copy, move, and remove operations. The intent of this interface is to act as an abstract base interface. If we want to create objects that support these life cycle operations, we simply inherit from LifeCycleObject:

#include <CosNaming.idl> #pragma prefix "acme.com"

481

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

module CCS { // ...

interface Thermometer : CosLifeCycle::LifeCycleObject { readonly attribute ModelType model;

readonly attribute AssetType asset_num; readonly attribute TempType temperature; attribute LocType location;

};

interface Thermostat : Thermometer { // ...

}; // ...

};

Here we modify the IDL for the Thermometer interface to inherit from

LifeCycleObject. (Because Thermostat inherits from Thermometer, this means that thermostats also support the life cycle operations.)

12.4.1 Destroying Objects

To destroy an object, the client invokes the remove operation on the object. For example:

CCS::Thermometer_var t = ...; t->remove(); assert(t->_non_existent());

//Get a thermometer...

//Permanently destroy the device

//Must return true

After the client invokes the remove operation, the device is permanently gone. In this example, the code demonstrates this by asserting that the _non_existent member function on the CORBA::Object base class returns true. If, after calling remove, the client were to invoke another operation on the thermometer, perhaps to read the current temperature, the operation would throw an OBJECT_NOT_EXIST exception.

It is important to be clear about what is being destroyed here. The remove operation permanently ends the life cycle of an object. This means that all operations after calling remove must raise OBJECT_NOT_EXIST (or TRANSIENT in some cases—see Section 14.4.5). Moreover, all invocations made by other clients via references to the same thermometer also must raise OBJECT_NOT_EXIST. After the object is destroyed, this also means that other operations, such as the list and find operations on the controller, will no longer return the destroyed device. In other words, the remove operation terminates the conceptual CORBA object and not just the servant that represents the object.

The implementation of remove is not quite as simple as that of the factory operations. In particular, how to implement remove correctly depends on the policies of the POA responsible for the device servants.

482

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

Implementing the remove Operation with a Servant Locator

For use with servant locators, remove is easy to implement:

void Thermometer_impl::

remove() throw(CORBA::SystemException)

{

//Remove self from the m_assets map. m_ctrl->remove_impl(m_anum);

//Inform network that the device is gone. if (ICP_offline(m_anum) != 0)

abort();

}

The code updates the controller's m_assets by deleting the entry corresponding to the device and informs the network that the device is now gone. The servant locator performs the actual destruction of the servant in its postinvoke operation. Any remaining state cleanup happens in the destructor of the servant:

Thermometer_impl:: ~Thermometer_impl()

{

if (m_ctrl->exists(m_anum))

m_ctrl->add_impl(m_anum, 0); // Clear servant pointer

}

The destructor first checks whether there is still an entry for the servant in the controller's m_assets map. If no entry is found, the destructor is being called as the result of a remove invocation from a client. In that case, the CORBA object is already destroyed, and the destructor need not do any more work for this simple example. (In a more complex application, the destructor of an object could perform further finalization of the persistent state, such as deleting memory for private data members or closing files.)

On the other hand, if the destructor still finds an entry for this servant in the m_assets map, only the C++ servant for the device is being destroyed, but the device itself still exists. This happens if, for example, the CCS server shuts down. In that case, we want to destroy only the servant but must not remove knowledge of the device's existence from the controller. (The controller must still write the device's asset number into its persistent file.) The destructor deals with this case by setting the device's servant pointer in the m_assets map to null but leaving the entry itself intact. This indicates that the CORBA object still exists but no longer has a servant in memory.

Note that this version of remove is appropriate only for a single-threaded server, in

which it is impossible for multiple requests to be executing concurrently. (Chapter 21 shows how to do this correctly in a multithreaded server.) In fact, the code we show in this chapter assumes that the entire server application has only a single thread and that all POAs that host CCS objects have the SINGLE_THREAD_MODEL policy value. The

483

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

reason we assume this is that our Thermometer and Thermostat servants must occasionally access data structures kept in our Controller servant. As we explain in Section 11.4.7, interactions among servants registered with different POAs may, depending on the ORB implementation, need to be prepared to deal with multithreading issues if the underlying server application is multithreaded. This is because the ORB implementation might assign a separate thread to each POA even if all those POAs have the SINGLE_THREAD_MODEL policy value.

Implementing the remove Operation with a Servant Activator

For use with servant activators, remove must be implemented differently. Recall from Chapter 11 that a servant activator implies the RETAIN policy value on the POA, so the POA maintains the Active Object Map for us. This means that the controller contains only a set of asset numbers instead of a map from asset numbers to servant pointers. To correctly remove a device if we are using a servant activator, we must add another private data member to the Thermometer_impl class:

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

// As before...

private:

bool m_removed; // To support remove() // Remainder as before...

};

The m_removed member is initialized to false by the constructor of the class and is used by the servant activator. We show how this works in a moment. But first, here is the implementation of remove:

void Thermometer_impl::

remove() throw(CORBA::SystemException)

{

// Make an OID for self. ostrstream ostr;

ostr < < m_anum < < ends; char * str = ostr.str();

PortableServer::ObjectId_var oid = PortableServer::string_to_ObjectId(str);

ostr->rdbuf().freeze(0);

poa->deactivate_object(oid);

// Deactivate self.

// Remove device from m_assets set.

m_ctrl->remove_impl(m_anum);

 

m_removed = true;

// Mark self as removed.

}

484

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

This code is surprisingly simple. In fact, it has only three steps. First, the code calls deactivate_object, supplying the thermometer's object ID. After deactivate_object returns, the code then removes the device from the controller's asset set. Finally, it marks the device as removed by setting the m_removed member to true. This innocent-looking function triggers quite a complex trail of activity.

After deactivate_object is called, the POA eventually removes the servant's entry from the Active Object Map. It waits until there are no more active requests for the target's object ID. After the entry is removed, the CORBA object representing the thermometer no longer exists.

The call to deactivate_object eventually (but not immediately) results in a call to etherealize on the servant activator.

Following the call to deactivate_object, the method sets the m_removed private data member to true. As you will see shortly, we need this knowledge to correctly deal with destruction of the remaining object state for the servant.

Now remove returns control to the ORB run time. The POA tracks the number of calls that are still in progress in the deactivated object. (In a threaded server, there may be several invocations in progress in the same object simultaneously.) After all invocations for this object have completed, the POA invokes the etherealize function on the servant activator to tell it that it should now clean up the remaining object state.

In our example, the etherealize function is simple:

void ThermometerActivator_impl:: etherealize(

const PortableServer::ObjectId & oid,

PortableServer::POA_ptr

poa,

PortableServer::Servant

servant,

CORBA::Boolean

cleanup_in_progress,

CORBA::Boolean

remaining_activations

) throw(CORBA::SystemException)

 

{

 

// Destroy servant.

 

if (!remaining_activations)

 

delete servant;

 

}

 

etherealize calls delete on the servant pointer. Note that remaining_activations will be false in this example because we are not using a single servant to represent multiple CORBA objects. In a design that maps multiple CORBA objects to a single servant, remaining_activations is true while there are still entries in the Active Object Map for the servant, and etherealize must delete the servant only when all entries for the servant are removed.

485

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

The call to delete made by etherealize causes the thermometer's destructor to be invoked:

Thermometer_impl:: ~Thermometer_impl()

{

if (m_removed) {

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

}

}

The destructor tests the m_removed member. If the CORBA object was removed, the destructor informs the network that the device was permanently removed.

Evaluating the remove Implementation

The servant locator implementation of remove is straightforward. In contrast, the implementation of remove with a servant activator is more complex. In particular, why do we need all this machinery involving deactivate_object, etherealize, the m_removed member, and the destructor? Or, to phrase the question differently, why not simply implement remove in the following way?

void Thermometer_impl::

remove() throw(CORBA::SystemException)

{

// Clean up state. m_ctrl->remove_impl(m_anum); ICP_offline(m_anum);

// Self-destruct.

// Bad news!

delete this;

}

In a single-threaded server, this code would (almost) work. The first step removes the asset number from the set of assets in the controller and marks the device off-line, effectively destroying the device's state. In the second step, the servant simply destroys itself.

Unfortunately, the POA does not allow us to self-destruct this way. The behavior is undefined if we delete a servant that still has an entry in the Active Object Map. If we simply delete the servant as shown, the servant will be correctly destroyed, but the POA has no idea that this has happened, and it thinks the CORBA object still exists. If a client makes a call via a reference to the device that was incarnated by the now-destroyed servant, the POA still finds an entry to the servant in the Active Object Map. When the POA then dispatches the incoming call to its servant method, the server is likely to core dump because the memory for that servant instance no longer exists.

486

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

Even if we were to use reference counting for our servants and call _remove_ref instead of delete, the example would still be wrong. We would avoid destroying the servant out from under the POA's Active Object Map, but the POA would still think the CORBA object was active because _remove_ref affects only the servant and not the CORBA object.

By calling deactivate_object, as in the preceding example, we correctly inform the POA that the object no longer exists by breaking the association between the object ID and the servant.

The etherealize function is responsible for deleting the servant once there are no remaining activations for that servant, and that causes the destructor to be called.

Finally, in the destructor, we test the m_removed member one more time and mark the device as being off-line only if it was actually destroyed. Again, we might as well do this in etherealize, so why wait until the destructor runs? In this example, we could have done this because it assumes the whole server is single-threaded. However, as we will see in Chapter 21, doing this in the destructor instead of inside remove or etherealize can result in better performance in a multithreaded server by reducing lock contention. For example, the POA guarantees that it will serialize calls to incarnate and etherealize, so the sooner we get out of etherealize, the sooner the servant activator becomes available again to activate another object. Furthermore, etherealize is a method on our servant activator, and not our servant, so it cannot see the servant's m_removed data member. Making it public just so that etherealize can see it, or adding public accessor functions for it, adds unnecessarily to the coupling between the servant and the servant activator.

Note that the etherealize function we have shown assumes that the servant class does not use reference counting, so etherealize can directly call delete. For reference-counted servants, instead of calling delete, etherealize simply decrements the reference count:

void ThermometerActivator_impl:: etherealize(

const PortableServer::ObjectId & /* oid */,

PortableServer::POA_ptr

/* poa */,

PortableServer::Servant

servant,

CORBA::Boolean

/* cleanup_in_progress */,

CORBA::Boolean

remaining_activations

) throw(CORBA::SystemException)

 

{

 

// Destroy servant.

 

if (!remaining_activations)

 

servant->_remove_ref();

 

}

 

487

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

After the reference count drops to zero, the _remove_ref method calls delete to destroy the servant, so the net effect of this version of etherealize is the same as for the earlier one that does not use reference counting.

Summary of Steps During remove

Depending on exactly how you have implemented your server, the policy settings on the POA, and whether you are using servant activators, you have many different options for implementing the remove operation. We illustrate the most complex scenario here by showing how to implement remove with a POA policy of RETAIN and in the presence of a servant activator. The key points of the preceding section are as follows.

We use a separate servant for each CORBA object.

The POA uses the RETAIN policy, so it has an Active Object Map for the servants.

A servant activator is used to instantiate servants on demand.

This design is a very common one for CORBA servers. Whenever you follow this general approach, we recommend that you implement remove according to the following steps.

Step 1.

In the body of the remove operation, break the CORBA object-to-servant association by calling deactivate_object and mark the servant as removed. This technique ensures that the POA will no longer accept new requests from other clients for the same object.

Step 2.

In etherealize, either call delete or, for reference-counted servants,

_remove_ref, but only if remaining_activations is false. This technique ensures that if you map several CORBA objects to a single servant, the servant will be deleted only when it no longer incarnates any CORBA objects. In addition, for multithreaded servers, this approach keeps lock contention to a minimum.

Step 3.

In the destructor of the servant, remove the remaining state for the object. If m_removed is true, destroy all of the object's state, including its persistent state. If m_removed is false, destroy only the state associated with the servant and do not destroy the state associated with the CORBA object. This ensures that no resources are leaked.

Unfortunately, given the weak guarantees the POA provides for actually removing the Active Object Map entry for a deactivated object and etherealizing its servant (as we describe in Section 11.9), your object and servant may stay alive a lot longer than you think they will. You therefore might have to check the equivalent of an m_removed data member in each of your servant's methods and throw OBJECT_NOT_EXIST if it is true. With this approach, even if the POA keeps dispatching requests to your servant after

488

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

remove has called deactivate_object, clients will still be properly informed that the object no longer exists. This is a tedious but viable workaround for the shortcomings of deactivate_object.

Why Is remove on the Object Instead of the Factory?

Developers who are new to distributed objects are frequently puzzled by the question, "Why is it that remove is an operation on the object and not on the factory? Surely, if the factory can create an object, it can also destroy it again later, so shouldn't the remove operation be on the factory, too?"

To see the motivation for making remove part of the object's interface and not the factory's interface, consider a system that has two separate clients. One client's job is to create objects and make them available to the system. Let's call this client the creator. The other client's job is to dispose of objects when they are no longer needed. Let's call this client the destroyer. Assume that our system deals with many different types of objects, that we have thousands of objects, and that there are dozens of factories to create them.

Given these assumptions, it becomes easy to see that making the remove operation part of the factory's interface would cause problems. The object references in our system may be passed from process to process many times during their lifetime. For example, we could be dealing with a workflow system in which different parts of the workflow are controlled by different servers and are passed from server to server as the workflow progresses. Eventually, when a workflow is complete, its object references are passed to the destroyer to dispose of the objects in the workflow. The destroyer now would have a serious problem if the remove operations were on the various factories: for each object to be destroyed, the destroyer would have to have not only the object's reference but also the object's factory reference.

In a large system, it is easy to lose track of the associations between objects and their factories. We could choose to store these associations in a service, but then we would immediately have to deal with consistency issues: if the service's notion of which objects exist ever got out of sync with the actual situation, we would have corrupt state in the system.

By keeping the remove operation on each individual object, we avoid the need to keep track of object-to-factory associations. To destroy an object, the destroyer requires only the object's reference and, by invoking the remove operation, can instruct the object to commit suicide.

Note that we could also solve the problem by adding an operation to each object that returns a reference for the object's factory to the destroyer. However, there is no need to do this. If for some reason we want the factories to destroy the objects they created, we can simply store a reference to a factory inside each object as part of each object's private

489

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

state. The implementation of remove in each object can then simply delegate the remove operation to its own factory.

Yet another problem with making remove a factory operation is the issue of object reference identity. Given only a reference to an object, the factory may not be able to reliably identify the object that belongs to the reference. This problem arises because of the weak semantics of the is_equivalent operation on the Object interface. Rather than explain this issue in detail here, we defer it until Section 20.3.3, where we discuss it in the context of the Callback pattern.

Overall, keeping remove as an operation on each object is a far cleaner and better encapsulated solution than making remove part of the factory's interface. We strongly recommend that you follow this approach.

12.4.2 Copying Objects

Here again is the copy operation on the LifeCycleObject interface:

//File: CosLifeCycle.idl #include <CosNaming.idl> #pragma prefix "omg.org"

module CosLifeCycle { //...

interface LifeCycleObject { LifeCycleObject copy(

in FactoryFinder there, in Criteria the_criteria

)raises(

NoFactory, NotCopyable, InvalidCriteria, CannotMeetCriteria

);

// ...

}; // ...

};

For the moment, we will ignore the there and the_criteria parameters. The intent of the copy operation is that a client can invoke it on an object to obtain a reference to a new object that is a copy of the original in some way. Unfortunately, the copy operation does not make a lot of sense for the objects in the CCS server because physical devices such as thermometers do not have copy semantics. To illustrate the general use of copy, we assume that the client uses objects of type ImageFile, which support copying.

Using the copy Operation

To create a copy of an image object, the client would invoke the copy operation this way:

490

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

// Get image object...

ImageFile_var image_1 = ...;

CosLifeCycle::FactoryFinder_var ff; // Initialized to nil CosLifeCycle::Criteria c; // Initialized to empty

// Make copy of the image.

CosLifeCycle::LifeCycleObject_var obj = image_1->copy (ff, c);

// Narrow to copied-to type.

ImageFile_var image_2 = ImageFile::_narrow(obj);

//Making changes to image_2 now won't affect image_1

//because image_2 is a new object that was copied.

Conceptually, the copy operation is very much like a factory because both a factory operation and copy create a new object. The difference is that for copy, the initial state for the new object is not passed as parameters but instead is taken from the source object. In many ways, copy is the conceptual equivalent of a C++ copy constructor or, more accurately, the equivalent of a virtual clone member function that creates a copy of an object polymorphically.

Because the implementation of copy typically is similar to that of a factory operation, we do not show an implementation here. Instead, let us examine the copy operation in more detail.

The copy operation returns a reference of type LifeCycleObject, which in turn means that the calling client must narrow the reference before it can use it. The copy operation returns a generic reference because it has no other choice: the operation's interface must be suitable for copying objects of arbitrary type, so there is no way to make the return type more specific. (We could have made the return type less specific by using type Object instead, but that would loosen the type system more than necessary. Because copy is supposed to make a copy of the same type as the source, it follows that if the source inherits from LifeCycleObject, so will the copy.)

In the preceding example, we passed a nil reference and an empty sequence to the copy operation. There is nothing wrong with this, and, in fact, the specification mentions this as a valid use of the operation. By passing a nil reference and an empty sequence, we are not passing any additional information to the object that is supposed to create a copy of itself. In other words, the assumption is that the source object can copy itself without further help in the form of additional parameters. This may be a valid assumption for some objects but typically does not hold for all objects.

Using the there and the the_criteria Parameters

We mentioned in the preceding section that a copy operation is similar to a factory operation. However, the copy operation is invoked on the object to be copied and not on

491

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

a factory. If the source object does not have sufficient knowledge to copy itself, this behavior presents a problem. In addition, we may want the copy to be created "somewhere else," such as in a different image database. To permit objects to be ignorant of the details of how to copy themselves and to allow copies to be created "elsewhere," we can pass a non-nil there parameter to the copy operation. The there parameter is an object reference to an object of type FactoryFinder:

module CosLifeCycle {

 

Key;

typedef CosNaming::Name

typedef Object

Factory;

typedef sequence<Factory>

Factories;

exception NoFactory

{

Key search_key; };

interface FactoryFinder {

Factories find_factories(in Key factory_key) raises(NoFactory);

}; // ...

};

The idea is that the copy operation can call the find_factories operation on the passed object to locate a factory that can create a copy. The find_factories operation returns a sequence of object references (of type Object). After find_factories returns, the copy operation somehow picks one of the returned factory references and, to create a copy of itself, delegates object creation to that factory.

The factory_key parameter is a multicomponent name as used by the Naming Service (see Chapter 18). It is similar to a UNIX pathname and is passed to the factory finder to somehow direct it toward suitable factories (whose references could be stored in the Naming Service).

If anything goes wrong, the find_factories operation can raise the NoFactory exception to indicate that it could not locate a suitable factory.

The second parameter to the copy operation is the the_criteria parameter, of type

Criteria:

module CosLifeCycle {

// ...

typedef struct NVP { CosNaming::Istring name; any value;

} NameValuePair;

typedef sequence <NameValuePair> Criteria; // ...

};

492

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

As you can see, the the_criteria parameter is a sequence of name-value pairs, or CORBA's equivalent of a function with untyped parameters. The intent of the_criteria is to supply additional information either to guide the choice of factory by the factory finder or to supply additional parameters to the factory, such as a database name, location, or file name for the copy of an image file.

We suspend our discussion of copy for the moment and return to it again in Section 12.5.

12.4.3 Moving Objects

The move operation has a signature similar to that of the copy operation:

// ...

interface LifeCycleObject {

void

move(

there,

 

in FactoryFinder

 

in Criteria

the_criteria

 

) raises(

 

NoFactory, NotMovable, InvalidCriteria, CannotMeetCriteria

);

// ...

}; // ...

The intent of the move operation is to physically move an object from one location to another without invalidating the reference to the moved object. The moved object is said to have migrated to the new location—for example, from inside one server on one machine to inside another server on a different machine. The parameters to the operation are the same as for the copy operation and are meant to provide further information as to where the object should be moved. However, the contents and meaning of the parameters are not further specified.

We suspend our discussion of move for the moment and return to it in Section 12.5.

12.4.4 Generic Factories

The Life Cycle Service also defines a GenericFactory interface:

module CosLifeCycle {

// ...

interface GenericFactory { boolean supports(in Key k);

Object create_object(in Key k, in Criteria the_criteria) raises(

493