Table of Contents Previous Next
Logo
Freeze : 36.6 Using the Freeze Evictor in a File System Server
Copyright © 2003-2008 ZeroC, Inc.

36.6 Using the Freeze Evictor in a File System Server

In this section, we present file system implementations that use a background-save evictor. The implementations are based on the ones discussed in Chapter 31, and in this section we only discuss code that illustrates use of the Freeze evictor.
In general, incorporating a Freeze evictor into your application requires the following steps:
1. Evaluate your existing Slice definitions for a suitable persistent object type.
2. If no suitable type is found, you typically define a new derived class that captures your persistent state requirements. Consider placing these definitions in a separate file: they 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 code (using slice2freeze or slice2freezej) for your new definitions.
4. Create an evictor and register it as a servant locator with an object adapter.
5. Create instances of your persistent type and register them with the evictor.

36.6.1 Slice Definitions

Fortunately, it is unnecessary for us to change any of the existing file system Slice definitions to incorporate the Freeze evictor. However, we do need to add metadata definitions to inform the evictor which operations modify object state (see Section 36.5.5):
module Filesystem
{
    // ...

    interface Node
    {
        idempotent string name();

        ["freeze:write"]
        void destroy() throws PermissionDenied;
    };

    // ...

    interface File extends Node
    {
        idempotent Lines read();

        ["freeze:write"]
        idempotent void write(Lines text) throws GenericError;
    };

    // ...

    interface Directory extends Node
    {
        idempotent NodeDescSeq list();

        idempotent NodeDesc find(string name) throws NoSuchName;

        ["freeze:write"]
        File* createFile(string name) throws NameInUse;

        ["freeze:write"]
        Directory* createDirectory(string name) throws NameInUse;
    };
};
This definitions are identical to the original ones, with the exception of the added ["freeze:write"] directives.
The remaining definitions are in derived classes:
module Filesystem {
    class PersistentDirectory;

    class PersistentNode implements Node {
        string nodeName;
        PersistentDirectory* parent;
    };

    class PersistentFile extends PersistentNode implements File {
        Lines text;
    };

    class PersistentDirectory extends PersistentNode
        implements Directory {
        void removeNode(string name);

        NodeDict nodes;
    };
};
As you can see, we have sub-classed all of the node interfaces. Let us examine each one in turn.
The PersistentNode class adds two data members: nodeName1 and parent. The file system implementation requires that a child node knows its parent node in order to properly implement the destroy operation. Previous implementations had a state member of type DirectoryI, but that is not workable here. It is no longer possible to pass the parent node to the child node’s constructor because the evictor may be instantiating the child node (via a factory), and the parent node will not be known. Even if it were known, another factor to consider is that there is no guarantee that the parent node will be active when the child invokes on it, because the evictor may have evicted it. We solve these issues by storing a proxy to the parent node. If the child node invokes on the parent node via the proxy, the evictor automatically activates the parent node if necessary.
The PersistentFile class is very straightforward, simply adding a text member representing the contents of the file. Notice that the class extends PersistentNode, and therefore inherits the state members declared by the base class.
Finally, the PersistentDirectory class defines the removeNode operation, and adds the nodes state member representing the immediate children of the directory node. Since a child node contains only a proxy for its PersistentDirectory parent, and not a reference to an implementation class, there must be a Slice-defined operation that can be invoked when the child is destroyed.
If we had followed the advice at the beginning of Section 36.5, we would have defined Node, File, and Directory classes in a separate PersistentFilesystem module, but in this example we use the existing Filesystem module for the sake of simplicity.

36.6.2 Implementing the File System Server in C++

The Server main Program

The server’s main program is responsible for creating the evictor and 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 <PersistentFilesystemI.h>

