Table of Contents Previous Next
Logo
Freeze : 40.4 Using a Freeze Map in the File System Server
Copyright © 2003-2009 ZeroC, Inc.

40.4 Using a Freeze Map in the File System Server

We can use a Freeze map to add persistence to the file system server, and we present C++ and Java implementations in this section. However, as you will see in Section 40.5, a Freeze evictor is often a better choice for applications (such as the file system server) in which the persistent value is an Ice object.
In general, incorporating a Freeze map into your application requires the following steps:
1. Evaluate your existing Slice definitions for suitable key and value types.
2. If no suitable key or value types are found, define new (possibly derived) types that capture your persistent state requirements. Consider placing these definitions in a separate file: these types are only used by the server for persistence, and therefore do not need to appear in the "public" definitions required by clients. Also consider placing your persistent types in a separate module to avoid name clashes.
3. Generate a Freeze map for your persistent types using the Freeze compiler.
4. Use the Freeze map in your operation implementations.

40.4.1 Choosing Key and Value Types

Our goal is to implement the file system using a Freeze map for all persistent storage, including files and their contents. Our first step is to select the Slice types we will use for the key and value types of our map. We will keep the same basic design as in Chapter 35, therefore we need a suitable representation for persistent files and directories, as well as a unique identifier for use as a key.
Conveniently enough, Ice objects already have a unique identifier of type Ice::Identity, and this will do fine as the key type for our map.
Unfortunately, the selection of a value type is more complicated. Looking over the Filesystem module in Chapter 35, we do not find any types that capture all of our persistent state, so we need to extend the module with some new types:
module Filesystem {
    class PersistentNode {
        string name;
    };

    class PersistentFile extends PersistentNode {
        Lines text;
    };

    class PersistentDirectory extends PersistentNode {
        NodeDict nodes;
    };
};
Our Freeze map will therefore map from Ice::Identity to PersistentNode, where the values are actually instances of the derived classes PersistentFile or PersistentDirectory. If we had followed the advice at the beginning of Section 40.4, we would have defined File and Directory classes in a separate PersistentFilesystem module, but in this example we use the existing Filesystem module for the sake of simplicity.

40.4.2 Implementing the File System Server in C++

In this section we present a C++ file system implementation that uses a Freeze map for persistent storage. The implementation is based on the one discussed in Chapter 35, and, in this section, we only discuss code that illustrates use of the Freeze map.

Generating the Map

Now that we have selected our key and value types, we can generate the map as follows:
$ slice2freeze I$(ICE_HOME)/slice dict \
    IdentityNodeMap,Ice::Identity,Filesystem::PersistentNode \
    IdentityNodeMap Filesystem.ice \
    $(ICE_HOME)/slice/Ice/Identity.ice
The resulting map class is named IdentityNodeMap.

The Server main Program

The server’s main program is responsible for initializing the root directory node. Many of the administrative duties, such as creating and destroying a communicator, are handled by the class Ice::Application, as described in Section 8.3.1. Our server main program has now become the following:
#include <FilesystemI.h>
#include <Ice/Application.h>
#include <Freeze/Freeze.h>

using namespace std;
using namespace Filesystem;

class FilesystemApp : public virtual Ice::Application {
public:
    FilesystemApp(const string& envName) : 
        _envName(envName) { }

    virtual int run(int, char*[]) {
        // Terminate cleanly on receipt of a signal
        //
        shutdownOnInterrupt();

        // Install object factories
        //
        communicator()>addObjectFactory(
            PersistentFile::ice_factory(), 
            PersistentFile::ice_staticId());
        
        communicator()>addObjectFactory(
            PersistentDirectory::ice_factory(), 
            PersistentDirectory::ice_staticId());

        // Create an object adapter (stored in the NodeI::_adapter
        // static member)
        //
        NodeI::_adapter = communicator()>
            createObjectAdapterWithEndpoints(
                "FreezeFilesystem", "default p 10000");

        //
        // Set static members used to create connections and maps
        //
        NodeI::_communicator = communicator();
        NodeI::_envName = _envName;
        NodeI::_dbName = "mapfs";

        // Find the persistent node for the root directory, or
        // create it if not found
        //
        Freeze::ConnectionPtr connection = 
            Freeze::createConnection(communicator(), _envName);
        IdentityNodeMap persistentMap(connection, NodeI::_dbName);

        Ice::Identity rootId = communicator()>stringToIdentity("RootDir");
        PersistentDirectoryPtr pRoot;
        {
            IdentityNodeMap::iterator p = 
                persistentMap.find(rootId);
            
            if (p != persistentMap.end()) {
                pRoot = 
                    PersistentDirectoryPtr::dynamicCast(
                        p>second);
                assert(pRoot);
            } else {
                pRoot = new PersistentDirectory;
                pRoot>name = "/";
                persistentMap.insert(
                    IdentityNodeMap::value_type(rootId, pRoot));
            }
        }

        // Create the root directory (with name "/" and no parent)
        //
        DirectoryIPtr root = new DirectoryI(rootId, pRoot, 0);

        // Ready to accept requests now
        //
        NodeI::_adapter>activate();

        // Wait until we are done
        //
        communicator()>waitForShutdown();
        if (interrupted()) {
            cerr << appName()
                 << ": received signal, shutting down" << endl;
        }

        return 0;
    }

private:
    string _envName;

};

