A dispatch interceptor is a server-side mechanism that allows you to intercept incoming client requests before they are given to a servant. The interceptor can examine the incoming request; in particular, it can see whether request dispatch is collocation-optimized and examine the
Current information for the request (which provides access to the operation name, object identity, and so on).
A dispatch interceptor can dispatch a request to a servant and check whether the dispatch was successful; if not, the interceptor can choose to retry the dispatch. This functionality is useful to automatically retry requests that have failed due to a recoverable error condition, such as a database deadlock exception. (Freeze uses dispatch interceptors for this purpose in its evictor implementation—see
Section 36.5.)
Dispatch interceptors are not defined in Slice, but are provided as an API that is specific to each programming language. The remainder of this section presents the interceptor API for C++; for Java and .NET, the API is analogous, so we do not show it here.
namespace Ice {
class DispatchInterceptor : public virtual Object {
public:
virtual DispatchStatus dispatch(Request&) = 0;
};
typedef IceInternal::Handle<DispatchInterceptor>
DispatchInterceptorPtr;
}
Note that a DispatchInterceptor is‑a Object, that is, you use a dispatch interceptor as a servant.
To create a dispatch interceptor, you must derive a class from DispatchInterceptor and provide an implementation of the pure virtual
dispatch function. The job of
dispatch is to pass the request to the servant and to return a dispatch status, defined as follows:
namespace Ice {
enum DispatchStatus {
DispatchOK, DispatchUserException, DispatchAsync
};
}
namespace Ice {
class Request {
public:
virtual bool isCollocated();
virtual const Current& getCurrent();
};
}
•
isCollocated returns true if the dispatch is directly into the target servant as a collocation-optimized dispatch (see
Section 28.21). If the dispatch is not collocation-optimized, the function returns false.
•
getCurrent provides access to the
Current object for the request (see
Section 28.6), which provides access to information about the request, such as the object identity of the target object, the object adapter used to dispatch the request, and the operation name.
Note that Request, for performance reasons, is
not thread-safe. This means that you must not concurrently dispatch from different threads using the same
Request object. (Concurrent dispatch for different requests does not cause any problems.)
To use a dispatch interceptor, you instantiate your derived class and register it as a servant with the Ice run time in the usual way, by adding the interceptor to the ASM, or returning the interceptor as a servant from a call to
locate on a servant locator.
Your implementation of the dispatch function must dispatch the request to the actual servant. Here is a very simple example implementation of an interceptor that dispatches the request to the servant passed to the interceptor’s constructor:
class InterceptorI : public Ice::DispatchInterceptor {
public:
InterceptorI(const Ice::ObjectPtr& servant)
: _servant(servant) {}
virtual Ice::DispatchStatus dispatch(Ice::Request& request) {
return _servant‑>ice_dispatch(request);
}
Ice::ObjectPtr _servant;
};
Note that our implementation of dispatch calls
ice_dispatch on the target servant to dispatch the request.
ice_dispatch does the work of actually invoking the operation.
Also note that dispatch returns whatever is returned by
ice_dispatch. You should always implement your interceptor in this way and not change this return value.
ExampleIPtr servant = new ExampleI;
Ice::DispatchInterceptorPtr interceptor =
new InterceptorI(servant);
adapter‑>add(interceptor,
communicator‑>stringToIdentity("ExampleServant"));
Note that, because dispatch interceptor is‑a servant, this means that the servant to which the interceptor dispatches need not be the actual servant. Instead, it could be another dispatch interceptor that ends up dispatching to the real servant. In other words, you can chain dispatch interceptors; each interceptor’s
dispatch function is called until, eventually, the last interceptor in the chain dispatches to the actual servant.
A more interesting use of a dispatch interceptor is to retry a call if it fails due to a recoverable error condition. Here is an example that retries a request if it raises a local exception defined in Slice as follows:
local exception DeadlockException { /* ... */ };
Note that this is a local exception. Local exceptions that are thrown by the servant propagate to
dispatch and can be caught there. A database might throw such an exception if the database detects a locking conflict during an update. We can retry the request in response to this exception using the following
dispatch implementation:
virtual Ice::DispatchStatus dispatch(Ice::Request& request) {
while (true) {
try {
return _servant‑>ice_dispatch(request);
} catch (const DeadlockException&) {
// Happens occasionally
}
}
}
If an operation throws a user exception (as opposed to a local exception), the user exception cannot be caught by
dispatch as an exception but, instead, is reported by the return value of
ice_dispatch: a return value of
DispatchUserException indicates that the operation raised a user exception. You can retry a request in response to a user exception as follows:
virtual Ice::DispatchStatus dispatch(Ice::Request& request) {
Ice::DispatchStatus d;
do {
d = _servant‑>ice_dispatch(request);
} while (d == Ice::DispatchUserException);
return d;
}
This is fine as far as it goes, but not particularly useful because the preceding code retries if
any kind of user exception is thrown. However, typically, we want to retry a request only if a
specific user exception is thrown. The problem here is that the
dispatch function does not have direct access to the actual exception that was thrown—all it knows is that
some user exception was thrown, but not which one.
To retry a request for a specific user exception, you need to implement your servants such that they leave some "footprint" behind if they throw the exception of interest. This allows your request interceptor to test whether the user exception should trigger a retry. There are various techniques you can use to achieve this. For example, you can use thread-specific storage to test a retry flag that is set by the servant if it throws the exception or, if you use transactions, you can attach the retry flag to the transaction context. However, doing so is more complex; the intended use case is to permit retry of requests in response to local exceptions, so we suggest you retry requests only for local exceptions.
The most common use case for a dispatch interceptor is as a default servant. Rather than having an explicit interceptor for individual servants, you can return a dispatch interceptor as the servant from a call to
locate on a servant locator (see
28.8.2). You can then choose the "real" servant to which to dispatch the request inside
dispatch, prior to calling
ice_dispatch. This allows you to intercept and selectively retry requests based on their outcome, which cannot be done using a servant locator.