Using a separate servant for each Ice object in this fashion is common to many server implementations: the technique is simple to implement and provides a natural mapping from Ice objects to servants. Typically, on start‑up, the server instantiates a separate servant for each Ice object, activates each servant, and then calls
activate on the object adapter to start the flow of requests.
For many servers, neither criterion presents a problem: provided that the number of servants is small enough and that the servants can be initialized quickly, this is a perfectly acceptable design. However, the design does not scale well: the memory requirements of the server grow linearly with the number of Ice objects so, if the number of objects gets too large (or if each servant stores too much state), the server runs out of memory.
Ice offers two APIs that help you scale servers to larger numbers of objects: servant locators and
default servants. A default servant is essentially a simplified version of a servant locator that satisfies the majority of use cases, whereas a servant locator provides more flexibility for those applications that require it. Refer to
Section 32.8 for more information on default servants.
In a nutshell, a servant locator is a local object that you implement and attach to an object adapter. Once an adapter has a servant locator, it consults its ASM to locate a servant for an incoming request as usual. If a servant for the request can be found in the ASM, the request is dispatched to that servant. However, if the ASM does not have an entry for the object identity of the request, the object adapter calls back into the servant locator to ask it to provide a servant for the request. The servant locator either
This simple mechanism allows us to scale servers to provide access to an unlimited number of Ice objects: instead of instantiating a separate servant for each and every Ice object in existence, the server can instantiate servants for only a subset of Ice objects, namely those that are actually used by clients.
Servant locators are most commonly used by servers that provide access to databases: typically, the number of entries in the database is far larger than what the server can hold in memory. Servant locators allow the server to only instantiate servants for those Ice objects that are actually used by clients.
Another common use for servant locators is in servers that are used for process control or network management: in that case, there is no database but, instead, there is a potentially very large number of devices or network elements that must be controlled via the server. Otherwise, this scenario is the same as for large databases: the number of Ice objects exceeds the number of servants that the server can hold in memory and, therefore, requires an approach that allows the number of instantiated servants to be less than the number of Ice objects.
module Ice {
local interface ServantLocator {
["UserException"]
Object locate( Current curr,
out LocalObject cookie);
["UserException"]
void finished( Current curr,
Object servant,
LocalObject cookie);
void deactivate(string category);
};
};
Note that ServantLocator is a local interface. To create an actual implementation of a servant locator, you must define a class that is derived from
Ice::ServantLocator and provide implementations of the
locate,
finished, and
deactivate operations. The Ice run time invokes the operations on your derived class as follows:
You can also throw user exceptions from locate. If the user exception is in the corresponding operation’s exception specification, that user exception is returned to the client. User exceptions thrown by
locate that are not listed in the exception specification of the corresponding operation are returned to the client as
UnknownUserException. Non-Ice exceptions are returned to the client as
UnknownException (see
page 119).
The cookie out-parameter to
locate allows you return a local object to the object adapter. The object adapter does not care about the contents of that object (and it is legal to return a null cookie). Instead, the Ice run time passes whatever cookie you return from
locate back to you when it calls
finished. This allows you to pass an arbitrary amount of state from
locate to the corresponding call to
finished.
If a call to locate has returned a servant to the Ice run time, the Ice run time dispatches the incoming request to the servant. Once the request is complete (that is, the operation being invoked has completed), the Ice run time calls
finished, passing the servant whose operation has completed, the
Current object for the request, and the cookie that was initially created by
locate. This means that every call to
locate is balanced by a corresponding call to
finished (provided that
locate actually returned a servant).
If you throw an exception from finished, the Ice run time propagates the thrown exception back to the client. As for
locate, you can throw user exceptions from
finished. If a user exception is in the corresponding operation’s exception specification, that user exception is returned to the client. User exceptions that are not in the corresponding operation’s exception specification are returned to the client as
UnknownUserException.
finished can also throw run-time exceptions. However, only
ObjectNotExistException,
OperationNotExistException, and
FacetNotExistException are propagated without change to the client; other run-time exceptions are returned to the client as
UnknownLocalException.
The deactivate operation allows a servant locator to clean up once it is no longer needed. (For example, the locator might close a database connection.) The Ice run time passes the category of the servant locator being deactivated to the
deactivate operation.
The run time calls deactivate when the object adapter to which the servant locator is attached is destroyed. More precisely,
deactivate is called when you call
destroy on the object adapter, or when you call
destroy on the communicator (which implicitly calls
destroy on the object adapter).
Once the run time has called deactivate, it is guaranteed that no further calls to
locate or
finished can happen, that is,
deactivate is called exactly once, after all operations dispatched via this servant locator have completed.
This also explains why deactivate is not called as part of
ObjectAdapter::deactivate:
ObjectAdapter::deactivate initiates deactivation and returns immediately, so it cannot call
ServantLocator::deactivate directly, because there might still be outstanding requests dispatched via this servant locator that have to complete first—in turn, this would mean that either
ObjectAdapter::deactivate could block (which it must not do) or that a call to
ServantLocator::deactivate could be followed by one or more calls to
finished (which must not happen either).
It is important to realize that the Ice run time does not “remember” the servant that is returned by a particular call to
locate. Instead, the Ice run time simply dispatches an incoming request to the servant returned by
locate and, once the request is complete, calls
finished. In particular, if two requests for the same servant arrive more or less simultaneously, the Ice run time calls
locate and
finished once for each request. In other words,
locate establishes the association between an object identity and a servant; that association is valid only for a single request and is never used by the Ice run time to dispatch a different request.
The Ice run time guarantees that every operation invocation that involves a servant locator is bracketed by calls to
locate and
finished, that is, every call to
locate is balanced by a corresponding call to
finished (assuming that the call to
locate actually returned a servant, of course).
In addition, the Ice run time guarantees that locate, the operation, and
finished are called by the same thread. This guarantee is important because it allows you to use
locate and
finished to implement thread-specific pre- and post-processing around operation invocations. (For example, you can start a transaction in
locate and commit or roll back that transaction in
finished, or you can acquire a lock in
locate and release the lock in
finished.
2)
Note that, if you are using asynchronous method dispatch, the thread that starts a call is not necessarily the thread that finishes it. In that case,
finished is called by whatever thread executes the operation implementation, which may be a different thread than the one that called
locate.
The Ice run time also guarantees that deactivate is called when you deactivate the object adapter to which the servant locator is attached. The
deactivate call is made only once all operations that involved the servant locator are finished, that is,
deactivate is guaranteed not to run concurrently with
locate or
finished, and is guaranteed to be the
last call made to a servant locator.
These semantics allow you to extract the maximum amount of parallelism from your application code (because the Ice run time does not serialize invocations when serialization may not be necessary). Of course, this means that you must protect access to shared data from
locate and
finished with mutual exclusion primitives as necessary.
An object adapter does not automatically know when you create a servant locator. Instead, you must explicitly register servant locators with the object adapter:
module Ice {
local interface ObjectAdapter {
// ...
void addServantLocator(ServantLocator locator,
string category);
ServantLocator removeServantLocator(string category);
ServantLocator findServantLocator(string category);
// ...
};
};
As you can see, the object adapter allows you to add, remove, and find servant locators. Note that, when you register a servant locator, you must provide an argument for the
category parameter. The value of the
category parameter controls which object identities the servant locator is responsible for: only object identities with a matching
category member (see
page 939) trigger a corresponding call to
locate. An incoming request for which no explicit entry exists in the ASM and with a category for which no servant locator is registered returns an
ObjectNotExistException to the client.
addServantLocator has the following semantics:
•
You can register different servant locators for different categories, or you can register the same single servant locator multiple times (each time for a different category). In the former case, the category is implicit in the servant locator instance that is called by the Ice run time; in the latter case, the implementation of
locate can find out which category the incoming request is for by examining the object identity member of the
Current object that is passed to
locate.
removeServantLocator removes and returns the servant locator for a specific category (including the empty category) with the following semantics:
• A call to removeServantLocator returns immediately without waiting for the completion of any pending requests on that servant locator; such requests still complete normally by calling
finished on the servant locator.
findServantLocator allows you to retrieve the servant locator for a specific category (including the empty category). If no match is found, the operation returns null.
Every incoming request implicitly identifies a specific object adapter for the request (because the request arrives at a specific transport endpoint and, therefore, identifies a particular object adapter). The incoming request carries an object identity that must be mapped to a servant. To locate a servant, the Ice run time goes through the following steps, in the order shown:
4.
If the category of the incoming object identity is non-empty and no servant could be found in the preceding steps, look for a servant locator that is registered for that category. If such a servant locator is registered, call
locate on the servant locator and, if
locate returns a servant, dispatch the request to that servant, followed by a call to
finished; otherwise, if the call to
locate returns null, raise
ObjectNotExistException or
FacetNotExistException in the client.
6.
Raise ObjectNotExistException or
FacetNotExistException in the client. (
ObjectNotExistException is raised if the ASM does not contain a servant with the given identity at all,
FacetNotExistException is raised if the ASM contains a servant with a matching identity, but a non-matching facet.)
It is important to keep these call dispatch semantics in mind because they enable a number of powerful implementation techniques. Each technique allows you to streamline your server implementation and to precisely control the trade-off between performance, memory consumption, and scalability. To illustrate the possibilities, we outline a number of the most common implementation techniques in the following section.
To illustrate the concepts outlined in the previous sections, let us examine a (very simple) implementation of a servant locator. Consider that we want to create an electronic phone book for the entire world’s telephone system (which, clearly, involves a very large number of entries, certainly too many to hold the entire phone book in memory). The actual phone book entries are kept in a large database. Also assume that we have a search operation that returns the details of a phone book entry. The Slice definitions for this application might look something like the following:
struct Details {
// Lots of details about the entry here...
};
interface PhoneEntry {
idempotent Details getDetails();
idempotent void updateDetails(Details d);
// ...
};
struct SearchCriteria {
// Fields to permit searching...
};
interface PhoneBook {
idempotent PhoneEntry* search(SearchCriteria c);
// ...
};
The details of the application do not really matter here; the important point to note is that each phone book entry is represented as an interface for which we need to create a servant eventually, but we cannot afford to keep servants for all entries permanently in memory.
Each entry in the phone database has a unique identifier. This identifier might be an internal database identifier, or a combination of field values, depending on exactly how the database is constructed. The important point is that we can use this database identifier to link the proxy for an Ice object to its persistent state: we simply use the database identifier as the object identity. This means that each proxy contains the primary access key that is required to locate the persistent state of each Ice object and, therefore, instantiate a servant for that Ice object.
class MyServantLocator : public virtual Ice::ServantLocator {
public:
virtual Ice::ObjectPtr locate(const Ice::Current& c,
Ice::LocalObjectPtr& cookie);
virtual void finished(const Ice::Current& c,
const Ice::ObjectPtr& servant,
const Ice::LocalObjectPtr& cookie);
virtual void deactivate(const std::string& category);
};
Note that MyServantLocator inherits from
Ice::ServantLocator and implements the pure virtual functions that are generated by the
slice2cpp compiler for the
Ice::ServantLocator interface. Of course, as always, you can add additional member functions, such as a constructor and destructor, and you can add private data members as necessary to support your implementation.
In C++, you can implement the locate member function along the following lines:
Ice::ObjectPtr
MyServantLocator::locate(const Ice::Current& c,
Ice::LocalObjectPtr& cookie)
{
// Get the object identity. (We use the name member
// as the database key.)
//
std::string name = c.id.name;
// Use the identity to retrieve the state from the database.
//
ServantDetails d;
try {
d = DB_lookup(name);
} catch (const DB_error&)
return 0;
}
// We have the state, instantiate a servant and return it.
//
return new PhoneEntryI(d);
}
For the time being, the implementations of finished and
deactivate are empty and do nothing.
The DB_lookup call in the preceding example is assumed to access the database. If the lookup fails (presumably, because no matching record could be found),
DB_lookup throws a
DB_error exception. The code catches that exception and returns zero instead; this raises
ObjectNotExistException in the client to indicate that the client used a proxy to a no‑longer existent Ice object.
Note that locate instantiates the servant on the heap and returns it to the Ice run time. This raises the question of when the servant will be destroyed. The answer is that the Ice run time holds onto the servant for as long as necessary, that is, long enough to invoke the operation on the returned servant and to call
finished once the operation has completed. Thereafter, the servant is no longer needed and the Ice run time destroys the smart pointer that was returned by
locate. In turn, because no other smart pointers exist for the same servant, this causes the destructor of the
PhoneEntryI instance to be called, and the servant to be destroyed.
The upshot of this design is that, for every incoming request, we instantiate a servant and allow the Ice run time to destroy the servant once the request is complete. Depending on your application, this may be exactly what is needed, or it may be prohibitively expensive—we will explore designs that avoid creation and destruction of a servant for every request shortly.
public class MyServantLocator implements Ice.ServantLocator {
public Ice.Object locate(Ice.Current c,
Ice.LocalObjectHolder cookie)
{
// Get the object identity. (We use the name member
// as the database key.
String name = c.id.name;
// Use the identity to retrieve the state
// from the database.
//
ServantDetails d;
try {
d = DB.lookup(name);
} catch (DB.error e) {
return null;
}
// We have the state, instantiate a servant and return it.
//
return new PhoneEntryI(d);
}
public void finished(Ice.Current c,
Ice.Object servant,
java.lang.Object cookie)
{
}
public void deactivate(String category)
{
}
}
All implementations of locate follow the pattern illustrated by the previous pseudo-code:
1. Use the id member of the passed
Current object to obtain the object identity. Typically, only the
name member of the identity is used to retrieve servant state. The
category member is normally used to select a servant locator. (We will explore use of the category member shortly.)
Note that, in this example, we have installed the servant locator for the empty category. This means that
locate on our servant locator will be called for invocations to any of our Ice objects (because the empty category acts as the default). In effect, with this design, we are not using the
category member of the object identity. This is fine, as long as all our servants all have the same, single interface. However, if we need to support several different interfaces in the same server, this simple strategy is no longer sufficient.
32.7.6 Using the category Member of the Object Identity
The simple example in the preceding section always instantiates a servant of type PhoneEntryI. In other words, the servant locator implicitly is aware of the type of servant the incoming request is for. This is not a very realistic assumption for most servers because, usually, a server provides access to objects with several different interfaces. This poses a problem for our
locate implementation: somehow, we need to decide inside
locate what type of servant to instantiate. You have several options for solving this problem:
This technique uses part of the object identity to denote what type of object to instantiate. For example, in our file system application, we have directory and file objects. By convention, we could prepend a ‘
d’ to the identity of every directory and prepend an ‘
f’ to the identity of every file. The servant locator then can use the first letter of the identity to decide what type of servant to instantiate:
Ice::ObjectPtr
MyServantLocator::locate(const Ice::Current& c,
Ice::LocalObjectPtr& cookie)
{
// Get the object identity. (We use the name member
// as the database key.)
//
std::string name = c.id.name;
std::string realId = c.id.name.substr(1);
try {
if (name[0] == 'd') {
// The request is for a directory.
//
DirectoryDetails d = DB_lookup(realId);
return new DirectoryI(d);
} else {
// The request is for a file.
//
FileDetails d = DB_lookup(realId);
return new FileI(d);
}
} catch (DatabaseNotFoundException&) {
return 0;
}
}
•
Use the category member of the object identity to denote the type of servant to instantiate.
This is the recommended approach: for every interface type, we assign a separate identifier as the value of the
category member of the object identity. (For example, we can use ‘
d’ for directories and ‘
f’ for files.) Instead of registering a single servant locator, we create two different servant locator implementations, one for directories and one for files, and then register each locator for the appropriate category:
class DirectoryLocator : public virtual Ice::ServantLocator {
public:
virtual Ice::ObjectPtr locate(const Ice::Current& c,
Ice::LocalObjectPtr& cookie)
{
// Code to locate and instantiate a directory here...
}
virtual void finished(const Ice::Current& c,
const Ice::ObjectPtr& servant,
const Ice::LocalObjectPtr& cookie)
{
}
virtual void deactivate(const std::string& category)
{
}
};
class FileLocator : public virtual Ice::ServantLocator {
public:
virtual Ice::ObjectPtr locate(const Ice::Current& c,
Ice::LocalObjectPtr& cookie)
{
// Code to locate and instantiate a file here...
}
virtual void finished(const Ice::Current& c,
const Ice::ObjectPtr& servant,
const Ice::LocalObjectPtr& cookie)
{
}
virtual void deactivate(const std::string& category)
{
}
};
// ...
// Register two locators, one for directories and
// one for files.
//
adapter‑>addServantLocator(new DirectoryLocator(), "d");
adapter‑>addServantLocator(new FileLocator(), "f");
Yet another option is to use the category member of the object identity, but to use a single default servant locator (that is, a locator for the empty category). With this approach, all invocations go to the single default servant locator, and you can switch on the
category value inside the implementation of the
locate operation to determine which type of servant to instantiate. However, this approach is harder to maintain than the previous one; the
category member of the Ice object identity exists specifically to support servant locators, so you might as well use it as intended.
Occasionally, it can be useful to be able to pass information between locate and
finished. For example, the implementation of
locate could choose among a number of alternative database backends, depending on load or availability and, to properly finalize state, the implementation of
finish might need to know which database was used by
locate. To support such scenarios, you can create a cookie in your
locate implementation; the Ice run time passes the value of the cookie to
finished after the operation invocation has completed. The cookie must derive from
Ice::LocalObject and can contain whatever state and member functions are useful to your implementation:
class MyCookie : public virtual Ice::LocalObject {
public:
// Whatever is useful here...
};
typedef IceUtil::Handle<MyCookie> MyCookiePtr;
class MyServantLocator : public virtual Ice::ServantLocator {
public:
virtual Ice::ObjectPtr locate(const Ice::Current& c,
Ice::LocalObjectPtr& cookie)
{
// Code as before...
// Allocate and initialize a cookie.
//
cookie = new MyCookie(...);
return new PhoneEntryI;
}
virtual void finished(const Ice::Current& c,
const Ice::ObjectPtr& servant,
const Ice::LocalObjectPtr& cookie)
{
// Down‑cast cookie to actual type.
//
MyCookiePtr mc = MyCookiePtr::dynamicCast(cookie);
// Use information in cookie to clean up...
//
// ...
}
virtual void deactivate(const std::string& category);
};