int
main(int argc, char* argv[])
{
    FilesystemApp app("db");
    return app.main(argc, argv);
}
Let us examine the changes in detail. First, we are now including PersistentFilesystemI.h. This header file includes all of the other Freeze (and Ice) header files this source file requires.
Next, we define the class FilesystemApp as a subclass of Ice::Application, and provide a constructor taking a string argument:
  FilesystemApp(const string& envName) : 
      _envName(envName) { }
The string argument represents the name of the database environment, and is saved for later use in run.
One of the first tasks run performs is installing the Ice object factories for PersistentFile and PersistentDirectory. Although these classes are not exchanged via Slice operations, they are marshalled and unmarshalled in exactly the same way when saved to and loaded from the database, therefore factories are required. Since these Slice classes have no operations, we can use their built‑in factories.
        communicator()>addObjectFactory(
            PersistentFile::ice_factory(), 
            PersistentFile::ice_staticId());
        
        communicator()>addObjectFactory(
            PersistentDirectory::ice_factory(), 
            PersistentDirectory::ice_staticId());
Next, we set all the NodeI static members.
        NodeI::_adapter = communicator()>
            createObjectAdapterWithEndpoints(
                "FreezeFilesystem", "default p 10000");

        NodeI::_communicator = communicator();
        NodeI::_envName = _envName;
        NodeI::_dbName = "mapfs";
Then we create a Freeze connection and a Freeze map. When the last connection to a Berkeley DB environment is closed, Freeze automatically closes this environment, so keeping a connection in the main function ensures the underlying Berkeley DB environment remains open. Likewise, we keep a map in the main function to keep the underlying Berkeley DB database open.
        Freeze::ConnectionPtr connection = 
            Freeze::createConnection(communicator(), _envName);
        IdentityNodeMap persistentMap(connection, NodeI::_dbName);
Now we need to initialize the root directory node. We first query the map for the identity of the root directory node; if no match is found, we create a new PersistentDirectory instance and insert it into the map. We use a scope to close the iterator after use; otherwise, this iterator could keep locks and prevent subsequent access to the map through another connection.
        Ice::Identity rootId = communicator()>
                        stringToIdentity("RootDir");
        PersistentDirectoryPtr pRoot;
        {
            IdentityNodeMap::iterator p = 
                persistentMap.find(rootId);
            
            if (p != persistentMap.end()) {
                pRoot = 
                    PersistentDirectoryPtr::dynamicCast(
                        p>second);
                assert(pRoot);
            } else {
                pRoot = new PersistentDirectory;
                pRoot>name = "/";
                persistentMap.insert(
                    IdentityNodeMap::value_type(rootId, pRoot));
            }
        }
Finally, the main function instantiates the FilesystemApp, passing db as the name of the database environment.
int
main(int argc, char* argv[])
{
    FilesystemApp app("db");
    return app.main(argc, argv);
}

The Servant Class Definitions

