Table of Contents Previous Next
Logo
Freeze : 40.5 The Freeze Evictor
Copyright © 2003-2007 ZeroC, Inc.

40.5 The Freeze Evictor

The Freeze evictor combines persistence and scalability features into a single facility that is easily incorporated into Ice applications.
As an implementation of the ServantLocator interface (see Section 32.7), the Freeze evictor takes advantage of the fundamental separation between Ice object and servant to activate servants on‑demand from persistent storage, and to deactivate them again using customized eviction constraints. Although an application may have thousands of Ice objects in its database, it is not practical to have servants for all of those Ice objects resident in memory simultaneously. The application can conserve resources and gain greater scalability by setting an upper limit on the number of active servants, and letting the Freeze evictor handle the details of servant activation, persistence, and deactivation.
The Freeze evictor maintains a queue of active servants, ordered using a “least recently used” eviction algorithm: if the queue is full, the least recently used servant is evicted to make room for a new servant.
Here is the sequence of events for activating a servant as shown in Figure 40.3. Let us assume that we have configured the evictor with a size of five, that the queue is full, and that a request has arrived for a servant that is not currently active.
1. A client invokes an operation.
2. The object adapter invokes on the evictor to locate the servant.
3. The evictor first checks its active servant queue and fails to find the servant, so it instantiates the servant and restores its persistent state from the database.
4. The evictor adds an item for the servant (servant 1) at the head of the queue.
5. The queue’s length now exceeds the configured maximum, so the evictor removes servant 6 from the queue as soon as it is eligible for eviction. This occurs when there are no outstanding requests pending on servant 6, and its state has been safely stored in the database.
6. The object adapter dispatches the request to the new servant.
Figure 40.3. An evictor queue after restoring servant 1 and evicting servant 6.

40.5.1 Object Factories

The Freeze evictor is a generic facility in that it manages instances of Object subclasses. Applications are therefore free to use as many object types as necessary, with one requirement: an Ice object factory must be registered for each type.

40.5.2 Servant Association

With the Freeze evictor, each (object identity, facet) pair is associated with its own dedicated persistent object (servant). Such a persistent object cannot serve several identities or facets. Each servant is loaded and saved independently of other servants; in particular, there is no special grouping for the servants that serve the facets of a given Ice object.
Like an object adapter, the Freeze evictor provides operations named add, addFacet, remove and removeFacet. They have the same signature and semantics, except that with the Freeze evictor the mapping and the state of the mapped servants is stored in a database.

40.5.3 Detecting Updates

