Now that we have had a look at the issues around object life cycle, let us return to our file system application and add life cycle operations to it, so clients can create and destroy files and directories.
module Filesystem {
exception GenericError {
string reason;
};
exception PermissionDenied extends GenericError {};
exception NameInUse extends GenericError {};
exception NoSuchName extends GenericError {};
interface Node {
idempotent string name();
void destroy() throws PermissionDenied;
};
// ...
};
Note that destroy can throw a
PermissionDenied exception. This is necessary because we must prevent attempts to destroy the root directory.
The File interface is the same as the one we saw in
Chapter 5:
module Filesystem {
// ...
sequence<string> Lines;
interface File extends Node {
idempotent Lines read();
idempotent void write(Lines text) throws GenericError;
};
// ...
};
Note that, because File derives from
Node, it inherits the
destroy operation we defined for
Node.
The Directory interface now looks somewhat different from the previous version:
•
The list operation returns a sequence of structures instead of a list of proxies: for each entry in a directory, the
NodeDesc structure provides the name, type, and proxy of the corresponding file or directory.
•
Directories provide a find operation that returns the description of the nominated node. If the nominated node does not exist, the operation throws a
NoSuchName exception.
•
The createFile and
createDirectory operations create a file and directory, respectively. If a file or directory already exists, the operations throw a
NameInUse exception.
module Filesystem {
// ...
enum NodeType { DirType, FileType };
struct NodeDesc {
string name;
NodeType type;
Node* proxy;
};
sequence<NodeDesc> NodeDescSeq;
interface Directory extends Node {
idempotent NodeDescSeq list();
idempotent NodeDesc find(string name) throws NoSuchName;
File* createFile(string name) throws NameInUse;
Directory* createDirectory(string name) throws NameInUse;
};
};
Note that this design is somewhat different from the factory design we saw in Section 35.5.1. In particular, we do not have a single object factory; instead, we have as many factories as there are directories, that is, each directory creates files and directories only in that directory.
The following two sections describe the implementation of this design in C++ and Java. You can find the full code of the implementation (including languages other than C++ and Java) in the
demo/book/lifecycle directory of your Ice distribution.
•
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 code in Chapter 9, 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.
The NodeI Base 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 Chapter 9,
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 in
Section 35.6.5. 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, as described in
Section 35.6.5:
string
FilesystemI::NodeI::name(const Current&)
{
IceUtil::Mutex::Lock lock(_m);
if (_destroyed)
throw ObjectNotExistException(__FILE__, __LINE__);
return _name;
}
The DirectoryI Class
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:
// ...
};
}
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;
// ...
};
}
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 ASM entry for the destroyed directory and removes itself from its parent’s
_contents map. Note that, for the reason we explained in
Section 35.7.1, we call
removeEntry outside the synchronization.
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);
}
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;
}
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;
}
The FileI Class
The constructor of FileI is trivial: it simply initializes the data members of its base class::
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);
}
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.
As we discussed in Section 35.8, this implementation permits only one operation in each directory and file to execute at a time. In practice, this degree of concurrency will usually be adequate; if you need more parallelism, you can use read–write locks as described earlier.
•
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 code in Chapter 13, the entire implementation resides in a
FilesystemI package instead of being part of the
Filesystem package. Doing this is not essential, but is a little cleaner because it keeps the implementation in a package that is separate from the Slice-generated package.
The NodeI Interface
Our DirectoryI and
FileI servants derive from a common
NodeI base interface. This interface is not essential, but useful because it allows us to treat servants of type
DirectoryI and
FileI polymorphically:
package FilesystemI;
public interface NodeI
{
Ice.Identity id();
}
The only method is the id method, which returns the identity of the corresponding node.
The DirectoryI Class
As in Chapter 13, the
DirectoryI class derives from the generated base class
_DirectoryDisp. In addition, the class implements the
NodeI interface.
DirectoryI must implement each of the Slice operations, leading to the following outline:
package FilesystemI;
import Ice.*;
import Filesystem.*;
public class DirectoryI extends _DirectoryDisp
implements NodeI
{
public Identity
id();
public synchronized String
name(Current c);
public synchronized NodeDesc[]
list(Current c);
public synchronized NodeDesc
find(String name, Current c) throws NoSuchName;
public synchronized FilePrx
createFile(String name, Current c) throws NameInUse;
public synchronized DirectoryPrx
createDirectory(String name, Current c) throws NameInUse;
public void
destroy(Current c) throws PermissionDenied;
// ...
}
package FilesystemI;
import Ice.*;
import Filesystem.*;
public class DirectoryI extends _DirectoryDisp
implements NodeI
{
// ...
public DirectoryI();
public DirectoryI(String name, DirectoryI parent);
public synchronized void
removeEntry(String name);
private String _name; // Immutable
private DirectoryI _parent; // Immutable
private Identity _id; // Immutable
private boolean _destroyed;
private java.util.Map<String, NodeI> _contents;
}
The _name and
_parent members store the name of this node and a reference to the node’s parent directory. (The root directory’s
_parent member is null.) Similarly, the
_id member stores the identity of this directory. The
_name,
_parent, and
_id members are immutable once they have been initialized by the constructor. The
_destroyed member prevents the race condition we discussed in
Section 35.6.5; to interlock access to
_destroyed (as well as the
_contents member) we can use synchronized methods (as for the
name method), or use a
synchronized(this) block.
The _contents map records the contents of a directory: it stores the name of an entry, together with a reference to the child node.
public DirectoryI()
{
this("/", null);
}
public DirectoryI(String name, DirectoryI parent)
{
_name = name;
_parent = parent;
_id = new Identity();
_destroyed = false;
_contents = new java.util.HashMap<String, NodeI>();
_id.name = parent == null ? "RootDir" : Util.generateUUID();
}
The real constructor initializes the _name,
_parent,
_id,
_destroyed, and
_contents members. Note that nodes other than the root directory use a UUID as the object identity.
The removeEntry method is called by the child to remove itself from its parent’s
_contents map:
public synchronized void
removeEntry(String name)
{
_contents.remove(name);
}
The implementation of the Slice name operation simply returns the name of the node, but also checks whether the node has been destroyed, as described in
Section 35.6.5:
public synchronized String
name(Current c)
{
if (_destroyed)
throw new ObjectNotExistException();
return _name;
}
Note that this method is synchronized, so the _destroyed member cannot be accessed concurrently.
Here is the destroy member function for directories:
public void
destroy(Current c) throws PermissionDenied
{
if (_parent == null)
throw new PermissionDenied("Cannot destroy root directory");
synchronized(this) {
if (_destroyed)
throw new ObjectNotExistException();
if (_contents.size() != 0)
throw new 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 ASM entry for the destroyed directory and removes itself from its parent’s
_contents map. Note that, for the reason we explained in
Section 35.7.1, we call
removeEntry outside the synchronization.
The createDirectory implementation acquires the lock 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:
public synchronized DirectoryPrx
createDirectory(String name, Current c) throws NameInUse
{
if (_destroyed)
throw new ObjectNotExistException();
if (name.length() == 0 || _contents.containsKey(name))
throw new NameInUse(name);
DirectoryI d = new DirectoryI(name, this);
ObjectPrx node = c.adapter.add(d, d.id());
_contents.put(name, d);
return DirectoryPrxHelper.uncheckedCast(node);
}
The createFile implementation is identical, except that it creates a file instead of a directory:
public synchronized FilePrx
createFile(String name, Current c) throws NameInUse
{
if (_destroyed)
throw new ObjectNotExistException();
if (name.length() == 0 || _contents.containsKey(name))
throw new NameInUse(name);
FileI f = new FileI(name, this);
ObjectPrx node = c.adapter.add(f, f.id());
_contents.put(name, f);
return FilePrxHelper.uncheckedCast(node);
}
public synchronized NodeDesc[]
list(Current c)
{
if(_destroyed)
throw new ObjectNotExistException();
NodeDesc[] ret = new NodeDesc[_contents.size()];
java.util.Iterator<java.util.Map.Entry<String, NodeI> > pos =
_contents.entrySet().iterator();
for(int i = 0; i < _contents.size(); ++i) {
java.util.Map.Entry<String, NodeI> e = pos.next();
NodeI p = e.getValue();
ret[i] = new NodeDesc();
ret[i].name = e.getKey();
ret[i].type = p instanceof FileI
? NodeType.FileType : NodeType.DirType;
ret[i].proxy = NodePrxHelper.uncheckedCast(
c.adapter.createProxy(p.id()));
}
return ret;
}
The find operation proceeds along similar lines:
public synchronized NodeDesc
find(String name, Current c) throws NoSuchName
{
if (_destroyed)
throw new ObjectNotExistException();
NodeI p = _contents.get(name);
if (p == null)
throw new NoSuchName(name);
NodeDesc d = new NodeDesc();
d.name = name;
d.type = p instanceof FileI
? NodeType.FileType : NodeType.DirType;
d.proxy = NodePrxHelper.uncheckedCast(
c.adapter.createProxy(p.id()));
return d;
}
The FileI Class
The FileI class is similar to the
DirectoryI class. The data members store the name, parent, and identity of the file, as well as the
_destroyed flag and the contents of the file (in the
_lines member). The constructor initializes these members:
package FilesystemI;
import Ice.*;
import Filesystem.*;
import FilesystemI.*;
public class FileI extends _FileDisp
implements NodeI
{
// ...
public FileI(String name, DirectoryI parent)
{
_name = name;
_parent = parent;
_destroyed = false;
_id = new Identity();
_id.name = Util.generateUUID();
}
private String _name;
private DirectoryI _parent;
private boolean _destroyed;
private Identity _id;
private String[] _lines;
}
public synchronized String
name(Current c)
{
if (_destroyed)
throw new ObjectNotExistException();
return _name;
}
public Identity
id()
{
return _id;
}
public synchronized String[]
read(Current c)
{
if (_destroyed)
throw new ObjectNotExistException();
return _lines;
}
public synchronized void
write(String[] text, Current c)
{
if (_destroyed)
throw new ObjectNotExistException();
_lines = (String[])text.clone();
}
public void
destroy(Current c)
{
synchronized(this) {
if (_destroyed)
throw new ObjectNotExistException();
c.adapter.remove(id());
_destroyed = true;
}
_parent.removeEntry(_name);
}
The preceding implementation is provably deadlock free. All methods hold only one lock at a time, so they cannot deadlock with each other or themselves. While the locks are held, the methods do not call other methods that acquire locks, so any potential deadlock can only arise by concurrent calls to another mutating method, 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 instance; for concurrent calls to
destroy on different nodes, each node locks itself, releases itself again, and then acquires and releases a lock on its parent (by calling
removeEntry), also making deadlock impossible.
As we discussed in Section 35.8, this implementation permits only one operation in each directory and file to execute at a time. In practice, this degree of concurrency will usually be adequate; if you need more parallelism, you can use read–write locks as described earlier.