We also must change the servant classes to incorporate the Freeze map. We are maintaining the multiple-inheritance design from Chapter 35, but we have added some methods and changed the constructor arguments and state members.
Let us examine the definition of NodeI first. You will notice the addition of the getPersistentNode method, which allows NodeI to gain access to the persistent node in order to implement the Node operations. Another alternative would have been to add a PersistentNodePtr member to NodeI, but that would have forced the FileI and DirectoryI classes to downcast this member to the appropriate subclass.
namespace Filesystem {
    class NodeI : virtual public Node {
    public:
        // ... Ice operations ...
        static Ice::ObjectAdapterPtr _adapter;
        static Ice::CommunicatorPtr _communicator;
        static std::string _envName;
        static std::string _dbName;
    protected:
        NodeI(const Ice::Identity&, const DirectoryIPtr&);
        virtual PersistentNodePtr getPersistentNode() const = 0;
        PersistentNodePtr find(const Ice::Identity&) const;
        IdentityNodeMap _map;
        DirectoryIPtr _parent;
        IceUtil::RecMutex _nodeMutex;
        bool _destroyed;
    public:
        const Ice::Identity _id;
    };
}
Other changes of interest in NodeI are the addition of the members _map and _destroyed, and we have changed the NodeI constructor to accept an Ice::Identity argument.
The FileI class now has a single state member of type PersistentFilePtr, representing the persistent state of this file. Its constructor has also changed to accept Ice::Identity and PersistentFilePtr.
namespace Filesystem {
    class FileI : virtual public File,
                  virtual public NodeI {
    public:
        // ... Ice operations ...
        FileI(const Ice::Identity&, const PersistentFilePtr&,
              const DirectoryIPtr&);
    protected:
        virtual PersistentNodePtr getPersistentNode() const;
    private:
        PersistentFilePtr _file;
    };
}
The DirectoryI class has undergone a similar transformation.
namespace Filesystem {
    class DirectoryI : virtual public Directory,
                       virtual public NodeI {
    public:
        // ... Ice operations ...
        DirectoryI(const Ice::Identity&,
                   const PersistentDirectoryPtr&,
                   const DirectoryIPtr&);
        // ...
    protected:
        virtual PersistentNodePtr getPersistentNode() const;
        // ...
    private:
        // ...
        PersistentDirectoryPtr _dir;
    };
}

Implementing FileI

Let us examine how the implementations have changed. The FileI methods are still fairly trivial, but there are a few aspects that need discussion.
First, each operation now checks the _destroyed member and raises Ice::ObjectNotExistException if the member is true. This is necessary in order to ensure that the Freeze map is kept in a consistent state. For example, if we allowed the write operation to proceed after the file node had been destroyed, then we would have mistakenly added an entry back into the Freeze map for that file. Previous file system implementations ignored this issue because it was relatively harmless, but that is no longer true in this version.
Next, notice that the write operation calls put on the map after changing the text member of its PersistentFile object. Although the file’s PersistentFilePtr member points to a value in the Freeze map, changing that value has no effect on the persistent state of the map until the value is reinserted into the map, thus overwriting the previous value.
Finally, the constructor now accepts an Ice::Identity. This differs from previous implementations in that the identity used to be created by the NodeI constructor. However, as we will see later, the caller needs to determine the identity prior to invoking the subclass constructors. Similarly, the constructor is not responsible for creating a PersistentFile object, but rather is given one. This accommodates our two use cases: creating a new file, and restoring an existing file from the map.
Filesystem::Lines
Filesystem::FileI::read(const Ice::Current&)
{
    IceUtil::RWRecMutex::RLock lock(_nodeMutex);

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

    return _file>text;
}

void
Filesystem::FileI::write(const Filesystem::Lines& text,
                         const Ice::Current&)
{
    IceUtil::RWRecMutex::WLock lock(_nodeMutex);

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

    _file>text = text;
    _map.put(IdentityNodeMap::value_type(_id, _file));
}

Filesystem::FileI::FileI(const Ice::Identity& id, 
                         const PersistentFilePtr& file, 
                         const DirectoryIPtr& parent) :
    NodeI(id, parent), _file(file)
{
}

Filesystem::PersistentNodePtr
Filesystem::FileI::getPersistentNode()
{
    return _file;
}

Implementing DirectoryI

