Now that clients can create PhoneEntry objects, let us consider how to allow clients to destroy them again. One obvious design is to add a
destroy operation to the factory—after all, seeing that a factory knows how to create objects, it stands to reason that it also knows how to destroy them again:
exception PhoneEntryNotExists {
string name;
string phNum;
};
interface PhoneEntryFactory {
PhoneEntry* create(string name, string phNum)
throws PhoneEntryExists;
void destroy(PhoneEntry* pe) // Bad idea!
throws PhoneEntryNotExists;
};
While this works (and certainly can be implemented without problems), it is generally a bad idea. For one, an immediate problem we need to deal with is what should happen if a client passes a proxy to an already-destroyed object to
destroy. We could raise an
ObjectNotExistException to indicate this, but that is not a good idea because it makes it ambiguous as to which object does not exist: the factory, or the entry. (By convention, if a client receives an
ObjectNotExistException for an invocation, what does not exist is the object the operation was targeted at, not some other object that in turn might be contacted by the operation.) This forces us to add a separate
PhoneEntryNotExists exception to deal with the error condition, which makes the interface a little more complex.
A second and more serious problem with this design is that, in order to destroy an entry, the client must not only know which entry to destroy, but must also know
which factory created the entry. For our example, with only a single factory, this is not a serious concern. However, for more complex systems with dozens of factories (possibly in multiple server processes), it rapidly becomes a problem: for each object, the application code somehow has to keep track of which factory created what object; if any part of the code ever loses track of where an object originally came from, it can no longer destroy that object.
Of course, we could mitigate the problem by adding an operation to the PhoneEntry interface that returns a proxy to its factory. That way, clients could ask each object to provide the factory that created the object. However, that needlessly complicates the Slice definitions and really is just a band-aid on a fundamentally flawed design. A much better choice is to add the
destroy operation to the
PhoneEntry interface instead:
interface PhoneEntry {
idempotent string name();
idempotent string getNumber();
idempotent void setNumber(string phNum);
void destroy();
};
With this approach, there is no need for clients to somehow keep track of which factory created what object. Instead, given a proxy to a
PhoneEntry object, a client simply invokes the
destroy operation on the object and the
PhoneEntry obligingly commits suicide. Note that we also no longer need a separate exception to indicate the "object does not exist" condition because we can raise
ObjectNotExistException instead—the exception exists precisely to indicate this condition and, because
destroy is now an operation on the phone entry itself, there is no ambiguity about which object it is that does not exist.
interface PhoneEntry {
// ...
idempotent void destroy(); // Wrong!
};
interface PhoneEntryFactory {
idempotent PhoneEntry* create(string name, string phNum)
throws PhoneEntryExists;
};
The idea is that create and
destroy can be
idempotent operations because it is safe to let the Ice run time retry the operation in the event of a temporary network failure. However, this assumption is not true. To see why, consider the following scenario:
6.
The application receives an ObjectNotExistException and falsely concludes that it tried to destroy a non-existent object when, in fact, the object did exist and was destroyed as intended.
A similar scenario can be constructed for create: in that case, the application will receive a
PhoneEntryExists exception when, in fact, the entry did not exist and was created successfully.
These scenarios illustrate that create and destroy are never idempotent: sending one
create or
destroy invocation for a particular object is not the same as sending two invocations: the outcome depends on whether the first invocation succeeded or not, so
create and
destroy are not idempotent.
As far as the Ice run time is concerned, the act of destroying an Ice object is to remove the mapping between its proxy and its servant. In other words, an Ice object is destroyed when we remove its ASM entry. Once the ASM entry is gone, incoming operations for the object raise
ObjectNotExistException, as they should.
void
PhoneEntryI::destroy(const Current& c)
{
try {
c.adapter‑>remove(c.id);
} catch (const Ice::NotRegisteredException&)
throw Ice::ObjectNotExistException(__FILE__, __LINE__);
}
}
The implementation removes the ASM entry for the servant, thereby destroying the Ice object. If the entry does not exist (presumably, because the object was destroyed previously),
destroy throws an
ObjectNotExistException, as you would expect.
The ASM entry is removed as soon as destroy calls
remove on the object adapter. Assuming that we implement
create as we saw earlier, so no other part of the code retains a smart pointer to the servant
1, this means that the ASM holds the only smart pointer to the servant, so the servant’s reference count is 1. Once the ASM entry is removed (and its smart pointer destroyed), the reference count of the servant drops to zero. In C++, this triggers a call to the destructor of the servant, and the heap-allocated servant is deleted just as it should be; in languages such as Java and C#, this makes the servant eligible for garbage collection, so it will be deleted eventually as well.
Things get more interesting if we consider concurrent scenarios. One such scenario involves concurrent calls to
create and
destroy. Suppose we have the following sequence of events:
1. Client A creates a phone entry.
2.
Client A passes the proxy for the entry to client B.
3.
Client A destroys the entry again.
4.
Client A calls
create for the same entry (passing the same name, which serves as the object identity) and, concurrently, client B calls
destroy on the entry.
Clearly, something is strange about this scenario, because it involves two clients asking for conflicting things, with one client trying to create an object that existed previously, while another client tries to destroy the object that—unbeknownst to that client—was destroyed earlier.
Exactly what is seen by client A and client B depends on how the operations are dispatched in the server. In particular, the outcome depends on the order in which the calls on the object adapter to
add (in
create) and
remove (in
destroy) on the servant are executed:
•
If the thread processing client A’s invocation executes
add before the thread processing client B’s invocation, client A’s call to
add succeeds. Internally, the calls to
add and
remove are serialized, and client B’s call to
remove blocks until client A’s call to
add has completed. The net effect is that both clients see their respective invocations complete successfully.
•
If the thread processing client Bs invocation executes
remove before the thread processing client A’s invocation executes
add, client B’s thread receives a
NotRegisteredException, which results in an
ObjectNotExistException in client B. Client A’s thread then successfully calls
add, creating the object and returning its proxy.
This example illustrates that, if life cycle operations interleave in this way, the outcome depends on thread scheduling. However, as far as the Ice run time is concerned, doing this is perfectly safe: concurrent access does not cause problems for memory management or the integrity of data structures.
The preceding scenario allows two clients to attempt to perform conflicting operations. This is possible because clients can control the object identity of each phone entry: if the object identity were hidden from clients and assigned by the server (the server could assign a UUID to each entry, for example), the above scenario would not be possible. We will return to a more detailed discussion of such object identity issues in
Section 31.9.
•
Client A holds a proxy to an existing object and passes that proxy to client B.
•
Client B calls the
setNumber operation on the object.
•
Client A calls
destroy on the object
while Client B’s call to setNumber is still executing.
The immediate question is what this means with respect to memory management. In particular, client A’s thread calls
remove on the object adapter while client B’s thread is still executing inside the object. If this call to
remove were to delete the servant immediately, it would delete the servant while client B’s thread is still executing inside the servant, with potentially disastrous results.
The answer is that this cannot happen. Whenever the Ice run time dispatches an incoming invocation to a servant, it increments the servant’s reference count for the duration of the call, and decrements the reference count again once the call completes. Here is what happens to the servant’s reference count for the preceding scenario:
2.
Client B’s invocation of
setNumber arrives and the Ice run time increments the reference count to 2 before dispatching the call.
3.
While setNumber is still executing, client A’s invocation of
destroy arrives and the Ice run time increments the reference count to 3 before dispatching the call.
4.
Client A’s thread calls
remove on the object adapter, which destroys the smart pointer in the ASM and so decrements the reference to 2.
5.
Either setNumber or
destroy may complete first. It does not matter which call completes—either way, the Ice run time decrements the reference count as the call completes, so after one of these calls completes, the reference count drops to 1.
6.
Eventually, when the final call (setNumber or
destroy) completes, the Ice run time decrements the reference count once again, which causes the count to drop to zero. In turn, this triggers the call to
delete (which calls the servant’s destructor).
The net effect is that, while operations are executing inside a servant, the servant’s reference count is always greater than zero. As the invocations complete, the reference count drops until, eventually, it reaches zero. However, that can only happen once no operations are executing, that is, once the servant is idle. This means that the Ice run time guarantees that a servant’s destructor runs only once the final operation invocation has drained out of the servant, so it is impossible to "pull memory out from underneath an executing invocation".
3
Here is a very simple implementation of our PhoneEntryI servant. (Methods are inlined for convenience only. Also note that, for the time being, this code ignores concurrency issues, which we return to in
Section 31.6.5.)
class PhoneEntryI : public PhoneEntry {
public:
PhoneEntryI(const string& name, const string& phNum)
: _name(name), _phNum(phNum)
{
}
virtual string
name(const Current&) {
return _name;
}
virtual string
getNumber(const Current&) {
return _phNum;
}
virtual void
setNumber(const string& phNum, const Current&) {
_phNum = phNum;
}
virtual void
destroy(const Current& c) {
try {
c.adapter‑>remove(c.id);
} catch (const Ice::NotRegisteredException&)
throw Ice::ObjectNotExistException(__FILE__, __LINE__);
}
}
private:
const string _name;
string _phNum;
};
With this servant, destroy does just the right thing: it calls
delete on the servant once the servant is idle, which in turn calls the destructor, so the memory used by the
_name and
_phNum data members is reclaimed.
However, real servants are rarely this simple. In particular, destruction of an Ice object may involve non-trivial actions, such as flushing a file, committing a transaction, making a remote call on another object, or updating a hardware device. For example, instead of storing the details of a phone entry in member variables, the servant could be implemented to store the details in a file; in that case, destroying the Ice object would require closing the file. Seeing that the Ice run time calls the destructor of a servant only once the servant becomes idle, the destructor would appear to be an ideal place to perform such actions, for example:
class PhoneEntryI : public PhoneEntry {
public:
// ...
~PhoneEntryI()
{
_myStream.close(); // Bad idea
}
private:
fstream _myStream;
};
The problem with this code is that it can fail, for example, if the file system is full and buffered data cannot be written to the file. Such clean-up failure is a general issue for non-trivial servants: for example, a transaction can fail to commit, a remote call can fail if the network goes down, or a hardware device can be temporarily unresponsive.
If we encounter such a failure, we have a serious problem: we cannot inform the client of the error because, as far as the client is concerned, the
destroy call completed just fine. The client will therefore assume that the Ice object was correctly destroyed. However, the system is now in an inconsistent state: the Ice object was destroyed (because its ASM entry was removed), but the object’s state still exists (possibly with incorrect values), which can cause errors later.
Another reason for avoiding such state clean-up in C++ destructors is that destructors cannot throw exceptions: if they do, and do so in the process of being called during unwinding of the stack due to some other exception, the program goes directly to
terminate and does not pass "Go". (There are a few exotic cases in which it is possible to throw from a destructor and get away with it but, in general, is an excellent idea to maintain the no-throw guarantee for destructors.) So, if anything goes wrong during destruction, we are in a tight spot: we are forced to swallow any exception that might be encountered by the destructor, and the best we can do is log the error, but not report it to the client.
Finally, using destructors to clean up servant state does not port well to languages such as Java and C#. For these languages, similar considerations apply to error reporting from a finalizer and, with Java, finalizers may not run at all. Therefore, we recommend that you perform any clean-up actions in the body of
destroy instead of delaying clean-up until the servant’s destructor runs.
Note that the foregoing does not mean that you cannot reclaim servant resources in destructors; after all, that is what destructors are for. But it
does mean that you should not try to reclaim resources from a destructor if the attempt can fail (such as deleting records in an external system as opposed to, for example, deallocating memory or adjusting the value of variables in your program).
The factory we defined in Section 31.5.1 is what is known as a
pure object factory because
create is the only operation it provides. However, it is common for factories to do double duty and also act as collection managers that provide additional operations, such as
list and
find:
// ...
sequence<PhoneEntry*> PhoneEntries;
interface PhoneEntryFactory {
PhoneEntry* create(string name, string phNum)
throws PhoneEntryExists;
idempotent PhoneEntry find(string name);
idempotent PhoneEntries list();
};
find returns a proxy for the phone entry with the given name, and a null proxy if no such entry exists.
list returns a sequence that contains the proxies of all existing entries.
PhoneEntryPrx
PhoneEntryFactory::find(const string& name, const Current& c)
{
CommunicatorPtr comm = c.adapter‑>getCommunicator();
PhoneEntryPrx pe;
Identity id = comm‑>stringToIdentity(name);
if (c.adapter‑>find(id)) {
pe = PhoneEntryPrx::uncheckedCast(
c.adapter‑>createProxy(id));
}
return pe;
}
If an entry exists in the ASM for the given name, the code creates a proxy for the corresponding Ice object and returns it. This code works correctly even for threaded servers: because the look‑up of the identity in the ASM is atomic, there is no problem with other threads concurrently modifying the ASM (for example, while servicing calls from other clients to
create or
destroy).
Unfortunately, implementing list is not as simple because it needs to iterate over the collection of entries, but the object adapter does not provide any iterator for ASM entries.
4 Therefore, we must maintain our own list of entries inside the factory:
class PhoneEntryFactoryI : public PhoneEntryFactory
{
public:
// ...
void remove(const string&, const ObjectAdapterPtr&);
private:
Mutex _lcMutex;
set<string> _names;
};
The idea is to have a set of names of existing entries, and to update that set in create and
destroy as appropriate. However, for threaded servers, that raises a concurrency issue: if we have clients that can concurrently call
create,
destroy, and
list, we need to interlock these operations to avoid corrupting the
_names set (because STL containers are not thread-safe). This is the purpose of the mutex
_lcMutex (
life
cycle
mutex) in the factory:
create,
destroy, and
list can each lock this mutex to ensure exclusive access to the
_names set.
Another issue is that our implementation of destroy must update the set of entries that is maintained by the factory. This is the purpose of the
remove member function: it removes the specified name from the
_names set as well as from the ASM (of course, under protection of the
_lcMutex lock). However,
destroy is a method on the
PhoneEntryI servant, whereas
remove is a method on the factory, so the servant must know how to reach the factory. Because the factory is a singleton, we can fix this by adding a static
_factory member to the
PhoneEntryI class:
class PhoneEntryI : public PhoneEntry {
public:
// ...
static PhoneEntryFactoryIPtr _factory;
private:
const string _name;
string _phNum;
};
The code in main then creates the factory and initializes the static member variable, for example:
PersonI::_factory = new PersonFactoryI;
// Add factory to ASM and activate object
// adapter here...
This works, but it leaves a bad taste in our mouth because it sets up a cyclic dependency between the phone entry servants and the factory: the factory knows about the servants, and each servant knows about the factory so it can call
remove on the factory. In general, such cyclic dependencies are a bad idea: if nothing else, they make a design harder to understand.
We could remove the cyclic dependency by moving the _names set and its associated mutex into a separate class instance that is referenced from both
PhoneEntryFactoryI and
PhoneEntryI. That would get rid of the cyclic dependency as far as the C++ type system is concerned but, as we will see later, it would not really help because the factory and its servants turn out to be mutually dependent regardless (because of concurrency issues). So, for the moment, we’ll stay with this design and examine better alternatives after we have explored the concurrency issues in more detail.
PhoneEntries
PhoneEntryFactoryI::list(const Current& c)
{
Mutex::Lock lock(_lcMutex);
CommunicatorPtr comm = c.adapter‑>getCommunicator();
PhoneEntries pe;
set<string>::const_iterator i;
for (i = _names.begin(); i != _names.end(); ++i) {
ObjectPrx o = c.adapter‑>createProxy(
comm‑>stringToIdentity(name));
pe.push_back(PhoneEntryPrx::uncheckedCast(o));
}
return pe;
}
Note that list acquires a lock on the life cycle mutex, to prevent concurrent modification of the
_names set by
create and
destroy. In turn, our
create implementation now also locks the life cycle mutex:
PhoneEntryPrx
PhoneEntryFactory::create(const string& name,
const string& phNum,
const Current& c)
{
Mutex::Lock lock(_lcMutex);
PhoneEntryPrx pe;
try {
CommunicatorPtr comm = c.adapter‑>getCommunicator();
PhoneEntryPtr servant = new PhoneEntryI(name, phNum);
pe = PhoneEntryPrx::uncheckedCast(
c.adapter‑>add(servant,
comm‑>stringToIdentity(name)));
} catch (const Ice::AlreadyRegisteredException&) {
throw PhoneEntryExists(name, phNum);
}
_names.insert(name);
return pe;
}
With this implementation, we are safe if create and
list run concurrently: only one of the two operations can acquire the life cycle lock at a time, so there is no danger of corrupting the
_names set.
destroy is now trivial to implement: it simply calls
remove on the factory:
void
PhoneEntryI::destroy(const Current& c)
{
_factory‑>remove(_name, c.adapter);
}
void
PhoneEntryFactoryI::remove(const string& name,
const ObjectAdapterPtr& a)
{
Mutex::Lock lock(_lcMutex);
try
{
a‑>remove(a‑>getCommunicator()‑>stringToIdentity(name));
} catch(const NotRegisteredException&)
{
throw ObjectNotExistException(__FILE__, __LINE__);
}
_names.erase(name);
}
Again, because destroy locks
_lcMutex, we protect the
_names set from concurrent modification, so
create,
destroy, and
list can be executed concurrently without corrupting any data structures.
So far, we have mostly ignored the implementations of getNumber and
setNumber. Obviously,
getNumber and
setNumber must be interlocked against concurrent access—without this interlock, concurrent requests from clients could result in one thread writing to the
_phNum member while another thread is reading it, with unpredictable results. (Conversely, the name operation need not have an interlock because the name of a phone entry is immutable.) To interlock
getNumber and
setNumber, we can add a mutex
_m to
PhoneEntryI:
class PhoneEntryI : public PhoneEntry {
public:
// ...
static PhoneEntryFactoryIPtr _factory;
private:
const string _name;
string _phNum;
Mutex _m;
};
The getNumber and
setNumber implementations then lock
_m to protect
_phNum from concurrent access:
string
PhoneEntryI::name(const Current&)
{
return _name;
}
string
PhoneEntryI::getNumber(const Current&)
{
// Incorrect implementation!
Mutex::Lock lock(_m);
return _phNum;
}
void
PhoneEntryI::setNumber(const string& phNum, const Current&)
{
// Incorrect implementation!
Mutex::Lock lock(_m);
_phNum = phNum;
}
This looks good but, as it turns out, destroy throws a spanner in the works: as shown, this code suffers from a rare, but real, race condition. Consider the situation where a client calls
destroy at the same time as another client calls
setNumber. In a server with a thread pool with more than one thread, the calls can be dispatched in separate threads and can therefore execute concurrently.
•
The thread dispatching the setNumber call locates the servant, enters the operation implementation, and is suspended by the scheduler immediately on entry to the operation, before it can lock
_m.
•
The thread dispatching the destroy call locates the servant, enters
destroy, locks
_m, successfully removes the servant from the
_names set, and returns.
The problem here is that a thread can enter the servant and be suspended before it gets a chance to acquire a lock. With the code as it stands, this is not a problem:
setNumber will simply update the
_phNum member variable in a servant that no longer has an ASM entry. In other words, the Ice object is already destroyed—it just so happens that the servant for that Ice object is still hanging around because there is still an operation executing inside it. Any updates to the servant will succeed (even though they are useless because the servant’s destructor will run as soon as the last invocation leaves the servant.)
Note that this scenario is not unique to C++ and can arise even with Java synchronized operations: in that case, a thread can be suspended just after the Ice run time has identified the target servant, but before it actually calls the operation on the target servant. While the thread is suspended, another thread can execute
destroy.
While this race condition does not affect our implementation, it does affect more complex applications, particularly if the servant modifies external state, such as a file system or database. For example,
setNumber could modify a file in the file system; in that case,
destroy would delete that file and probably close a file descriptor or stream. If we were to allow
setNumber to continue executing after
destroy has already done its job, we would likely encounter problems:
setNumber might not find the file where it expects it to be or try to use the closed file descriptor and return an error; or worse,
setNumber might end up re-creating the file in the process of updating the already-destroyed entry’s phone number. (What exactly happens depends on how we write the code for each operation.)
Of course, we can try to anticipate these scenarios and handle the error conditions appropriately, but doing this for complex systems with complex servants rapidly gets out of hand: in each operation, we would have to ask ourselves what might happen if the servant is destroyed concurrently and, if so, take appropriate recovery action.
It is preferable to instead deal with interleaved invocations of destroy and other operations in a systematic fashion. We can do this by adding a
_destroyed member to the
PhoneEntryI servant. This member is initialized to false by the constructor and set to true by
destroy. On entry to every operation (including
destroy), we lock the mutex, test the
_destroyed flag, and throw
ObjectNotExistException if the flag is set:
class PhoneEntryI : public PhoneEntry {
public:
// ...
static PhoneEntryFactoryIPtr _factory;
private:
const string _name;
string _phNum;
bool _destroyed;
Mutex _m;
};
PhoneEntryI::PhoneEntryI(const string& name, const string& phNum)
: _name(name), _phNum(phNum), _destroyed(false)
{
}
string
PhoneEntryI::name(const Current&)
{
Mutex::Lock lock(_m);
if (_destroyed)
throw ObjectNotExistException(__FILE__, __LINE__);
return _name;
}
string
PhoneEntryI::getNumber(const Current&)
{
Mutex::Lock lock(_m);
if (_destroyed)
throw ObjectNotExistException(__FILE__, __LINE__);
return _phNum;
}
void
PhoneEntryI::setNumber(const string& phNum, const Current&)
{
Mutex::Lock lock(_m);
if (_destroyed)
throw ObjectNotExistException(__FILE__, __LINE__);
_phNum = phNum;
}
void
PhoneEntryI::destroy(const Current& c)
{
Mutex::Lock lock(_m);
if (_destroyed)
throw ObjectNotExistException(__FILE__, __LINE__);
_destroyed = true;
_factory‑>remove(_name, c.adapter); // Dubious!
}
If you are concerned about the repeated code on entry to every operation, you can put that code into a member function or base class to make it reusable (although the benefits of doing so are probably too minor to make this worthwhile).
Using the _destroyed flag, if an operation is dispatched and suspended before it can lock the mutex and, meanwhile,
destroy runs to completion in another thread, it becomes impossible for an operation to operate on the state of such a "zombie" servant: the test on entry to each operation ensures that any operation that runs after
destroy immediately raises
ObjectNotExistException.
Also note the "dubious" comment in destroy: the operation first locks
_m and, while holding that lock, calls
remove on the factory, which in turn locks its
_lcMutex. This is not wrong as such, but as we will see shortly, it can easily lead to deadlocks if we modify the application later.