Preliminary documentation for Ice 3.7.1 Beta. Do not use in production applications. 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++11
namespace FilesystemI
{
    class DirectoryI;

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

        NodeI(const std::string&, const std::shared_ptr<DirectoryI>&);
    
        const std::string _name;
        const std::shared_ptr<DirectoryI> _parent;
        bool _destroyed;
        Ice::Identity _id;
        std::mutex _mutex;
    };
...
}

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++11
FilesystemI::NodeI::NodeI(const string& nm, const shared_ptr<DirectoryI>& parent)
    : _name(nm), _parent(parent), _destroyed(false)
{
    //
    // Create an identity. The root directory has the fixed identity "RootDir".
    //
    if(parent != nullptr)
    {
        _id.name = Ice::generateUUID();
    }
    else
    {
        _id.name = "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++11
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++11
string
FilesystemI::NodeI::name(const Current& c)
{
    lock_guard<mutex> lock(_mutex);

    if(_destroyed)
    {
        throw ObjectNotExistException(__FILE__, __LINE__, c.id, c.facet, c.operation);
    }

    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++11
namespace FilesystemI
{
    class DirectoryI : public NodeI, public Filesystem::Directory, public std::enable_shared_from_this<DirectoryI>
    {
    public:

        virtual Filesystem::NodeDescSeq list(const Ice::Current&) override;
        virtual Filesystem::NodeDesc find(std::string, const Ice::Current&) override;
        std::shared_ptr<Filesystem::FilePrx> createFile(std::string, const Ice::Current&) override;
        std::shared_ptr<Filesystem::DirectoryPrx> createDirectory(std::string, const Ice::Current&) override;
        virtual void destroy(const Ice::Current&) override;
    
        // ...
    };
}

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

C++11
namespace FilesystemI 
{
    class DirectoryI : public NodeI, public Filesystem::Directory, public std::enable_shared_from_this<DirectoryI>
    {
    public:
        // ...

        DirectoryI(const std::string& = "/", const std::shared_ptr<DirectoryI>& = nullptr);

        void removeEntry(const std::string&);

    private:

        using Contents = std::map<std::string, std::shared_ptr<NodeI>>;
        Contents _contents;
    };
}

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

The constructor simply initializes the NodeI base class:

C++11
FilesystemI::DirectoryI::DirectoryI(const string& name, const shared_ptr<DirectoryI>& parent)
    : NodeI(name, parent)
{
}

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

C++11
void
FilesystemI::DirectoryI::removeEntry(const string& name)
{
    lock_guard<mutex> lock(_m);
    _contents.erase(name);
}

Here is the destroy member function for directories:

C++11
void
FilesystemI::DirectoryI::destroy(const Current& c)
{
    if(!_parent)
    {
        throw PermissionDenied("Cannot destroy root directory");
    }
    else
    {
        lock_guard<mutex> lock(_mutex);

        if(_destroyed)
        {
            throw ObjectNotExistException(__FILE__, __LINE__, c.id, c.facet, c.operation);
        }

        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++11
shared_ptr<DirectoryPrx>
FilesystemI::DirectoryI::createDirectory(string nm, const Current& c)
{
    lock_guard<mutex> lock(_mutex);

    if(_destroyed)
    {
        throw ObjectNotExistException(__FILE__, __LINE__, c.id, c.facet, c.operation);
    }

    if(nm.empty() || _contents.find(nm) != _contents.end())
    {
        throw NameInUse(nm);
    }

    auto d = make_shared<DirectoryI>(nm, shared_from_this());
    auto node = c.adapter->add(d, d->id());
    _contents[nm] = d;
    return Ice::uncheckedCast<DirectoryPrx>(node);
}

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

C++11
shared_ptr<FilePrx>
FilesystemI::DirectoryI::createFile(string nm, const Current& c)
{
    lock_guard<mutex> lock(_mutex);

    if(_destroyed)
    {
        throw ObjectNotExistException(__FILE__, __LINE__, c.id, c.facet, c.operation);
    }

    if(nm.empty() || _contents.find(nm) != _contents.end())
    {
        throw NameInUse(nm);
    }

    auto f = make_shared<FileI>(nm, shared_from_this());
    auto node = c.adapter->add(f, f->id());
    _contents[nm] = f;
    return Ice::uncheckedCast<FilePrx>(node);
}

Here is the implementation of list:

C++11
NodeDescSeq
FilesystemI::DirectoryI::list(const Current& c)
{
    lock_guard<mutex> lock(_mutex);

    if(_destroyed)
    {
        throw ObjectNotExistException(__FILE__, __LINE__, c.id, c.facet, c.operation);
    }

    NodeDescSeq ret;
    for(const auto& i: _contents)
    {
        NodeDesc d;
        d.name = i.first;
        d.type = dynamic_pointer_cast<File>(i.second) ? NodeType::FileType : NodeType::DirType;
        d.proxy = Ice::uncheckedCast<NodePrx>(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++11
NodeDesc
FilesystemI::DirectoryI::find(string nm, const Current& c)
{
    lock_guard<mutex> lock(_mutex);

    if(_destroyed)
    {
        throw ObjectNotExistException(__FILE__, __LINE__, c.id, c.facet, c.operation);
    }

    auto pos = _contents.find(nm);
    if(pos == _contents.end())
    {
        throw NoSuchName(nm);
    }

    auto p = pos->second;
    NodeDesc d;
    d.name = nm;
    d.type = dynamic_pointer_cast<File>(p) ? NodeType::FileType : NodeType::DirType;
    d.proxy = Ice::uncheckedCast<NodePrx>(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++11
FilesystemI::FileI::FileI(const string& nm, const shared_ptr<DirectoryI>& parent)
    : NodeI(nm, parent)
{
}

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

C++11
Lines
FilesystemI::FileI::read(const Current& c)
{
    lock_guard<mutex> lock(_mutex);

    if(_destroyed)
    {
        throw ObjectNotExistException(__FILE__, __LINE__, c.id, c.facet, c.operation);
    }

    return _lines;
}

void
FilesystemI::FileI::write(Lines text, const Current& c)
{
    lock_guard<mutex> lock(_mutex);

    if(_destroyed)
    {
        throw ObjectNotExistException(__FILE__, __LINE__, c.id, c.facet, c.operation);
    }

    _lines = move(text);
}

void
FilesystemI::FileI::destroy(const Current& c)
{
    {
        lock_guard<mutex> lock(_mutex);

        if(_destroyed)
        {
            throw ObjectNotExistException(__FILE__, __LINE__, c.id, c.facet, c.operation);
        }

        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