The DirectoryI implementation requires more substantial changes. We begin our discussion with the createDirectory operation.
Filesystem::DirectoryI::createDirectory(
    const std::string& name, 
    const Ice::Current& current)
{
    IceUtil::RWRecMutex::WLock lock(_nodeMutex);

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

    checkName(name);

    PersistentDirectoryPtr persistentDir 
        = new PersistentDirectory;
    persistentDir>name = name;
    DirectoryIPtr dir = new DirectoryI(
        communicator()>stringToIdentity(IceUtil::generateUUID()), 
        persistentDir, this);
    assert(find(dir>_id) == 0);
    _map.put(make_pair(dir>_id, persistentDir));

    DirectoryPrx proxy = DirectoryPrx::uncheckedCast(
        current.adapter>createProxy(dir>_id));

    NodeDesc nd;
    nd.name = name;
    nd.type = DirType;
    nd.proxy = proxy;
    _dir>nodes[name] = nd;
    _map.put(IdentityNodeMap::value_type(_id, _dir));

    return proxy;
}
After validating the node name, the operation creates a PersistentDirectory1 object for the child directory, which is passed to the DirectoryI constructor along with a unique identity. Next, we store the child’s PersistentDirectory object in the Freeze map. Finally, we initialize a new NodeDesc value and insert it into the parent’s node table and then reinsert the parent’s PersistentDirectory object into the Freeze map.
The implementation of the createFile operation has the same structure as createDirectory.
Filesystem::FilePrx
Filesystem::DirectoryI::createFile(const std::string& name, 
                                   const Ice::Current& current)
{
    IceUtil::RWRecMutex::WLock lock(_nodeMutex);

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

    checkName(name);

    PersistentFilePtr persistentFile = new PersistentFile;
    persistentFile>name = name;
    FileIPtr file = new FileI(
        communicator()>stringToIdentity(IceUtil::generateUUID()), 
        persistentFile, this);
    assert(find(file>_id) == 0);
    _map.put(make_pair(file>_id, persistentFile));

    FilePrx proxy = FilePrx::uncheckedCast(
        current.adapter>createProxy(file>_id));

    NodeDesc nd;
    nd.name = name;
    nd.type = FileType;
    nd.proxy = proxy;
    _dir>nodes[name] = nd;
    _map.put(IdentityNodeMap::value_type(_id, _dir));

    return proxy;
}
The next significant change is in the DirectoryI constructor. The body of the constructor now instantiates all of its immediate children, which effectively causes all nodes to be instantiated recursively.
For each entry in the directory’s node table, the constructor locates the matching entry in the Freeze map. The key type of the Freeze map is Ice::Identity, so the constructor obtains the key by invoking ice_getIdentity on the child’s proxy.
Filesystem::DirectoryI::DirectoryI(
    const Ice::Identity& id, 
    const PersistentDirectoryPtr& dir,                     
    const DirectoryIPtr& parent) :
    NodeI(id, parent), _dir(dir)
{
    // Instantiate the child nodes
    //
    for (NodeDict::iterator p = dir>nodes.begin(); 
         p != dir>nodes.end(); ++p) {
        Ice::Identity id = p>second.proxy>ice_getIdentity();
        PersistentNodePtr node = find(id);
        assert(node != 0);
        if (p>second.type == DirType) {
            PersistentDirectoryPtr pDir = 
                PersistentDirectoryPtr::dynamicCast(node);
            assert(pDir);
            DirectoryIPtr d = new DirectoryI(id, pDir, this);
        } else {
            PersistentFilePtr pFile = 
                PersistentFilePtr::dynamicCast(node);
            assert(pFile);
            FileIPtr f = new FileI(id, pFile, this);
        }
    }
}
If it seems inefficient for the DirectoryI constructor to immediately instantiate all of its children, you are right. This clearly will not scale well to large node trees, so why are we doing it?
Previous implementations of the file system service returned transient proxies from createDirectory and createFile. In other words, if the server was stopped and restarted, any existing child proxies returned by the old instance of the server would no longer work. However, now that we have a persistent store, we should endeavor to ensure that proxies will remain valid across server restarts. There are a couple of implementation techniques that satisfy this requirement:
1. Instantiate all of the servants in advance, as shown in the DirectoryI constructor.
2. Use a servant locator.
We chose not to include a servant locator in this example because it complicates the implementation and, as we will see in Section 40.5, a Freeze evictor is ideally suited for this application and a better choice than writing a servant locator.
The last DirectoryI method we discuss is removeChild, which removes the entry from the node table, and then reinserts the PersistentDirectory object into the map to make the change persistent.
void
Filesystem::DirectoryI::removeChild(const string& name)
{
    IceUtil::RecMutex::Lock lock(_nodeMutex);

    _dir>nodes.erase(name);
    _map.put(IdentityNodeMap::value_type(_id, _dir));
}

Implementing NodeI

