Table of Contents Previous Next
Logo
Freeze : 36.5 Freeze Evictors
Copyright © 2003-2008 ZeroC, Inc.

36.5 Freeze Evictors

Freeze evictors combine persistence and scalability features into a single facility that is easily incorporated into Ice applications.
As an implementation of the ServantLocator interface (see Section 28.7), a 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 a Freeze evictor handle the details of servant activation, persistence, and deactivation.

36.5.1 Specifying Persistent State

The persistent state of servants managed by a Freeze evictor must be described in Slice. Specifically, every servants must implement a Slice class, and a Freeze evictor automatically stores and retrieves all the (Slice-defined) data members of these Slice classes. Data members that are not specified in Slice are not persistent.
A Freeze evictor relies on the Ice object factory facility to load persistent servants from disk: the evictor creates a brand new servant using the registered factory and then restores the servant’s data members. Therefore, for every persistent servant class you define, you need to register a corresponding object factory with the Ice communicator. (See the relevant language mapping chapter for more details on object factories.)

36.5.2 Servant Association

With a 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.

36.5.3 Background Save and Transactional Evictors

Freeze provides two types of evictors, a background save evictor and a transactional evictor, with corresponding local interfaces: BackgroundSaveEvictor and TransactionalEvictor. These two local interfaces derive from the Freeze::Evictor local interface, which defines most evictor operations, in particular add, addFacet, remove, and removeFacet.
Furthermore, the on-disk format of these two types of evictors is the same: you can switch from one type of evictor to the other without any data transformation.

Background Save Evictor

A background-save evictor keeps all its servants in a map and writes the state of newly created, modified, and deleted servants to disk asynchronously, in a background thread. You can configure how often servants are saved; for example you could decide the save every three minutes, or whenever ten or more servants have been modified. For applications with frequent updates, this allows you to group many updates together to improve performance.
The downside of the background-save evictor is recovery from a crash. Because saves are asynchronous, there is no way to "save now" a critical update. Moreover, you cannot group several related updates together: for example, if you transfer funds between two accounts (servants) and a crash occurs shortly after this update, it is possible that, once your application comes back up, you will see the update on one account but not on the other. Your application needs to handle such inconsistency when restarting after a crash.
Similarly, a background-save evictor provides no ordering guarantees for saves. If you update servant 1, servant 2, and then servant 1 again, it is possible that, after recovering from a crash, you will see the latest state for servant 1, but no updates at all for servant 2.

Transactional Evictor

A transactional evictor maintains a servant map as well, but only keeps read-only servants in this map. The state of these servants corresponds to the latest data on disk. Any servant creation, update, or deletion is performed within a database transaction. This transaction is committed (or rolled back) immediately, typically at the end of the dispatch of the current operation, and the associated servants are then discarded.
With such an evictor, you can ensure that several updates, often on different servants (possibly managed by different transactional evictors) are grouped together: either all or none of these updates occur. In addition, updates are written almost immediately, so crash recovery is a lot simpler: few (if any) updates will be lost, and you can maintain consistency between related persistent objects.
However, an application based on a transactional evictor is likely to write a lot more to disk than an application with a background-save evictor, which may have negative performance impact.

36.5.4 Eviction Strategy

Both background-save and transactional evictors associate a queue with their servant map, and manage this queue 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 36.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. (With a transactional evictor, we also assume this request does not change any persistent state.)
1. A client invokes an operation.
2. The object adapter invokes on the evictor to locate the servant.
3. The evictor first checks its servant map 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. With a background save evictor, this occurs once there are no outstanding requests pending on servant 6, and once the servant’s state has been safely stored in the database. With a transactional save, the servant is removed from the queue immediately.
6. The object adapter dispatches the request to the new servant.
Figure 36.3. An evictor queue after restoring servant 1 and evicting servant 6.

36.5.5 Detecting Updates

A Freeze evictor considers that a servant’s persistent state has been modified when a read-write operation on this servant completes. To indicate whether an operation is read-only or read-write, you add metadata directives to the Slice definitions of the objects:
• The ["freeze:write"] directive informs the evictor that an operation modifies the persistent state of the target servant.
• The ["freeze:read"] directive informs the evictor that an operation does not modify the persistent state of the target.
If no metadata directive is present, an operation is assumed to not modify its target.
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 target, and marks writeOp as an operation that does modify its target. Because, without any directive, an operation is assumed to not modify its target, 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 read-write operations, and readonlyOp as a read-only operation.
Note that it is important to correctly mark read-write operations with a ["freeze:write"] metadata directive—without the directive, Freeze will not know when an object has been modified and may not store the updated persistent state to disk.
Also note that, if you make calls directly on servants (so the calls are not dispatched via the Freeze evictor), the evictor will have no idea when a servant’s persistent state is modified; if any such direct call modifies the servant’s data members, the update may be lost.

