Documentation for Ice 3.5. The latest release is Ice 3.7. Refer to the space directory for other releases.

The implementation of our life cycle design has the following characteristics:

  • It uses UUIDs as the object identities for nodes to avoid object reincarnation problems.
  • When destroy is called on a node, the node needs to destroy itself and inform its parent directory that it has been destroyed (because the parent directory is the node's factory and also acts as a collection manager for child nodes).

Note that, in contrast to the initial version, the entire implementation resides in a FilesystemI namespace instead of being part of the Filesystem namespace. Doing this is not essential, but is a little cleaner because it keeps the implementation in a namespace that is separate from the Slice-generated namespace.

On this page:

Object Life Cycle Changes for the NodeI Class in C++

To begin with, let us look at the definition of the NodeI class:

C++
namespace FilesystemI {

    class DirectoryI;
    typedef IceUtil::Handle<DirectoryI> DirectoryIPtr;

    class NodeI : public virtual Filesystem::Node {
    public:
        virtual std::string name(const Ice::Current&);
        Ice::Identity id() const;

    protected:
        NodeI(const std::string& name, const DirectoryIPtr& parent);

        const std::string _name;
        const DirectoryIPtr _parent;
        bool _destroyed;
        Ice::Identity _id;
        IceUtil::Mutex _m;
    };

    // ...
}

The purpose of the NodeI class is to provide the data and implementation that are common to both FileI and DirectoryI, which use implementation inheritance from NodeI.

As in the initial version, NodeI provides the implementation of the name operation and stores the name of the node and its parent directory in the _name and _parent members. (The root directory's _parent member is null.) These members are immutable and initialized by the constructor and, therefore, const.

The _destroyed member, protected by the mutex _m, prevents the race condition we discussed earlier. The constructor initializes _destroyed to false and creates an identity for the node (stored in the _id member):

C++
FilesystemI::NodeI::NodeI(const string& name, const DirectoryIPtr& parent)
    : _name(name), _parent(parent), _destroyed(false)
{
    _id.name = parent ? IceUtil::generateUUID() : "RootDir";
}

The id member function returns a node's identity, stored in the _id data member. The node must remember this identity because it is a UUID and is needed when we create a proxy to the node:

C++
Identity
FilesystemI::NodeI::id() const
{
    return _id;
}

The data members of NodeI are protected instead of private to keep them accessible to the derived FileI and DirectoryI classes. (Because the implementation of NodeI and its derived classes is quite tightly coupled, there is little point in making these members private and providing separate accessors and mutators for them.)

The implementation of the Slice name operation simply returns the name of the node, but also checks whether the node has been destroyed:

C++
string
FilesystemI::NodeI::name(const Current&)
{
    IceUtil::Mutex::Lock lock(_m);

    if (_destroyed)
        throw ObjectNotExistException(__FILE__, __LINE__);

    return _name;
}

This completes the implementation of the NodeI base class.

Object Life Cycle Changes for the DirectoryI Class in C++

Next, we need to look at the implementation of directories. The DirectoryI class derives from NodeI and the Slice-generated Directory skeleton class. Of course, it must implement the pure virtual member functions for its Slice operations, which leads to the following (not yet complete) definition:

C++
namespace FilesystemI {

    // ...

    class DirectoryI : virtual public NodeI,
                       virtual public Filesystem::Directory {
    public:
        virtual Filesystem::NodeDescSeq list(const Ice::Current&);
        virtual Filesystem::NodeDesc find(const std::string&, const Ice::Current&);
        Filesystem::FilePrx createFile(const std::string&, const Ice::Current&);
        Filesystem::DirectoryPrx createDirectory(const std::string&, const Ice::Current&);
        virtual void destroy(const Ice::Current&);
        // ...

    private:
        // ...
    };
}

Each directory stores its contents in a map that maps the name of a directory to its servant:

C++
namespace FilesystemI {

    // ...

    class DirectoryI : virtual public NodeI,
                       virtual public Filesystem::Directory {
    public:
        // ...

        DirectoryI(const ObjectAdapterPtr& a,
                   const std::string& name,
                   const DirectoryIPtr& parent = 0);

        void removeEntry(const std::string& name);

    private:
        typedef std::map<std::string, NodeIPtr> Contents;
        Contents _contents;
        // ...
    };
}

Note that we use the inherited member _m to interlock operations.

The constructor simply initializes the NodeI base class:

C++
FilesystemI::DirectoryI::DirectoryI(const string& name, const DirectoryIPtr& parent)
    : NodeI(name, parent)
{
}

The removeEntry member function is called by the child to remove itself from its parent's _contents map:

C++
void
FilesystemI::DirectoryI::removeEntry(const string& name)
{
    IceUtil::Mutex::Lock lock(_m);
    Contents::iterator i = _contents.find(name);
    if(i != _contents.end())
    {
        _contents.erase(i);
    }
}

Here is the destroy member function for directories:

C++
void
FilesystemI::DirectoryI::destroy(const Current& c)
{
    if (!_parent)
        throw PermissionDenied("Cannot destroy root directory");
    {
        IceUtil::Mutex::Lock lock(_m);

        if (_destroyed)
            throw ObjectNotExistException(__FILE__, __LINE__);

        if (!_contents.empty())
            throw PermissionDenied("Cannot destroy non?empty directory");

        c.adapter?>remove(id());
        _destroyed = true;
    }

    _parent?>removeEntry(_name);
}

The code first prevents destruction of the root directory and then checks whether this directory was destroyed previously. It then acquires the lock and checks that the directory is empty. Finally, destroy removes the Active Servant Map (ASM) entry for the destroyed directory and removes itself from its parent's _contents map. Note that we call removeEntry outside the synchronization to avoid deadlocks.

The createDirectory implementation locks the mutex before checking whether the directory already contains a node with the given name (or an invalid empty name). If not, it creates a new servant, adds it to the ASM and the _contents map, and returns its proxy:

C++
DirectoryPrx
FilesystemI::DirectoryI::createDirectory(const string& name, const Current& c)
{
    IceUtil::Mutex::Lock lock(_m);

    if (_destroyed)
        throw ObjectNotExistException(__FILE__, __LINE__);

    if (name.empty() || _contents.find(name) != _contents.end())
        throw NameInUse(name);

    DirectoryIPtr d = new DirectoryI(name, this);
    ObjectPrx node = c.adapter?>add(d, d?>id());
    _contents[name] = d;
    return DirectoryPrx::uncheckedCast(node);
}

The createFile implementation is identical, except that it creates a file instead of a directory:

C++
FilePrx
FilesystemI::DirectoryI::createFile(const string& name, const Current& c)
{
    IceUtil::Mutex::Lock lock(_m);

    if (_destroyed)
        throw ObjectNotExistException(__FILE__, __LINE__);

    if (name.empty() || _contents.find(name) != _contents.end())
        throw NameInUse(name);

    FileIPtr f = new FileI(name, this);
    ObjectPrx node = c.adapter?>add(f, f?>id());
    _contents[name] = f;
    return FilePrx::uncheckedCast(node);
}

Here is the implementation of list:

C++
NodeDescSeq
FilesystemI::DirectoryI::list(const Current& c)
{
    IceUtil::Mutex::Lock lock(_m);

    if (_destroyed)
        throw ObjectNotExistException(__FILE__, __LINE__);

    NodeDescSeq ret;
    for (Contents::const_iterator i = _contents.begin(); i != _contents.end(); ++i)
    {
        NodeDesc d;
        d.name = i?>first;
        d.type = FilePtr::dynamicCast(i?>second) ? FileType : DirType;
        d.proxy = NodePrx::uncheckedCast(c.adapter?>createProxy(i?>second?>id()));
        ret.push_back(d);
    }
    return ret;
}

After acquiring the lock, the code iterates over the directory's contents and adds a NodeDesc structure for each entry to the returned vector.

The find operation proceeds along similar lines:

C++
NodeDesc
FilesystemI::DirectoryI::find(const string& name, const Current& c)
{
    IceUtil::Mutex::Lock lock(_m);

    if (_destroyed)
        throw ObjectNotExistException(__FILE__, __LINE__);

    Contents::const_iterator pos = _contents.find(name);
    if (pos == _contents.end())
        throw NoSuchName(name);

    NodeIPtr p = pos?>second;
    NodeDesc d;
    d.name = name;
    d.type = FilePtr::dynamicCast(p) ? FileType : DirType;
    d.proxy = NodePrx::uncheckedCast(c.adapter?>createProxy(p?>id()));
    return d;
}

Object Life Cycle Changes for the FileI Class in C++

The constructor of FileI is trivial: it simply initializes the data members of its base class::

C++
FilesystemI::FileI::FileI(const string& name, const DirectoryIPtr& parent)
    : NodeI(name, parent)
{
}

The implementation of the three member functions of the FileI class is also trivial, so we present all three member functions here:

C++
Lines
FilesystemI::FileI::read(const Current&)
{
    IceUtil::Mutex::Lock lock(_m);

    if (_destroyed)
        throw ObjectNotExistException(__FILE__, __LINE__);

    return _lines;
}

// Slice File::write() operation.

void
FilesystemI::FileI::write(const Lines& text, const Current&)
{
    IceUtil::Mutex::Lock lock(_m);

    if (_destroyed)
        throw ObjectNotExistException(__FILE__, __LINE__);

    _lines = text;
}

void
FilesystemI::FileI::destroy(const Current& c)
{
    {
        IceUtil::Mutex::Lock lock(_m);

        if (_destroyed)
            throw ObjectNotExistException(__FILE__, __LINE__);

        c.adapter?>remove(id());
        _destroyed = true;
    }

    _parent?>removeEntry(_name);
}

Object Life Cycle Concurrency Issues in C++

The preceding implementation is provably deadlock free. All member functions hold only one lock at a time, so they cannot deadlock with each other or themselves. While the locks are held, the functions do not call other member functions that acquire locks, so any potential deadlock can only arise by concurrent calls to another mutating function, either on the same node or on different nodes. For concurrent calls on the same node, deadlock is impossible because such calls are strictly serialized on the mutex _m; for concurrent calls to destroy on different nodes, each node locks its respective mutex _m, releases _m again, and then acquires and releases a lock on its parent (by calling removeEntry), also making deadlock impossible.

See Also
  • No labels