There are a few changes to the NodeI implementation that should be mentioned. First, you will notice the use of getPersistentNode in order to obtain the node’s name. We could have simply invoked the name operation, but that would incur the overhead of another mutex lock.
Then, the destroy operation removes the node from the Freeze map and sets the _destroyed member to true.
The NodeI constructor no longer computes a value for the identity. This is necessary in order to make our servants truly persistent. Specifically, the identity for a node is computed once when that node is created, and must remain the same for the lifetime of the node. The NodeI constructor therefore must not compute a new identity, but rather simply remember the identity that is given to it. The NodeI constructor also constructs the map object (_map data member). Remember this object is single-threaded: we use it only in constructors or when we have a write lock on the recursive read-write _nodeMutex.
Finally, the find function illustrates what needs to be done when iterating over a database that multiple threads can use concurrently. find catches Freeze::DeadlockException and retries.
std::string
Filesystem::NodeI::name(const Ice::Current&)
{
    IceUtil::RecMutex::Lock lock(_nodeMutex);

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

    return getPersistentNode()>name;
}

void
Filesystem::NodeI::destroy(const Ice::Current& current)
{
    IceUtil::RWRecMutex::WLock lock(_nodeMutex);

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

    if (!_parent) {
        Filesystem::PermissionDenied e;
        e.reason = "cannot remove root directory";
        throw e;
    }

    _parent>removeChild(getPersistentNode()>name);
    _map.erase(current.id);
    current.adapter>remove(current.id);
    _destroyed = true;
}


Filesystem::NodeI::NodeI(const Ice::Identity& id, 
                         const DirectoryIPtr& parent)
    : _map(Freeze::createConnection(_communicator, _envName), 
           _dbName),
      _parent(parent), _destroyed(false), _id(id)
{
    // Add the identity of self to the object adapter
    //
    _adapter>add(this, _id);
}

Filesystem::PersistentNodePtr 
Filesystem::NodeI::find(const Ice::Identity& id) const 
{
    for (;;) {
        try {
            IdentityNodeMap::const_iterator p = _map.find(id);
            if (p == _map.end())
                return 0;
            else
                return p>second;
        }
        catch(const Freeze::DeadlockException&) {
            // Try again
            // 
        }
    }
}

40.4.3 Implementing the File System Server in Java

In this section we present a Java file system implementation that utilizes a Freeze map for persistent storage. The implementation is based on the one discussed in Chapter 35; in this section we only discuss code that illustrates use of the Freeze map.

Generating the Map

Now that we have selected our key and value types, we can generate the map as follows:
$ slice2freezej I$(ICE_HOME)/slice dict \
    Filesystem.IdentityNodeMap,Ice::Identity,\
    Filesystem::PersistentNode\
    Filesystem.ice $(ICE_HOME)/slice/Ice/Identity.ice
The resulting map class is named IdentityNodeMap and is defined in the package Filesystem2.

The Server main Program

The server’s main program is responsible for initializing the root directory node. Many of the administrative duties, such as creating and destroying a communicator, are handled by the class Ice.Application as described in Section 12.3.1. Our server main program has now become the following:
import Filesystem.*;

public class Server extends Ice.Application {
    public
    Server(String envName)
    {
        _envName = envName;
    }

    public int
    run(String[] args)
    {
        // Install object factories
        //
        communicator().addObjectFactory(
            PersistentFile.ice_factory(),
            PersistentFile.ice_staticId());
        communicator().addObjectFactory(
            PersistentDirectory.ice_factory(),
            PersistentDirectory.ice_staticId());

        // Create an object adapter (stored in the _adapter
        // static member)
        //
        Ice.ObjectAdapter adapter =
            communicator().createObjectAdapterWithEndpoints(
                "FreezeFilesystem", "default p 10000");
        DirectoryI._adapter = adapter;
        FileI._adapter = adapter;

        //
        // Set static members used to create connections and maps
        //
        String dbName = "mapfs";
        DirectoryI._communicator = communicator();
        DirectoryI._envName = _envName;
        DirectoryI._dbName = dbName;
        FileI._communicator = communicator();
        FileI._envName = _envName;
        FileI._dbName = dbName;
        
        // Find the persistent node for the root directory. If
        // it doesn't exist, then create it.
        Freeze.Connection connection =
            Freeze.Util.createConnection(
                communicator(), _envName);
        IdentityNodeMap persistentMap = 
            new IdentityNodeMap(connection, dbName, true); 

        Ice.Identity rootId =
            Ice.Util.stringToIdentity("RootDir");
        PersistentDirectory pRoot =
            (PersistentDirectory)persistentMap.get(rootId);
        if (pRoot == null)
        {
            pRoot = new PersistentDirectory();
            pRoot.name = "/";
            pRoot.nodes = new java.util.HashMap();
            persistentMap.put(rootId, pRoot);
        }

        // Create the root directory (with name "/" and no parent)
        //
        DirectoryI root = new DirectoryI(rootId, pRoot, null);

        // Ready to accept requests now
        //
        adapter.activate();

        // Wait until we are done
        //
        communicator().waitForShutdown();

        // Clean up
        //
        connection.close();

        return 0;
    }