36.5.6 Evictor Iterator

A Freeze evictor iterator provides the ability to iterate over the identities of the objects stored in an evictor. The operations are similar to Java iterator methods: hasNext returns true while there more elements, and next returns the next identity:
local interface EvictorIterator {
    bool hasNext();
    Ice::Identity next();
};
You create such an iterator by calling getIterator on your evictor:
EvictorIterator getIterator(string facet, int batchSize);
The new iterator is specific to a facet (specified by the facet parameter). Internally, this iterator will retrieve identities in batches of batchSize objects; we recommend to use a fairly large batch size to get good performance.

36.5.7 Indexing a Database

A Freeze evictor supports the use of indexes to quickly find persistent servants using the value of a data member as the search criteria. The types allowed for these indexes are the same as those allowed for Slice map keys (see Section 4.9.4).
The slice2freeze and slice2freezej 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.
Indexes are associated with a Freeze evictor during evictor creation. See the definition of the createBackgroundSaveEvictor and createTransactionalEvictor functions/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 createBackgroundSaveEvictor or createTransactionalEvictor. When you use this feature, you must register the object factories for all of the facet types before you create your evictor.

36.5.8 Using a Servant Initializer

In some applications, it may be necessary to initialize a servant after the servant is instantiated by the evictor but before an operation is dispatched to the servant. The Freeze evictor allows an application to specify a servant initializer for this purpose.
To illustrate the sequence of events, let us assume that a request has arrived for a servant that is not currently active:
1. The evictor restores a servant for the target Ice object (and 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. If the evictor is a background-save evictor, it adds the servant to its cache.
4. The evictor dispatches the operation.
With a background-save evictor, the servant initializer is called before the object is inserted into the evictor’s 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 evictor cache.
There is only one restriction on what a servant initializer can do: it must not make a remote invocation on the object (facet) being initialized. Failing to follow this rule will result in deadlocks.
The file system implementation presented in Section 36.6 on page 1390 demonstrates the use of a servant initializer.

36.5.9 Background-Save Evictor Features

Creation

You create a background-save evictor in C++ with the global function Freeze::createBackgroundSaveEvictor, and in Java with the static method Freeze.Util.createBackgroundSaveEvictor.
For C++, the signatures are as follows:
BackgroundSaveEvictorPtr
createBackgroundSaveEvictor(
    const ObjectAdapterPtr& adapter,
    const string& envName,
    const string& filename,
    const ServantInitializerPtr& initializer = 0,
    const vector<IndexPtr>& indexes = vector<IndexPtr>(),
    bool createDb = true);

BackgroundSaveEvictorPtr
    createBackgroundSaveEvictor(
    const ObjectAdapterPtr& adapter,
    const string& envName,
    DbEnv& dbEnv,
    const string& filename,
    const ServantInitializerPtr& initializer = 0,
    const vector<IndexPtr>& indexes = vector<IndexPtr>(),
    bool createDb = true);
For Java, the the method signatures are:
public static BackgroundSaveEvictor
createBackgroundSaveEvictor(
    Ice.ObjectAdapter adapter,
    String envName,
    String filename,
    ServantInitializer initializer,
    Index[] indexes,
    boolean createDb);

public static BackgroundSaveEvictor
createBackgroundSaveEvictor(
    Ice.ObjectAdapter adapter,
    String envName,
    com.sleepycat.db.Environment dbEnv,
    String filename,
    ServantInitializer initializer,
    Index[] indexes,
    boolean createDb);
Both C++ and Java provide two overloaded functions or methods: in one case, Freeze opens and manages the underlying Berkeley DB environment; in the other case, you provide a DbEnv object that represents a Berkeley DB environment you opened yourself. (Usually, it is easiest to let Freeze take care of all interactions with Berkeley DB.)
The envName parameter represents the name of the underlying Berkeley DB environment, and is also used as the default Berkeley DB home directory. (See Freeze.Evictor.env-name.DbHome in the Ice properties reference.)
The filename parameter represents the name of the Berkeley DB database file associated with this evictor. The persistent state of all your servants is stored in this file.
The initializer parameter represents the servant initializer. It is an optional parameter in C++; in Java, pass null if you do not need a servant initializer.
The indexes parameter is a vector or array of evictor indexes. It is an optional parameter in C++; in Java, pass null if your evictor does not define any index.
Finally, the createDb parameter tells Freeze what to do when the corresponding Berkeley DB database does not exist. When true, Freeze creates a new database; when false, Freeze raises a Freeze::DatabaseException.

Saving Thread

All persistence activity of a background-save 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 that result in a large number of modified servants in a short period of time, you can also configure the evictor’s thread to begin saving as soon as the number of modified servants reaches a certain threshold.

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 background-save 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 that are defined in Slice using the same mechanism.

Keeping Servants in Memory

Occasionally, 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.
local interface BackgroundSaveEvictor extends Evictor {
    void keep(Ice::Identity id);
    void keepFacet(Ice::Identity id, string facet);
    void release(Ice::Identity id);
    void releaseFacet(Ice::Identity id, string facet);
};
keep and keepFacet are recursive: you need to call release or releaseFacet for this servant 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 while you have many evictable objects that are modified but not yet saved.)

