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:
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):
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:
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:
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:
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:
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:
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:
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:
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:
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:
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
:
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:
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::
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:
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.