    public static void
    main(String[] args)
    {
        Server app = new Server("db");
        app.main("Server", args);
        System.exit(0);
    }

    private String _envName;
}
Let us examine the changes in detail. First, we define the class Server as a subclass of Ice.Application, and provide a constructor taking a string argument:
    public
    Server(String envName)
    {
        _envName = envName;
    }
The string argument represents the name of the database environment, and is saved for later use in run.
One of the first tasks run performs is installing the Ice object factories for PersistentFile and PersistentDirectory. Although these classes are not exchanged via Slice operations, they are marshalled and unmarshalled in exactly the same way when saved to and loaded from the database, therefore factories are required. Since these Slice classes have no operations, we can use their built‑in factories.
        communicator().addObjectFactory(
            PersistentFile.ice_factory(),
            PersistentFile.ice_staticId());
        communicator().addObjectFactory(
            PersistentDirectory.ice_factory(),
            PersistentDirectory.ice_staticId());
Next, we set all the DirectoryI and FileI static members.
       Ice.ObjectAdapter adapter =
            communicator().createObjectAdapterWithEndpoints(
                "FreezeFilesystem", "default p 10000");
        DirectoryI._adapter = adapter;
        FileI._adapter = adapter;

        String dbName = "mapfs";
        DirectoryI._communicator = communicator();
        DirectoryI._envName = _envName;
        DirectoryI._dbName = dbName;
        FileI._communicator = communicator();
        FileI._envName = _envName;
        FileI._dbName = dbName;
Then we create a Freeze connection and a Freeze map. When the last connection to a Berkeley DB environment is closed, Freeze automatically closes this environment, so keeping a connection in the main function ensures the underlying Berkeley DB environment remains open. Likewise, we keep a map in the main function to keep the underlying Berkeley DB database open.
        Freeze.Connection connection =
            Freeze.Util.createConnection(
                communicator(), _envName);
        IdentityNodeMap persistentMap = 
            new IdentityNodeMap(connection, dbName, true); 
Now we need to initialize the root directory node. We first query the map for the identity of the root directory node; if no match is found, we create a new PersistentDirectory instance and insert it into the map.
        Ice.Identity rootId =
            Ice.Util.stringToIdentity("RootDir");
        PersistentDirectory pRoot =
            (PersistentDirectory)persistentMap.get(rootId);
        if (pRoot == null)
        {
            pRoot = new PersistentDirectory();
            pRoot.name = "/";
            pRoot.nodes = new java.util.HashMap();
            persistentMap.put(rootId, pRoot);
        }
Finally, the main function instantiates the Server class, passing db as the name of the database environment.
    public static void
    main(String[] args)
    {
        Server app = new Server("db");
        app.main("Server", args);
        System.exit(0);
    }

The Servant Class Definitions

We also must change the servant classes to incorporate the Freeze map. We are maintaining the design from Chapter 35, but we have added some methods and changed the constructor arguments and state members.
The FileI class has three new static members, _communicator, _envName and _dbName, that are used to instantiate the new _connection (a Freeze connection) and _map (an IdentityNodeMap) members in each FileI object. We keep the connection so that we can close it explicitly when we no longer need it.
The class also has a new instance member of type PersistentFile, representing the persistent state of this file, and a boolean member to indicate whether the node has been destroyed. Finally, we have changed its constructor to accept Ice.Identity and PersistentFile.
package Filesystem;

public class FileI extends _FileDisp
{
    public
    FileI(Ice.Identity id, PersistentFile file, DirectoryI parent)
    {
        // ...
    }

    // ... Ice operations ...

