Table of Contents Previous Next
Logo
The Ice Run Time in Detail : 32.7 Servant Locators
Copyright © 2003-2009 ZeroC, Inc.

32.7 Servant Locators

Using an adapter’s ASM to map Ice objects to servants has a number of design implications:
• Each Ice object is represented by a different servant.1
• All servants for all Ice objects are permanently in memory.
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.
There is nothing wrong with the above design, provided that two criteria are met:
1. The server has sufficient memory available to keep a separate servant instantiated for each Ice object at all times.
2. The time required to initialize all the servants on start‑up is acceptable.
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 uses servant locators to allow you to scale servers to larger numbers of objects.

32.7.1 Overview

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
• instantiates a servant and passes it to the Ice run time, in which case the request is dispatched to that newly instantiated servant, or
• the servant locator indicates failure to locate a servant to the Ice run time, in which case the client receives an ObjectNotExistException.
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.

32.7.2 Servant Locator Interface

A servant locator has the following interface:
module Ice {
    local interface ServantLocator {
        Object locate(    Current     curr,
                      out LocalObject cookie);

        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:
• locate
Whenever a request arrives for which no entry exists in the ASM, the Ice run time calls locate. The implementation of locate (which you provide as part of the derived class) is supposed to return a servant that can process the incoming request. Your implementation of locate can behave in three possible ways:
1. Instantiate and return a servant for the current request. In this case, the Ice run time dispatches the request to the newly instantiated servant.
2. Return null. In this case, the Ice run time raises an ObjectNotExistException in the client.
3. Throw a run-time exception. In this case, the Ice run time propagates the thrown exception back to the client. Keep in mind that all run-time exceptions, apart from ObjectNotExistException, OperationNotExistException, and FacetNotExistException, are presented as UnknownLocalException to the client.
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 124).
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.
• 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.
Non-Ice exceptions thrown from finished are returned to the client as UnknownException (see page 124).
If both the operation implementation and finished throw a user exception, the exception thrown by finished overrides the exception thrown by the operation.
• deactivate
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.

32.7.3 Threading Guarantees for Servant Locators

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 (see Chapter 33), 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.
Beyond this, the Ice run time provides no threading guarantees for servant locators. In particular:
• It is possible for invocations of locate to proceed concurrently (for the same object identity or for different object identities).
• It is possible for invocations of finished to proceed concurrently (for the same object identity or for different object identities).
• It is possible for invocations of locate and finished to proceed concurrently (for the same object identity or for different object identities).
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.

32.7.4 Servant Locator Registration

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 findServantLocator(string category);

        // ...
    };
};
As you can see, the object adapter allows you to add 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 859) 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 exactly one servant locator for a specific category. Attempts to call addServantLocator for the same category more than once raise an AlreadyRegisteredException.
• 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.
• It is legal to register a servant locator for the empty category. Such a servant locator is known as a default servant locator: if a request comes in for which no entry exists in the ASM, and whose category does not match the category of any other registered servant locator, the Ice run time calls locate on the default servant locator.
Note that, once registered, you cannot change or remove the servant locator for a category. The life cycle of the servant locators for an object adapter ends with the life cycle of the adapter: when the object adapter is deactivated, so are its servant locators.
The findServantLocator operation allows you to retrieve the servant locator for a specific category (including the empty category). If no servant locator is registered for the specified category, findServantLocator returns null.

Call Dispatch Semantics

The preceding rules may seem complicated, so here is a summary of the actions taken by the Ice run time to locate a servant for an incoming request.
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:
1. Look for the identity in the ASM. If the ASM contains an entry, dispatch the request to the corresponding servant.
2. If the category of the incoming object identity is non-empty, 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. (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.)
3. If the category of the incoming object identity is empty, or no servant locator could be found for the category in step 2, look for a default servant locator (that is, a servant locator that is registered for the empty category). If a default servant locator is registered, dispatch the request as for step 2.
4. Raise ObjectNotExistException or FacetNotExistException in the client.
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.

32.7.5 Implementing a Simple Servant Locator

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.
What follows is an outline implementation in C++. The class definition of our servant locator looks as follows:
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.
In Java, the implementation of our servant locator looks very similar:3
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.)
2. Retrieve the state of the Ice object from secondary storage (or the network) using the object identity as a key.
If the lookup succeeds, you have retrieved the state of the Ice object.
If the lookup fails, return null. In that case, the Ice object for the client’s request truly does not exist, presumably, because that Ice object was deleted earlier, but the client still has a proxy to the now-deleted object.
3. Instantiate a servant and use the state retrieved from the database to initialize the servant. (In this example, we pass the retrieved state to the servant constructor.)
4. Return the servant.
Of course, before we can use our servant locator, we must inform the adapter of its existence prior to activating the adapter, for example (in Java or C#):
MyServantLocator sl = new MyServantLocator();
adapter.addServantLocator(sl, "");
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:
• Use a separate object adapter for each interface type and use a separate servant locator for each object adapter.
This technique works fine, but has the down-side that each object adapter requires a separate transport endpoint, which is wasteful.
• Mangle a type identifier into the name component of the object identity.
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;
    }
}
While this works, it is awkward: not only do we need to parse the name member to work out what type of object to instantiate, but we also need to modify the implementation of locate whenever we add a new type to our application.
• 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.

32.7.7 Using Cookies

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 be derived 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)
    {
        // Downcast cookie to actual type.
        //
        MyCookiePtr mc = MyCookiePtr::dynamicCast(cookie);

        // Use information in cookie to clean up...
        //
        // ...
    }

    virtual void deactivate(const std::string& category);
};

1
It is possible to register a single servant with multiple identities. However, there is little point in doing so because a default servant (see Section 32.9.2) achieves the same thing.

2
Both transactions and locks usually are thread-specific, that is, only the thread that started a transaction can commit it or roll it back, and only the thread that acquired a lock can release the lock.

3
The C# implementation is virtually identical to the Java implementation, so we do not show it here.

Table of Contents Previous Next
Logo