36.5.10 Transactional Evictor Features

Creation

You create a transactional evictor in C++ with the global function Freeze::createTransactionalEvictor, and in Java with the static method Freeze.Util.createTransactionalEvictor.
For C++, the signatures are as follows:
typedef map<string, string> FacetTypeMap;

TransactionalEvictorPtr
createTransactionalEvictor(
    const ObjectAdapterPtr& adapter,
    const string& envName,
    const string& filename,
    const FacetTypeMap& facetTypes = FacetTypeMap(),
    const ServantInitializerPtr& initializer = 0,
    const vector<IndexPtr>& indexes = vector<IndexPtr>(),
    bool createDb = true);

TransactionalEvictorPtr
createTransactionalEvictor(
    const ObjectAdapterPtr& adapter,
    const string& envName,
    DbEnv& dbEnv,
    const string& filename,
    const FacetTypeMap& facetTypes = FacetTypeMap(),
    const ServantInitializerPtr& initializer = 0,
    const vector<IndexPtr>& indexes = vector<IndexPtr>(),
    bool createDb = true);
For Java, the the method signatures are:
public static TransactionalEvictor
createTransactionalEvictor(
    Ice.ObjectAdapter adapter,
    String envName,
    String filename,
    java.util.Map facetTypes,
    ServantInitializer initializer,
    Index[] indexes,
    boolean createDb);

public static TransactionalEvictor
createTransactionalEvictor(
    Ice.ObjectAdapter adapter,
    String envName,
    com.sleepycat.db.Environment dbEnv,
    String filename,
    java.util.Map facetTypes,
    ServantInitializer initializer,
    Index[] indexes,
    boolean createDb);
Both C++ and Java provide two overloaded functions or methods: in one case, Freeze opens and manages the underlying Berkeley DB environment; in the other case, you provide a DbEnv object that represents a Berkeley DB environment you opened yourself. (Usually, it is easier to let Freeze take care of all interactions with Berkeley DB.)
The envName parameter represents the name of the underlying Berkeley DB environment, and is also used as the default Berkeley DB home directory. See Freeze.Evictor.env-name.DbHome in the Ice properties reference.
The filename parameter represents the name of the Berkeley DB database file associated with this evictor. The persistent state of all your servants is stored in this file.
The facetTypes parameter allows you to specify a single class type (Ice type ID string) for each facet in your new evictor (see below). Most applications use only the default facet, represented by an empty string. This parameter is optional in C++; in Java, pass null if you do not want to specify such a facet-to-type mapping.
The initializer parameter represents the servant initializer. It is an optional parameter in C++; in Java, pass null if you do not need a servant initializer.
The indexes parameter is a vector or array of evictor indexes. It is an optional parameter in C++; in Java, pass null if your evictor does not define any index.
Finally, the createDb parameter tells Freeze what to do when the corresponding Berkeley DB database does not exist. When true, Freeze creates a new database; when false, Freeze raises a Freeze::DatabaseException.

Homogeneous Databases

When a transactional evictor processes an incoming request without an associated transaction, it first needs to find out whether the corresponding operation is read-only or read-write (as specified by the "freeze:read" and "freeze:write" operation metadata). This is straightforward if the evictor knows the target’s type; in this case, it simply instantiates and keeps a "dummy" servant to look up the attributes of each operation.
However, if the target type can vary, the evictor needs to look up and sometimes load a read-only servant to find this information. For read-write requests, it will then again load the servant from disk (within a new transaction). Once the transaction commits, the read-only servant—sometimes freshly loaded from disk—is discarded.
When you create a transactional evictor with createTransactionalEvictor, you can pass a facet name to type ID map to associate a single servant type with each facet and speed up the operation attribute lookups.

Synchronization

