In this section, we present file system implementations that use a transactional evictor. The implementations are based on the ones discussed in
Chapter 34, and in this section we only discuss code that illustrates use of the Freeze evictor.
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.
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 39.3.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;
};
};
#include <Filesystem.ice>
module Filesystem {
class PersistentDirectory;
class PersistentNode implements Node {
string nodeName;
PersistentDirectory* parent;
};
class PersistentFile
extends PersistentNode implements File {
Lines text;
};
dictionary<string, NodeDesc> NodeDict;
class PersistentDirectory
extends PersistentNode implements Directory {
["freeze:write"]
void removeNode(string name);
NodeDict nodes;
};
};
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 39.3, 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.
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*[])
{
Ice::ObjectFactoryPtr factory = new NodeFactory;
communicator()‑>addObjectFactory(
factory, PersistentFile::ice_staticId());
communicator()‑>addObjectFactory(
factory, PersistentDirectory::ice_staticId());
Ice::ObjectAdapterPtr adapter = communicator()‑>
createObjectAdapter("EvictorFilesystem");
Freeze::EvictorPtr evictor =
Freeze::createTransactionalEvictor(
adapter, _envName, "evictorfs");
FileI::_evictor = evictor;
DirectoryI::_evictor = evictor;
adapter‑>addServantLocator(evictor, "");
Ice::Identity rootId;
rootId.name = "RootDir";
if(!evictor‑>hasObject(rootId))
{
PersistentDirectoryPtr root = new DirectoryI;
root‑>nodeName = "/";
evictor‑>add(root, rootId);
}
adapter‑>activate();
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, "config.server");
}
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) { }
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
createTransactionalEvictor. The third argument to
createTransactionalEvictor 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.
NodeI::_evictor = Freeze::createTransactionalEvictor(
adapter, _envName, "evictorfs");
adapter‑>addServantLocator(NodeI::_evictor, "");
Ice::Identity rootId;
rootId.name = "RootDir";
if(!evictor‑>hasObject(rootId))
{
PersistentDirectoryPtr root = new DirectoryI;
root‑>nodeName = "/";
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, "config.server");
}
The servant classes must also be changed to incorporate the Freeze evictor. We no longer derive the servants from a common base class. Instead,
FileI and
DirectoryI each have their own
_destroyed and
_mutex members, as well as a static
_evictor smart pointer that points at the transactional evictor.
#include <PersistentFilesystem.h>
#include <IceUtil/IceUtil.h>
#include <Freeze/Freeze.h>
namespace Filesystem {
class FileI : virtual public PersistentFile {
public:
FileI();
// Slice operations...
static Freeze::EvictorPtr _evictor;
private:
bool _destroyed;
IceUtil::Mutex _mutex;
};
class DirectoryI : virtual public PersistentDirectory {
public:
DirectoryI();
// Slice operations...
virtual void removeNode(const std::string&,
const Ice::Current&);
static Freeze::EvictorPtr _evictor;
public:
bool _destroyed;
IceUtil::Mutex _mutex;
};
namespace Filesystem {
class NodeFactory : virtual public Ice::ObjectFactory {
public:
virtual Ice::ObjectPtr create(const std::string&);
virtual void destroy();
};
The FileI methods are mostly trivial, because the Freeze evictor handles persistence for us.
Filesystem::FileI::FileI() : _destroyed(false)
{
}
string
Filesystem::FileI::name(const Ice::Current& c)
{
IceUtil::Mutex::Lock lock(_mutex);
if (_destroyed) {
throw Ice::ObjectNotExistException(
__FILE__, __LINE__, c.id, c.facet, c.operation);
}
return nodeName;
}
void
Filesystem::FileI::destroy(const Ice::Current& c)
{
{
IceUtil::Mutex::Lock lock(_mutex);
if (_destroyed) {
throw Ice::ObjectNotExistException(
__FILE__, __LINE__, c.id, c.facet, c.operation);
}
_destroyed = true;
}
//
// Because we use a transactional evictor,
// these updates are guaranteed to be atomic.
//
parent‑>removeNode(nodeName);
_evictor‑>remove(c.id);
}
Filesystem::Lines
Filesystem::FileI::read(const Ice::Current& c)
{
IceUtil::Mutex::Lock lock(_mutex);
if (_destroyed) {
throw Ice::ObjectNotExistException(
__FILE__, __LINE__, c.id, c.facet, c.operation);
}
return text;
}
void
Filesystem::FileI::write(const Filesystem::Lines& text,
const Ice::Current& c)
{
IceUtil::Mutex::Lock lock(_mutex);
if (_destroyed) {
throw Ice::ObjectNotExistException(
__FILE__, __LINE__, c.id, c.facet, c.operation);
}
this‑>text = text;
}
The code checks that the node has not been destroyed before acting on the invocation by updating or returning state. Note that
destroy must update two separate nodes: as well as removing itself from the evictor, the node must also update the parent’s node map. Because we are using a transactional evictor, the two updates are guaranteed to be atomic, so it is impossible to the leave the file system in an inconsistent state.
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(_mutex);
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;
id.name = IceUtil::generateUUID();
PersistentDirectoryPtr dir = new DirectoryI;
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(_mutex);
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;
id.name = IceUtil::generateUUID();
PersistentFilePtr file = new FileI;
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;
}
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 == PersistentFile::ice_staticId())
return new FileI;
else if (type == PersistentDirectory::ice_staticId())
return new DirectoryI;
else {
assert(false);
return 0;
}
}
void
Filesystem::NodeFactory::destroy()
{
}
The server’s main method 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)
{
Ice.ObjectFactory factory = new NodeFactory();
communicator().addObjectFactory(
factory, PersistentFile.ice_staticId());
communicator().addObjectFactory(
factory, PersistentDirectory.ice_staticId());
Ice.ObjectAdapter adapter =
communicator().createObjectAdapter(
"EvictorFilesystem");
Freeze.Evictor evictor =
Freeze.Util.createTransactionalEvictor(
adapter, _envName, "evictorfs",
null, null, null, true);
DirectoryI._evictor = evictor;
FileI._evictor = evictor;
adapter.addServantLocator(evictor, "");
Ice.Identity rootId = new Ice.Identity();
rootId.name = "RootDir";
if(!evictor.hasObject(rootId))
{
PersistentDirectory root = new DirectoryI();
root.nodeName = "/";
root.nodes =
new java.util.HashMap<
java.lang.String, NodeDesc>();
evictor.add(root, rootId);
}
adapter.activate();
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;
}
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 transactional evictor by invoking
createTransactionalEvictor. The third argument to
createTransactionalEvictor is the name of the database, the fourth is null to indicate that we do not use facets, the fifth is null to indicate that we do not use a servant initializer, the sixth argument (
null) 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.Evictor evictor =
Freeze.Util.createTransactionalEvictor(
adapter, _envName, "evictorfs",
null, null, null, true);
DirectoryI._evictor = evictor;
FileI._evictor = evictor;
adapter.addServantLocator(evictor, "");
Ice.Identity rootId = new Ice.Identity();
rootId.name = "RootDir";
if(!evictor.hasObject(rootId))
{
PersistentDirectory root = new DirectoryI();
root.nodeName = "/";
root.nodes =
new java.util.HashMap<
java.lang.String, NodeDesc>();
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");
int status = app.main("Server", args, "config.server");
System.exit(status);
}
import Filesystem.*;
public final class FileI extends PersistentFile
{
public
FileI()
{
_destroyed = false;
}
// Slice operations...
public static Freeze.Evictor _evictor;
private boolean _destroyed;
}
The DirectoryI class has undergone a similar transformation.
import Filesystem.*;
public final class DirectoryI extends PersistentDirectory
{
public
DirectoryI()
{
_destroyed = false;
nodes = new java.util.HashMap
<java.lang.String, NodeDesc>();
}
// Slice operations...
public static Freeze.Evictor _evictor;
private boolean _destroyed;
}
The FileI methods are mostly trivial, because the Freeze evictor handles persistence for us.
public synchronized String
name(Ice.Current current)
{
if (_destroyed) {
throw new Ice.ObjectNotExistException(
current.id, current.facet, current.operation);
}
return nodeName;
}
public void
destroy(Ice.Current current)
throws PermissionDenied
{
synchronized(this) {
if (_destroyed) {
throw new Ice.ObjectNotExistException(
current.id, current.facet, current.operation);
}
_destroyed = true;
}
//
// Because we use a transactional evictor,
// these updates are guaranteed to be atomic.
//
parent.removeNode(nodeName);
_evictor.remove(current.id);
}
public synchronized String[]
read(Ice.Current current)
{
if (_destroyed) {
throw new Ice.ObjectNotExistException(
current.id, current.facet, current.operation);
}
return (String[])text.clone();
}
public synchronized void
write(String[] text, Ice.Current current)
throws GenericError
{
if (_destroyed) {
throw new Ice.ObjectNotExistException(
current.id, current.facet, current.operation);
}
this.text = text;
}
The code checks that the node has not been destroyed before acting on the invocation by updating or returning state. Note that
destroy must update two separate nodes: as well as removing itself from the evictor, the node must also update the parent’s node map. Because we are using a transactional evictor, the two updates are guaranteed to be atomic, so it is impossible to the leave the file system in an inconsistent state.
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(
java.util.UUID.randomUUID().toString());
PersistentDirectory dir = new DirectoryI();
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(
java.util.UUID.randomUUID().toString());
PersistentFile file = new FileI();
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;
}
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(PersistentFile.ice_staticId()))
return new FileI();
else if (type.equals(PersistentDirectory.ice_staticId()))
return new DirectoryI();
else {
assert(false);
return null;
}
}
public void
destroy()
{
}
}