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

39.3 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 32.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 applica­tion 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 appli­cation 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.

39.3.1 Specifying Persistent State

The persistent state of servants managed by a Freeze evictor must be described in Slice. Specifically, every servant 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 persis­tent 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.)

39.3.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.

39.3.3 Background Save and Transactional Evictors

Freeze provides two types of evictors, a background save evictor and a transac­tional 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 back­ground thread. You can configure how often servants are saved; for example you could decide to 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 force an immediate save to preserve 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 inconsistencies 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 an adverse impact on performance.

39.3.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 algo­rithm: 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 39.2. 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 39.2. An evictor queue after restoring servant 1 and evicting servant 6.

39.3.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 modi­fies 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 meta­data 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 direc­tive, 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.

39.3.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 are more elements, and next returns the next identity:
local interface EvictorIterator {
    bool hasNext();
    Ice::Identity next();
};
You create 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). Inter­nally, this iterator will retrieve identities in batches of batchSize objects; we recommend to use a fairly large batch size to get good performance.

39.3.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 dictionary 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 defi­nition of the createBackgroundSaveEvictor and createTransactionalEvictor functions 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.

39.3.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 guaran­teed 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 39.4 on page 1517 demonstrates the use of a servant initializer.

39.3.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 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: 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 initial­izer.
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 an index.
Finally, the createDb parameter tells Freeze what to do when the corre­sponding 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 servants 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 neces­sary 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 use the same mechanism to synchronize all operations that access the servant’s Slice-defined data members.

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.)

39.3.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 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: 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 initial­izer.
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 an index.
Finally, the createDb parameter tells Freeze what to do when the corre­sponding 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 some­times load a read-only servant to find this information. For read-write requests, it will then load the servant from disk a second time (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 the same data members concurrently.)
• 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 appli­cation 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 propa­gate to any other servant reached through a collocated call. If the target of a collo­cated 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 transac­tion 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 a 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 transac­tion, 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 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 transac­tion 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 to catch any deadlock exceptions or retry when deadlock occurs because the transactional evictor automatically retries its transactions whenever it encounters a deadlock situation.
However, this can affect how you implement your operations: for any opera­tion called within a transaction (mainly read-write operations), you must antici­pate 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. Two threads are involved here: the dispatch thread and the callback thread. The dispatch thread is a thread from an Ice thread pool tasked with dispatching a request, and the callback thread is the thread that invokes the AMD callback to send the response to the client. These threads may be one and the same if the servant invokes the AMD callback from the dispatch thread.
It is important to understand the threading semantics of an AMD request with respect to the transaction:
• If a successful AMD response is sent from the dispatch thread, the transaction is committed after the response is sent. If a deadlock occurs during this commit, the request is not retried and the client receives no indication of the failure.
• If a successful AMD response is sent from another thread, the evictor commits its transaction when the dispatch thread completes, regardless of whether the servant has sent the AMD response. The callback thread waits until the trans­action has been committed by the dispatch thread before sending the response.
• If a commit results in a deadlock and the AMD response has not yet been sent, the evictor cancels the original AMD callback and automatically retries the request again with a new AMD callback. Invocations on the original AMD callback are ignored (ice_response and ice_exception on this callback do nothing).
• Otherwise, if the servant sends an exception via the AMD callback, the response is sent directly to the client.

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 transac­tional 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 using 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 imple­ment all such updates within a read-write operation on some object.)

39.3.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 data­base 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 applica­tion 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 Slice module.)

Table of Contents Previous Next
Logo