Table of Contents Previous Next
Logo
Object Life Cycle : 34.6 Object Destruction
Copyright © 2003-2010 ZeroC, Inc.

34.6 Object Destruction

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 excep­tion 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 facto­ries (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 need­lessly complicates the Slice definitions and really is just a band-aid on a funda­mentally 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.

34.6.1 Idempotency and Life Cycle Operations

You may be tempted to write the life cycle operations as follows:
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:
1. A client invokes destroy on a phone entry.
2. The Ice run time sends the request to the server on the wire.
3. The connection goes down just after the request was sent, but before the reply for the request arrives in the client. It so happens that the request was received by the server and acted upon, and the reply from the server back to the client is lost because the connection is still down.
4. The Ice run time tries to read the reply for the request and realizes that the connection has gone down. Because the operation is marked idempotent, the run time attempts to re-establish the connection and send the request a second time, which happens to work.
5. The server receives the request to destroy the entry but, because the entry is destroyed already, the server returns an ObjectNotExistException to the client, which the Ice run time passes to the application code.
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.

34.6.2 Implementing a destroy Operation

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.
So, here is the most simple version of destroy:
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.

Object Destruction and Concurrency

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 servant1, 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 34.8.

Concurrent Execution of Life Cycle and Non-Life Cycle Operations2

Another scenario relates to concurrent execution of ordinary (non-life cycle) oper­ations and destroy:
• 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:
1. Initially, the servant is idle, so its reference count is at least 1 because the ASM entry stores a smart pointer to the servant. (The remainder of these steps assumes that the ASM stores the only smart pointer to the servant, so the refer­ence count is exactly 1.)
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

34.6.3 Cleaning Up Servant State

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 34.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 tempo­rarily 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).

34.6.4 Life Cycle and Collection Operations

The factory we defined in Section 34.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 addi­tional 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.
Here is a simple implementation of find:
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).

Cyclic Dependencies

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&) {
        IceUtil::Mutex::Lock lock(_namesMutex);

        set<string>::iterator i = _names.find(name);
        if (i != _names.end())
            _names.erase(i);
    }

private:
    IceUtil::Mutex _namesMutex;
    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 _namesMutex 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 (of course, under protection of the _namesMutex 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 vari­able, 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 depen­dency 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.
With this design, we can implement list as follows:
PhoneEntries
PhoneEntryFactoryI::list(const Current& c)
{
    Mutex::Lock lock(_namesMutex);

    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 mutex, to prevent concurrent modification of the _names set by create and destroy. In turn, our create implementa­tion now also locks the mutex:
PhoneEntryPrx
PhoneEntryFactory::create(const string& name,
                          const string& phNum,
                          const Current& c)
{
    Mutex::Lock lock(_namesMutex);

    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 lock at a time, so there is no danger of corrupting the _names set.
destroy is now trivial to implement: it simply removes the ASM entry and calls remove on the factory:
void
PhoneEntryI::destroy(const Current& c)
{
    // Note: not quite correct yet.
    c.adapter>remove(_name);
    _factory>remove(_name, c.adapter);
}
Note that this is not quite correct because we have not yet considered concurrency issues for destroy. We will consider this issue in the following section.

34.6.5 Life Cycle and Normal Operations

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 situa­tion 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 following sequence of events can occur:
• 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, successfully removes the servant from the ASM and the _names set, and returns.
• The thread that was suspended in setNumber is scheduled again, locks _m, and now operates on a conceptually already-destroyed Ice object.
Note that, even if we lock _m in destroy, the problem persists:
void
PhoneEntryI::destroy(const Current& c)
{
    // Note: still not correct.

    IceUtil::Mutex::Lock lock(_m);

    c.adapter>remove(_name);
    _factory>remove(_name, c.adapter);
}
Even though destroy now locks _m and so cannot run concurrently with getNumber and setNumber, the preceding scenario can still arise. 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 oper­ation.)
Of course, we can try to anticipate these scenarios and handle the error condi­tions 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 opera­tion (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;
    c.adapter>remove(_name);
    _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 oper­ation 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 own _namesMutex. This is not wrong as such, but as we will see shortly, it can easily lead to deadlocks if we modify the application later.

1
Or reference to the servant, in languages such as Java and C#.

2
This section applies to C++ only.

3
For garbage-collected languages, such as C# and Java, the language run time provides the same semantics: while the servant can be reached via any reference in the application or the Ice run time, the servant will not be reclaimed by the garbage collector.

4
The reason for this is that, during iteration, the ASM would have to be locked to protect it against concurrent access, but locking the ASM would prevent call dispatch during iteration and easily cause deadlocks.


Table of Contents Previous Next
Logo