using namespace std;
using namespace Filesystem;

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

    virtual int run(int, char*[])
    {
        // Install object factories.
        //
        Ice::ObjectFactoryPtr factory = new NodeFactory;
        communicator()>addObjectFactory(
            factory, PersistentFile::ice_staticId());
        communicator()>addObjectFactory(
            factory, PersistentDirectory::ice_staticId());

        // Create an object adapter.
        //
        Ice::ObjectAdapterPtr adapter =
            communicator()>createObjectAdapterWithEndpoints(
                                "FreezeFilesystem", "default p 10000");

        // Create the Freeze evictor (stored
        // in the NodeI::_evictor static member).
        //
        Freeze::ServantInitializerPtr init = new NodeInitializer;
        NodeI::_evictor = Freeze::createBackgroundSaveEvictor(
                           adapter, _envName, "evictorfs", init);
        adapter>addServantLocator(NodeI::_evictor, "");

        // Create the root node if it doesn't exist.
        //
        Ice::Identity rootId =
            communicator()>stringToIdentity("RootDir");
        if (!NodeI::_evictor>hasObject(rootId)) {
            PersistentDirectoryPtr root = new DirectoryI(rootId);
            root>nodeName = "/";
            NodeI::_evictor>add(root, rootId);
        }

        // Ready to accept requests now.
        //
        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. A single instance of NodeFactory is installed for both types.
        Ice::ObjectFactoryPtr factory = new NodeFactory;
        communicator()>addObjectFactory(
            factory,
            PersistentFile::ice_staticId());
        communicator()>addObjectFactory(
            factory,
            PersistentDirectory::ice_staticId());
After creating the object adapter, the program initializes a Freeze evictor by invoking createBackgroundSaveEvictor. The third argument to createBackgroundSaveEvictor is the name of the database file, which by default is created if it does not exist. The new evictor is then added to the object adapter as a servant locator for the default category.
        Freeze::ServantInitializerPtr init = new NodeInitializer;
        NodeI::_evictor = Freeze::createBackgroundSaveEvictor(
                           adapter, _envName, "evictorfs", init);
        adapter>addServantLocator(NodeI::_evictor, "");
Next, the program creates the root directory node if it is not already being managed by the evictor.
        Ice::Identity rootId =
            communicator()>stringToIdentity("RootDir");
        if (!NodeI::_evictor>hasObject(rootId)) {
            PersistentDirectoryPtr root = new DirectoryI(rootId);
            root>nodeName = "/";
            NodeI::_evictor>add(root, rootId);
        }
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

The servant classes must also be changed to incorporate the Freeze evictor. We are maintaining the multiple-inheritance design from Chapter 31, but we have changed the constructors and state members. In particular, each node implementation class has two constructors, one taking no parameters and one taking an Ice::Identity. The former is needed by the factory, and the latter is used when the node is first created. To comply with the background-save evictor requirements, NodeI implements IceUtil::AbstractMutex by deriving from the template class IceUtil::AbstractMutexI. The only other change of interest is a new state member in NodeI named _evictor.
namespace Filesystem {
    class NodeI : virtual public PersistentNode,
                  public IceUtil::AbstractMutexI<IceUtil::Mutex> {
    public:
        static Freeze::EvictorPtr _evictor;

    protected:
        NodeI();
        NodeI(const Ice::Identity&);

    public:
        const Ice::Identity _id;
    };

    class FileI : virtual public PersistentFile,
                  virtual public NodeI {
    public:
        virtual std::string name(const Ice::Current&);
        virtual void destroy(const Ice::Current&);

        virtual Lines read(const Ice::Current&);
        virtual void write(const Lines&, const Ice::Current&);

        FileI();
        FileI(const Ice::Identity&);
    };

    class DirectoryI : virtual public PersistentDirectory,
                       virtual public NodeI {
    public:
        virtual std::string name(const Ice::Current&);
        virtual void destroy(const Ice::Current&);

        virtual NodeDescSeq list(const Ice::Current&);
        virtual NodeDesc find(const std::string&,
                              const Ice::Current&);
        virtual DirectoryPrx createDirectory(const std::string&,
                                             const Ice::Current&);
        virtual FilePrx createFile(const std::string&,
                                   const Ice::Current&);
        virtual void removeNode(const std::string&,
                                const Ice::Current&);

        DirectoryI();
        DirectoryI(const Ice::Identity&);

    private:
        bool _destroyed;
    };
}
In addition to the node implementation classes, we have also declared implementations of an object factory and a servant initializer:
namespace Filesystem {
    class NodeFactory : virtual public Ice::ObjectFactory {
    public:
        virtual Ice::ObjectPtr create(const std::string&);
        virtual void destroy();
    };

    class NodeInitializer
        : virtual public Freeze::ServantInitializer {
    public:
        virtual void initialize(const Ice::ObjectAdapterPtr&,
                                const Ice::Identity&,
                                const std::string&,
                                const Ice::ObjectPtr&);
    };
}

Implementing NodeI

Notice that 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 remember the identity that is given to it.
Filesystem::NodeI::NodeI()
{
}

Filesystem::NodeI::NodeI(const Ice::Identity& id)
    : _id(id)
{
}

Implementing FileI

The FileI methods are mostly trivial, because the Freeze evictor handles persistence for us.
string
Filesystem::FileI::name(const Ice::Current&)
{
    return nodeName;
}

void
Filesystem::FileI::destroy(const Ice::Current&)
{
    parent>removeNode(nodeName);
    _evictor>remove(_id);
}

Filesystem::Lines
Filesystem::FileI::read(const Ice::Current&)
{
    IceUtil::Mutex::Lock lock(*this);

    return text;
}

void
Filesystem::FileI::write(const Filesystem::Lines& text,
                         const Ice::Current&)
{
    IceUtil::Mutex::Lock lock(*this);

    this>text = text;
}

Filesystem::FileI::FileI()
{
}

Filesystem::FileI::FileI(const Ice::Identity& id)
    : NodeI(id)
{
}

Implementing DirectoryI

The DirectoryI implementation requires more substantial changes. We begin our discussion with the createDirectory operation.
Filesystem::DirectoryPrx
Filesystem::DirectoryI::createDirectory(
    const string& name,
    const Ice::Current& c)
{
    IceUtil::Mutex::Lock lock(*this);

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

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

    Ice::Identity id = c.adapter>getCommunicator()>
        stringToIdentity(IceUtil::generateUUID());
    PersistentDirectoryPtr dir = new DirectoryI(id);
    dir>nodeName = name;
    dir>parent = PersistentDirectoryPrx::uncheckedCast(
                                c.adapter>createProxy(c.id));
    DirectoryPrx proxy = DirectoryPrx::uncheckedCast(
                                _evictor>add(dir, id));

    NodeDesc nd;
    nd.name = name;
    nd.type = DirType;
    nd.proxy = proxy;
    nodes[name] = nd;

    return proxy;
}
After validating the node name, the operation obtains a unique identity for the child directory, instantiates the servant, and registers it with the Freeze evictor. Finally, the operation creates a proxy for the child and adds the child to its node table.
The implementation of the createFile operation has the same structure as createDirectory.
Filesystem::FilePrx
Filesystem::DirectoryI::createFile(
    const string& name,
    const Ice::Current& c)
{
    IceUtil::Mutex::Lock lock(*this);

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

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

    Ice::Identity id = c.adapter>getCommunicator()>
        stringToIdentity(IceUtil::generateUUID());
    PersistentFilePtr file = new FileI(id);
    file>nodeName = name;
    file>parent = PersistentDirectoryPrx::uncheckedCast(
                        c.adapter>createProxy(c.id));
    FilePrx proxy = FilePrx::uncheckedCast(
                        _evictor>add(file, id));

    NodeDesc nd;
    nd.name = name;
    nd.type = FileType;
    nd.proxy = proxy;
    nodes[name] = nd;

    return proxy;
}

Implementing NodeFactory

We use a single factory implementation for creating two types of Ice objects: PersistentFile and PersistentDirectory. These are the only two types that the Freeze evictor will be restoring from its database.
Ice::ObjectPtr
Filesystem::NodeFactory::create(const string& type)
{
    if (type == "::Filesystem::PersistentFile")
        return new FileI;
    else if (type == "::Filesystem::PersistentDirectory")
        return new DirectoryI;
    else {
        assert(false);
        return 0;
    }
}

void
Filesystem::NodeFactory::destroy()
{
}

Implementing NodeInitializer

NodeInitializer is a trivial implementation of the Freeze::ServantInitializer interface whose only responsibility is setting the _id member of the node implementation. The evictor invokes the initialize operation after the evictor has created a servant and restored its persistent state from the database, but before any operations are dispatched to it.
void
Filesystem::NodeInitializer::initialize(
    const Ice::ObjectAdapterPtr&,
    const Ice::Identity& id,
    const string& facet,
    const Ice::ObjectPtr& obj)
{
    NodeIPtr node = NodeIPtr::dynamicCast(obj);
    assert(node);
    const_cast<Ice::Identity&>(node>_id) = id;
}

36.6.3 Implementing the File System Server in Java

The Server main Program

The server’s main program is responsible for creating the evictor and 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.
        //
        Ice.ObjectFactory factory = new NodeFactory();
        communicator().addObjectFactory(
                factory,
                PersistentFile.ice_staticId());
        communicator().addObjectFactory(
            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;

        //
        // Create the Freeze evictor (stored in the _evictor
        // static member).
        //
        Freeze.ServantInitializer init = new NodeInitializer();
        Freeze.Evictor evictor =
            Freeze.Util.createBackgroundSaveEvictor(
                adapter, _envName, "evictorfs", init, null, true);
        DirectoryI._evictor = evictor;
        FileI._evictor = evictor;

        adapter.addServantLocator(evictor, "");

        //
        // Create the root node if it doesn't exist.
        //
        Ice.Identity rootId =
            Ice.Util.stringToIdentity("RootDir");
        if (!evictor.hasObject(rootId))
        {
            PersistentDirectory root = new DirectoryI(rootId);
            root.nodeName = "/";
            root.nodes = new java.util.HashMap();
            evictor.add(root, rootId);
        }

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

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

        return 0;
    }

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

    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. A single instance of NodeFactory is installed for both types.
        Ice.ObjectFactory factory = new NodeFactory();
        communicator().addObjectFactory(
            factory,
            PersistentFile.ice_staticId());
        communicator().addObjectFactory(
            factory,
            PersistentDirectory.ice_staticId());
After creating the object adapter, the program initializes a background-save evictor by invoking createBackgroundSaveEvictor. The third argument to createBackgroundSaveEvictor is the name of the database, the fourth is the servant initializer, then the null argument indicates no indexes are in use, and the true argument requests that the database be created if it does not exist. The evictor is then added to the object adapter as a servant locator for the default category.
        Freeze.ServantInitializer init = new NodeInitializer();
        Freeze.Evictor evictor =
            Freeze.Util.createBackgroundSaveEvictor(
                adapter, _envName, "evictorfs", init, null, true);
        DirectoryI._evictor = evictor;
        FileI._evictor = evictor;
        
        adapter.addServantLocator(evictor, "");
Next, the program creates the root directory node if it is not already being managed by the evictor.
        Ice.Identity rootId =
            Ice.Util.stringToIdentity("RootDir");
        if (!evictor.hasObject(rootId))
        {
            PersistentDirectory root = new DirectoryI(rootId);
            root.nodeName = "/";
            root.nodes = new java.util.HashMap();
            evictor.add(root, rootId);
        }
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

The servant classes must also be changed to incorporate the Freeze evictor. We are maintaining the design from Chapter 31, but we have changed the constructors and state members. In particular, each node implementation class has two constructors, one taking no parameters and one taking an Ice::Identity. The former is needed by the factory, and the latter is used when the node is first created. The only other change of interest is the new state member _evictor.
package Filesystem;

public class FileI extends PersistentFile
{
    public
    FileI()
    {
        // ...
    }

    public
    FileI(Ice.Identity id)
    {
        // ...
    }

    // ... Ice operations ...

    public static Freeze.Evictor _evictor;
    public Ice.Identity _id;
}
The DirectoryI class has undergone a similar transformation.
package Filesystem;

public final class DirectoryI extends PersistentDirectory
{
    public
    DirectoryI()
    {
        // ...
    }

    public
    DirectoryI(Ice.Identity id)
    {
        // ...
    }

    // ... Ice operations ...

    public static Freeze.Evictor _evictor;
    public Ice.Identity _id;
}
Notice that the constructors no longer compute 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. A constructor therefore must not compute a new identity, but rather remember the identity given to it.

Implementing FileI

The FileI methods are mostly trivial, because the Freeze evictor handles persistence for us.
    public
    FileI()
    {
    }

    public
    FileI(Ice.Identity id)
    {
        _id = id;
    }

    public String
    name(Ice.Current current)
    {
        return nodeName;
    }

    public void
    destroy(Ice.Current current)
        throws PermissionDenied
    {
        parent.removeNode(nodeName);
        _evictor.remove(_id);
    }

    public synchronized String[]
    read(Ice.Current current)
    {
        return (String[])text.clone();
    }

    public synchronized void
    write(String[] text, Ice.Current current)
        throws GenericError
    {
        this.text = text;
    }

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
    {
        if (_destroyed) {
            throw new Ice.ObjectNotExistException(
                        current.id,
                        current.facet,
                        current.operation);
        }

        if (name.length() == 0 || nodes.containsKey(name))
            throw new NameInUse(name);

        Ice.Identity id =
            current.adapter.getCommunicator().stringToIdentity(
                Ice.Util.generateUUID());
        PersistentDirectory dir = new DirectoryI(id);
        dir.nodeName = name;
        dir.parent = PersistentDirectoryPrxHelper.uncheckedCast(
            current.adapter.createProxy(current.id));
        DirectoryPrx proxy = DirectoryPrxHelper.uncheckedCast(
            _evictor.add(dir, id));

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

        return proxy;
    }
After validating the node name, the operation obtains a unique identity for the child directory, instantiates the servant, and registers it with the Freeze evictor. Finally, the operation creates a proxy for the child and adds the child to its node table.
The implementation of the createFile operation has the same structure as createDirectory.
    public synchronized FilePrx
    createFile(String name, Ice.Current current)
        throws NameInUse
    {
        if(_destroyed)
        {
            throw new Ice.ObjectNotExistException(
                            current.id,
                            current.facet,
                            current.operation);
        }

        if(name.length() == 0 || nodes.containsKey(name))
            throw new NameInUse(name);

        Ice.Identity id =
            current.adapter.getCommunicator().stringToIdentity(
                Ice.Util.generateUUID());
        PersistentFile file = new FileI(id);
        file.nodeName = name;
        file.parent = PersistentDirectoryPrxHelper.uncheckedCast(
            current.adapter.createProxy(current.id));
        FilePrx proxy = FilePrxHelper.uncheckedCast(
            _evictor.add(file, id));

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

        return proxy;
    }

Implementing NodeFactory

We use a single factory implementation for creating two types of Ice objects: PersistentFile and PersistentDirectory. These are the only two types that the Freeze evictor will be restoring from its database.
package Filesystem;

public class NodeFactory implements Ice.ObjectFactory
{
    public Ice.Object
    create(String type)
    {
        if (type.equals("::Filesystem::PersistentFile"))
            return new FileI();
        else if (type.equals("::Filesystem::PersistentDirectory"))
            return new DirectoryI();
        else {
            assert(false);
            return null;
        }
    }

    public void
    destroy()
    {
    }
}

Implementing NodeInitializer

NodeInitializer is a trivial implementation of the Freeze::ServantInitializer interface whose only responsibility is setting the _id member of the node implementation. The evictor invokes the initialize operation after the evictor has created a servant and restored its persistent state from the database, but before any operations are dispatched to it.
package Filesystem;

public class NodeInitializer implements Freeze.ServantInitializer {
    public void
    initialize(Ice.ObjectAdapter adapter, Ice.Identity id,
               String facet, Ice.Object obj)
    {
        if (obj instanceof FileI)
            ((FileI)obj)._id = id;
        else
            ((DirectoryI)obj)._id = id;
    }
}

1
We used nodeName instead of name because name is already used as an operation in the Node interface.

Table of Contents Previous Next
Logo