    public static Ice.ObjectAdapter _adapter;
    public static Ice.Communicator _communicator;
    public static String _envName;
    public static String _dbName;
    public Ice.Identity _id;
    private Freeze.Connection _connection;
    private IdentityNodeMap _map;
    private PersistentFile _file;
    private DirectoryI _parent;
    private boolean _destroyed;
}
The DirectoryI class has undergone a similar transformation.
package Filesystem;

public final class DirectoryI extends _DirectoryDisp
{
    public
    DirectoryI(Ice.Identity id, PersistentDirectory dir,
               DirectoryI parent)
    {
        // ...
    }

    // ... Ice operations ...

    public static Ice.ObjectAdapter _adapter;
    public static Ice.Communicator _communicator;
    public static String _envName;
    public static String _dbName;
    public Ice.Identity _id;
    private Freeze.Connection _connection;
    private IdentityNodeMap _map;
    private PersistentDirectory _dir;
    private DirectoryI _parent;
    private boolean _destroyed;
}

Implementing FileI

Let us examine how the implementations have changed. The FileI methods are still fairly trivial, but there are a few aspects that need discussion.
First, each operation now checks the _destroyed member and raises Ice::ObjectNotExistException if the member is true. This is necessary in order to ensure that the Freeze map is kept in a consistent state. For example, if we allowed the write operation to proceed after the file node had been destroyed, then we would have mistakenly added an entry back into the Freeze map for that file. Previous file system implementations ignored this issue because it was relatively harmless, but that is no longer true in this version.
Next, notice that the write operation calls put on the map after changing the text member of its PersistentFile object. Although this object is a value in the Freeze map, changing the text member has no effect on the persistent state of the map until the value is reinserted into the map, thus overwriting the previous value.
Finally, the constructor now accepts an Ice::Identity. This differs from previous implementations in that the identity used to be computed by the constructor. In order to make our servants truly persistent, the identity for a node is computed once when that node is created, and must remain the same for the lifetime of the node. The FileI constructor therefore must not compute a new identity, but rather simply remember the identity that is given to it.
Similarly, the constructor is not responsible for creating a PersistentFile object, but rather is given one. This accommodates our two use cases: creating a new file, and restoring an existing file from the map.
    public
    FileI(Ice.Identity id, PersistentFile file,
          DirectoryI parent)
    {
        _connection = 
            Freeze.Util.createConnection(_communicator, _envName);
        _map = 
            new IdentityNodeMap(_connection, _dbName, false);
        _id = id;
        _file = file;
        _parent = parent;
        _destroyed = false;

        assert(_parent != null);

        // Add the identity of self to the object adapter
        //
        _adapter.add(this, _id);
    }

    public synchronized String
    name(Ice.Current current)
    {
        if (_destroyed)
            throw new Ice.ObjectNotExistException();

        return _file.name;
    }

    public synchronized void
    destroy(Ice.Current current)
        throws PermissionDenied
    {
        if (_destroyed)
            throw new Ice.ObjectNotExistException();

        _parent.removeChild(_file.name);
        _map.remove(current.id);
        current.adapter.remove(current.id);
        _map.close();
        _connection.close();
        _destroyed = true;
    }

    public synchronized String[]
    read(Ice.Current current)
    {
        if (_destroyed)
            throw new Ice.ObjectNotExistException();

        return _file.text;
    }

    public synchronized void
    write(String[] text, Ice.Current current)
        throws GenericError
    {
        if (_destroyed)
            throw new Ice.ObjectNotExistException();

        _file.text = text;
        _map.put(_id, _file);
    }

Implementing DirectoryI

The DirectoryI implementation requires more substantial changes. We begin our discussion with the createDirectory operation.
    public synchronized DirectoryPrx
    createDirectory(String name, Ice.Current current)
        throws NameInUse, IllegalName
    {
        if (_destroyed)
            throw new Ice.ObjectNotExistException();

        checkName(name);

        PersistentDirectory persistentDir
                                = new PersistentDirectory();
        persistentDir.name = name;
        persistentDir.nodes = new java.util.HashMap();
        DirectoryI dir = new DirectoryI(
            Ice.Util.stringToIdentity(Ice.Util.generateUUID()),
            persistentDir, this);
        assert(_map.get(dir._id) == null);
        _map.put(dir._id, persistentDir);

        DirectoryPrx proxy = DirectoryPrxHelper.uncheckedCast(
            current.adapter.createProxy(dir._id));

        NodeDesc nd = new NodeDesc();
        nd.name = name;
        nd.type = NodeType.DirType;
        nd.proxy = proxy;
        _dir.nodes.put(name, nd);
        _map.put(_id, _dir);

        return proxy;
    }