The Freeze Evictor considers that a servant has been modified when a mutating operation on this servant completes. Any mutating operation on a cached object causes the evictor to update the database with the new state of the object before evicting it from the cache. Conversely, nonmutating operations are evicted without writing the state of the object back to the database. To indicate whether an operation is mutating or not, you must add metadata directives to the Slice definitions of the objects:
• The ["freeze:write"] directive informs the evictor that an operation modifies its object.
• The ["freeze:read”] directive informs the evictor that an operation does not modify its object.
If no metadata directive is present, an operation is assumed to not modify its object.
Here is how you could mark the operations on an interface with these metadata directives:
interface Example {
    ["freeze:read"]  string readonlyOp();
    ["freeze:write"] void   writeOp();
};
This marks readonlyOp as an operation that does not modify its object, and marks writeOp as an operation that does modify its object. Because, without any directive, an operation is assumed to not modify its object, the preceding definition can also be written as follows:
interface Example {
    string readonlyOp(); // ["freeze:read"] implied
    ["freeze:write"] void writeOp();
};
The metadata directives can also be applied to an interface or a class to establish a default. This allows you to mark an interface as ["freeze:write"] and to only add a ["freeze:read"] directive to those operations that are read-only, for example:
["freeze:write"]
interface Example {
    ["freeze:read"] string readonlyOp();
                    void   writeOp1();
                    void   writeOp2();
                    void   writeOp3();
};
This marks writeOp1, writeOp2, and writeOp3 as mutating operations, and readonlyOp as a nonmutating operation.
Note that is important to correctly mark mutating operations with a ["freeze:write"] metadata directive—without the directive, Freeze will not know when an object has been modified and will evict the object from the cache without first updating the database, meaning that updates to the object are lost when the object is evicted.
Also note that, if you make calls directly on servants (so they are not dispatched via the Freeze evictor), the evictor will have no idea when an object is modified; if any such direct call modifies the object, the update may be lost when the object is evicted.

40.5.4 Saving Thread

All persistence activity of a Freeze evictor is handled in a background thread created by the evictor. This thread wakes up periodically and saves the state of all newly-registered, modified, and destroyed servant in the evictor’s queue.
For applications that experience bursts of activity, resulting in a large number of modified servants in a short period of time, the evictor’s thread can also be configured to begin saving as soon as the number of modified servants reaches a certain threshold.

40.5.5 Synchronization

When the saving thread takes a snapshot of a servant it is about to save, it is necessary to prevent the application from modifying the servant’s persistent data members at the same time.
The Freeze evictor and the application need to use a common synchronization to ensure correct behavior. In Java, this common synchronization is the servant itself: the Freeze evictor synchronizes the servant (a Java object) while taking the snapshot. In C++, the servant is required to inherit from the class IceUtil::AbstractMutex: the Freeze evictor locks the servant through this interface while taking a snapshot. On the application side, the servant’s implementation is required to synchronize all operations that access the servant’s data members defined in Slice using the same mechanism.

40.5.6 Keeping Servants in Memory

Sometimes automatically evicting and reloading all servants can be inefficient. You can remove a servant from the evictor’s queue by locking this servant “in memory” using the keep or keepFacet operation on the evictor. keep and keepFacet are recursive: you need to call release or releaseFacet for this object the same number of times to put it back in the evictor queue and make it eligible again for eviction.
Servants kept in memory (using keep or keepFacet) do not consume a slot in the evictor queue. As a result, the maximum number of servants in memory is approximately the number of kept servants plus the evictor size. It can be larger when if you have many evictable objects that are modified but not yet saved.

40.5.7 Indexing a Database

The Freeze evictor supports the use of indices to quickly find persistent objects using the value of a data member as the search criteria. The types allowed for these indices are the same as those allowed for Slice map keys (see Section 4.9.4).
The slice2freeze and slicefreezej tools can generate an Index class when passed the index option:
• index CLASS,TYPE,MEMBER
[,casesensitive|caseinsensitive]
CLASS is the name of the class to be generated. TYPE denotes the type of class to be indexed (objects of different classes are not included in this index). MEMBER is the name of the data member in TYPE to index. When MEMBER has type string, it is possible to specify whether the index is case-sensitive or not. The default is case-sensitive.
The generated Index class supplies three methods whose definitions are mapped from the following Slice operations:
• sequence<Ice::Identity>
findFirst(membertype index, int firstN)
Returns up to firstN objects of TYPE whose MEMBER is equal to index. This is useful to avoid running out of memory if the potential number of objects matching the criteria can be very large.
• sequence<Ice::Identity> find(membertype index)
Returns all the objects of TYPE whose MEMBER is equal to index.
• int count(<type> index)
Returns the number of objects of TYPE having MEMBER equal to index.
Indices are associated with a Freeze evictor during evictor creation. See the definition of the createEvictor methods for details.
Indexed searches are easy to use and very efficient. However, be aware that an index adds significant write overhead: with Berkeley DB, every update triggers a read from the database to get the old index entry and, if necessary, replace it.
If you add an index to an existing database, by default existing facets are not indexed. If you need to populate a new or empty index using the facets stored in your Freeze evictor, set the property Freeze.Evictor.envname.filename.PopulateEmptyIndices to a value other than 0, which instructs Freeze to iterate over the corresponding facets and create the missing index entries during the call to createEvictor. When you use this feature, you must register the object factories for all of the facet types before you invoke createEvictor.

40.5.8 Using a Servant Initializer

In some applications it may be necessary to initialize a servant after it is instantiated by the evictor but before an operation is dispatched to it. The Freeze evictor allows an application to specify a servant initializer for this purpose.
To clarify the sequence of events, let us assume that a request has arrived for an Ice facet that is not currently active:
1. The evictor restores a servant for the Ice facet from the database. This involves two steps:
1. The Ice run time locates and invokes the factory for the Ice facet’s type, thereby obtaining a new instance with uninitialized data members.
2. The data members are populated from the persistent state.
2. The evictor invokes the application’s servant initializer (if any) for the servant.
3. The evictor adds the servant to its cache.
4. The evictor dispatches the operation.
The servant initializer is called before the object is inserted into the Freeze evictor internal cache, and without holding any internal lock, but in such a way that when the servant initializer is called, the servant is guaranteed to be inserted in the Freeze evictor cache.
There is only one restriction on a what a servant initializer can do: it must not make a remote invocation on the facet being initialized. Failing to follow this rule will result in deadlocks.
The file system implementation presented in Section 40.6 on page 1420 demonstrates the use of a servant initializer.

40.5.9 Application Design Considerations

The Freeze evictor creates a snapshot of an Ice facet’s state for persistent storage by marshaling the facet, just as if the facet were being sent “over the wire” as a parameter to a remote invocation. Therefore, the Slice definitions for an object type must include the data members comprising the object’s persistent state.
For example, we could define a Slice class as follows:
class Stateless {
    void calc();
};
However, without data members, there will not be any persistent state in the database for objects of this type, and hence there is little value in using the Freeze evictor for this type.
Obviously, Slice object types need to define data members, but there are other design considerations as well. For example, suppose we define a simple application as follows:
class Account {
    void withdraw(int amount);
    void deposit(int amount);

    int balance;
};

interface Bank {
    Account* createAccount();
};
In this application, we would use the Freeze evictor to manage Account objects that have a data member balance representing the persistent state of an account.
From an object-oriented design perspective, there is a glaring problem with these Slice definitions: implementation details (the persistent state) are exposed in the client-server contract. The client cannot directly manipulate the balance member because the Bank interface returns Account proxies, not Account instances. However, the presence of the data member may cause unnecessary confusion for client developers.
A better alternative is to clearly separate the persistent state as shown below:
interface Account {
    void withdraw(int amount);
    void deposit(int amount);
};

interface Bank {
    Account* createAccount();
};

class PersistentAccount implements Account {
    int balance;
};
Now the Freeze evictor can manage PersistentAccount objects, while clients interact with Account proxies. (Ideally, PersistentAccount would be defined in a different source file and inside a separate module.)
Table of Contents Previous Next
Logo