With a transactional evictor, there is no need to perform any synchronization on the servants managed by the evictor:
• For read-only operations, the application must not modify any data member, and hence there is no need to synchronize. (Many threads can safely read concurrently the same data members.)
• For read-write operations, each operation dispatch gets its own private servant or servants (see transaction propagation below).
Not having to worry about synchronization can dramatically simplify your application code.

Transaction Propagation

Without a distributed transaction service, it is not possible to invoke several remote operations within the same transaction.
Nevertheless, Freeze supports transaction propagation for collocated calls: when a request is dispatched within a transaction, the transaction is associated with the dispatch thread and will propagate to any other servant reached through a collocated call. If the target of a collocated call is managed by a transactional evictor associated with the same database environment, Freeze reuses the propagated transaction to load the servant and dispatch the request. This allows you to group updates to several servants within a single transaction.
You can also control how a transactional evictor handles an incoming transaction through optional metadata added after "freeze:write" and "freeze:read". There are six valid directives:
• "freeze:read:never"
Verify that no transaction is propagated to this operation. If a transaction is present, the transactional evictor raises a Freeze::DatabaseException.
• "freeze:read:supports"
Accept requests with or without transaction, and re-use the transaction if present. "supports" is the default for "freeze:read" operations.
• "freeze:read:mandatory" and "freeze:write:mandatory"
Verify that a transaction is propagated to this operation. If there is no transaction, the transactional evictor raises a Freeze::DatabaseException.
• "freeze:read:required" and "freeze:write:required"
Accept requests with or without a transaction, and re-use the transaction if present. If no transaction is propagated, the transactional evictor creates a brand new transaction before dispatching the request. "required" is the default for "freeze:write" operations.

Commit or Rollback on User Exception

When a transactional evictor processes an incoming read-write request, it starts a new database transaction, loads a servant within the transaction, dispatches the request, and then either commits or rolls back the transaction depending on the outcome of this dispatch. If the dispatch does not raise an exception, the transaction is committed just before the response is sent back to the client. If the dispatch raises a system exception, the transaction is rolled back. If the dispatch raises a user exception, by default, the transaction is committed. However, you can configure Freeze to rollback on user-exceptions by setting Freeze.Evictor.env-name.fileName.RollbackOnUserException to a value other than 0.

Deadlocks and Automatic Retries

When reading and writing in separate concurrent transactions, deadlocks are likely to occur. For example, one transaction may lock pages in a particular order while another transaction locks the same pages in a different order; the outcome is a deadlock. Berkeley DB automatically detects such deadlocks, and "kills" one of the transactions.
With a Freeze transactional evictor, the application does not need catch any deadlock exceptions or retry when deadlock occurs because the transactional evictor automatically retries its transactions whenever it encounters a deadlock exception.
However, this can affect how you implement your operations: for any operation called within a transaction (mainly read-write operations), you must anticipate the possibility of several calls for the same request, all in the same dispatch thread.

Asynchronous Method Dispatch

When a transactional evictor dispatches a read-write operation implemented using AMD, it starts a transaction before dispatching the request, and commits or rolls back the transaction when the dispatch is done. The transactional evictor does not wait for the application to provide a response through the AMD callback to terminate the transaction.
If a deadlock occurs during dispatch, the transactional evictor retries automatically with a new AMD callback. The previous AMD callback is no longer capable of sending back the response back to the client (ice_response and ice_exception on this callback do nothing).

Transactions and Freeze Maps

A transactional evictor uses the same transaction objects as Freeze maps, which allows you to update a Freeze map within a transaction managed by a transactional evictor.
You can get the current transaction created by a transactional evictor by calling getCurrentTransaction. Then, you would typically retrieve the associated Freeze connection (with getConnection) and construct a Freeze map with this connection:
local interface TransactionalEvictor extends Evictor {
    Transaction getCurrentTransaction();
    void setCurrentTransaction(Transaction tx);
};
A transactional evictor also gives you the ability to associate your own transaction with the current thread, using setCurrentTransaction. This is useful if you want to perform many updates within a single transaction, for example to add or remove many servants in the evictor. (A less convenient alternative is to implement all such updates within a read-write operation on some object.)

36.5.11 Application Design Considerations

The Freeze evictor creates a snapshot of a servant’s state for persistent storage by marshaling the servant, just as if the servant 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 {
    ["freeze:write"] void withdraw(int amount);
    ["freeze:write"] void deposit(int amount);

    int balance;
};

interface Bank {
    Account* createAccount();
};
In this application, we would use a 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 {
    ["freeze:write"] void withdraw(int amount);
    ["freeze:write"] 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