After validating the node name, the operation creates a PersistentDirectory3 object for the child directory, which is passed to the DirectoryI constructor along with a unique identity. Next, we store the child’s PersistentDirectory object in the Freeze map. Finally, we initialize a new NodeDesc value and insert it into the parent’s node table and then reinsert the parent’s PersistentDirectory object into the Freeze map.
The implementation of the createFile operation has the same structure as createDirectory.
    public synchronized FilePrx
    createFile(String name, Ice.Current current)
        throws NameInUse, IllegalName
    {
        if (_destroyed)
            throw new Ice.ObjectNotExistException();

        checkName(name);

        PersistentFile persistentFile = new PersistentFile();
        persistentFile.name = name;
        FileI file = new FileI(Ice.Util.stringToIdentity(
            Ice.Util.generateUUID()), persistentFile, this);
        assert(_map.get(file._id) == null);
        _map.put(file._id, persistentFile);

        FilePrx proxy = FilePrxHelper.uncheckedCast(
            current.adapter.createProxy(file._id));

        NodeDesc nd = new NodeDesc();
        nd.name = name;
        nd.type = NodeType.FileType;
        nd.proxy = proxy;
        _dir.nodes.put(name, nd);
        _map.put(_id, _dir);

        return proxy;
    }
The next significant change is in the DirectoryI constructor. The body of the constructor now instantiates all of its immediate children, which effectively causes all nodes to be instantiated recursively.
For each entry in the directory’s node table, the constructor locates the matching entry in the Freeze map. The key type of the Freeze map is Ice::Identity, so the constructor obtains the key by invoking ice_getIdentity on the child’s proxy.
    public
    DirectoryI(Ice.Identity id, PersistentDirectory dir,
               DirectoryI parent)
    {
        _connection = 
            Freeze.Util.createConnection(_communicator, _envName);
        _map = 
            new IdentityNodeMap(_connection, _dbName, false);
        _id = id;
        _dir = dir;
        _parent = parent;
        _destroyed = false;

        // Add the identity of self to the object adapter
        //
        _adapter.add(this, _id);

        // Instantiate the child nodes
        //
        java.util.Iterator p = dir.nodes.values().iterator();
        while (p.hasNext()) {
            NodeDesc desc = (NodeDesc)p.next();
            Ice.Identity ident = desc.proxy.ice_getIdentity();
            PersistentNode node = (PersistentNode)_map.get(ident);
            assert(node != null);
            if (desc.type == NodeType.DirType) {
                PersistentDirectory pDir
                                     = (PersistentDirectory)node;
                DirectoryI d = new DirectoryI(ident, pDir, this);
            } else {
                PersistentFile pFile = (PersistentFile)node;
                FileI f = new FileI(ident, pFile, this);
            }
        }
    }
If it seems inefficient for the DirectoryI constructor to immediately instantiate all of its children, you are right. This clearly will not scale well to large node trees, so why are we doing it?
Previous implementations of the file system service returned transient proxies from createDirectory and createFile. In other words, if the server was stopped and restarted, any existing child proxies returned by the old instance of the server would no longer work. However, now that we have a persistent store, we should endeavor to ensure that proxies will remain valid across server restarts. There are a couple of implementation techniques that satisfy this requirement:
1. Instantiate all of the servants in advance, as shown in the DirectoryI constructor.
2. Use a servant locator.
We chose not to include a servant locator in this example because it complicates the implementation and, as we will see in Section 40.5, a Freeze evictor is ideally suited for this application and a better choice than writing a servant locator.
The last DirectoryI method we discuss is removeChild, which removes the entry from the node table, and then reinserts the PersistentDirectory object into the map to make the change persistent.
    synchronized void
    removeChild(String name)
    {
        _dir.nodes.remove(name);
        _map.put(_id, _dir);
    }

1
Since this Slice class has no operations, the compiler generates a concrete class that an application can instantiate.

2
We cannot generate IdentityNodeMap in the unnamed (top-level) package because Java’s name resolution rules would prevent the implementation classes in the Filesystem package from using it. See the Java Language Specification for more information.

3
Since this Slice class has no operations, the compiler generates a concrete class that an application can instantiate.

Table of Contents Previous Next
Logo