Dispatch Interceptors

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 and in particular its Current information.

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.

Dispatch Interceptors are currently available in C++11, C++98, C#, Java, Java Compat and Objective-C. They are defined through a native API in each programming language.

On this page:

Dispatch Interceptor Position

A dispatch interceptor is a servant that delegates requests to another servant. This other servant can be the real servant that unmarshals the parameters carried by the request and implements the target operation. It could also be another dispatch interceptor: this way, you can create a chain of dispatch interceptors, with the real servant at the end of this chain.

You register a dispatch interceptor with an object adapter like any other servant: it can be inserted into this object adapter's Active Servant Map, or registered as a default servant, or returned by a servant locator.

Dispatch interceptors are most often registered as default servants.

A dispatch follows these steps:

  1. The Ice run time reads the incoming request using a thread from its server thread pool (or object adapter thread pool if one is configured, or client thread pool for requests over bidir connections).
  2. If a dispatcher is configured, the Ice run time gives the request to the dispatcher to allow the dispatcher to execute this request in a different thread.
  3. The Ice run time locates the target servant (this may call locate on a servant locator).
  4. Assuming the target servant is a dispatch interceptor, it calls ice_dispatch on the next servant (which could be another dispatch interceptor); eventually a dispatch interceptor calls ice_dispatch on the real servant.
  5. The real servant unmarshals the input parameters, executes the operation and then:
    • marshals the return and out parameters,
    • throws an exception,
    • or does not complete synchronously (if it's implemented using AMD).

  6. The dispatch interceptor(s) now have the opportunity to execute code "on the way out", after completion of the dispatch by the real servant (with AMD, only the synchronous part of this dispatch has completed).
  7. For a synchronous dispatch through a servant locator, the Ice run time calls finished on the servant locator.
  8. For a synchronous dispatch, the Ice run time sends the reply to the client: the return and out parameters previously marshaled by the real servant, or an exception caught and marshaled by the Ice run time.

A dispatch interceptor can also loop through steps 4 and 6 and dispatch several times the same request before returning the result to Ice. For example:

C++11
bool
MyDispatchInterceptor::dispatch(Ice::Request& request)
{
    for(;;)
    {
        try
        {
            TransactionHolder tx(_db); 
            bool sync = findServant(request.getCurrent().id)->ice_dispatch(request);
            assert(sync); // this example works only for synchronous dispatch
            tx.commit();
            return sync;
        }
        catch(const DeadlockException&)
        {
            // Run this dispatch again, with the same request
            //
        }
    }
}

Dispatch Interceptor API

You create a dispatch interceptor by providing a class that derives from the DispatchInterceptor abstract class and implements the dispatch function or method. The job of dispatch is to pass the request to the servant and to return the value returned by the servant's ice_dispatch call.

The Ice run time provides basic information about the request to the dispatch function in the form of a Request object:

namespace Ice 
{
    class Request
    {
    public:
        virtual ~Request();
        virtual const Current& getCurrent() = 0;
    };
}
namespace Ice 
{
    class Request
    {
    public:
        virtual ~Request();
        virtual const Current& getCurrent() = 0;
    };
}
namespace Ice
{
    public interface Request
    {
        Current getCurrent();
    }
} 
package com.zeroc.Ice;

public interface Request
{
    Current getCurrent();
}
package Ice;

public interface Request
{
    Current getCurrent();
}
@protocol ICERequest <NSObject>
-(ICECurrent*) getCurrent;
@end 

getCurrent provides access to the Current object for the request, 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.). We also recommend that you do not change thread in your dispatch interceptor as this would defeat the dispatcher mechanism.

namespace Ice 
{
    class DispatchInterceptor : public virtual Object
    {
    public:
        virtual bool dispatch(Request&) = 0;
    };
}

The dispatch return value is a bool that indicates whether dispatch was executed synchronously (true) or asynchronously with AMD (false). You must return the value returned by the servant ice_dispatch function, or throw an exception.

namespace Ice 
{
    class DispatchInterceptor : public virtual Object
    {
    public:
         virtual bool dispatch(Request&) = 0;
    };
    typedef IceInternal::Handle<DispatchInterceptor> DispatchInterceptorPtr;
}

The dispatch return value is a bool that indicates whether dispatch was executed synchronously (true) or asynchronously with AMD (false). You must return the value returned by the servant ice_dispatch function, or throw an exception.

namespace Ice
{
    public abstract class DispatchInterceptor : Ice.ObjectImpl
    {
        public abstract System.Threading.Tasks.Task<Ice.OutputStream> dispatch(Request request);
    }
}

The dispatch return value is a Task instance if the request was dispatched asynchronously with AMD, or null if the request was dispatched synchronously. You must return the same value as returned by the servant ice_dispatch method, or a continuation created from the returned Task, or throw an exception. You can check for the completion of the Task and retry a dispatch if for example the task completed with an exception. It is fine to abandon a returned Task to retry a dispatch.

If the servant implementation uses the async keyword for the dispatch of a request, ice_dispatch always returns a Task object even if the dispatch completed synchronously with a response or exception.

package com.zeroc.Ice;

import java.util.concurrent.CompletionStage;

public abstract class DispatchInterceptor implements com.zeroc.Ice.Object
{
    public abstract CompletionStage<OutputStream> dispatch(Request request) throws UserException;
    ...
}

The dispatch return value is a CompletionStage<OutputStream> if the request was dispatched asynchronously with AMD, or null if the request was dispatched synchronously. You must return the value returned by the servant ice_dispatch method, or a dependent ContinuationStage, or throw an exception. You can check for the completion of the CompletionStage and retry a dispatch if for example the request completed with an exception. It is fine to abandon a returned CompletionStage to retry a dispatch.

package Ice;

public abstract class DispatchInterceptor extends ObjectImpl
{
    public abstract boolean dispatch(Request request) throws Ice.UserException;
}

The dispatch return value is a boolean that indicates whether dispatch was executed synchronously (true) or asynchronously with AMD (false). You must return the value returned by the servant ice_dispatch function or throw an exception.

@protocol ICEDispatchInterceptor <ICEObject>
-(void) dispatch:(id<ICERequest>)request;
@end

 The Objective-C mapping does not support asynchronous dispatch (AMD), therefore the return type of the dispatch method is void.

Using a Dispatch Interceptor

Your implementation of the dispatch function or method 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(std::shared_ptr<Ice::Object> servant) :
        _servant(std::move(servant)) 
    {
    }

    virtual bool dispatch(Ice::Request& request) override
    {
        return _servant->ice_dispatch(request);
    }

    std::shared_ptr<Ice::Object> _servant;
};
class InterceptorI : public Ice::DispatchInterceptor
{
public:
    InterceptorI(const Ice::ObjectPtr& servant) :
        _servant(servant) 
    {
    }

    virtual bool 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.

We can use this interceptor to intercept requests to a servant of any type as follows:

auto servant = std::make_shared<ExampleI>();
auto interceptor = std::make_shared<InterceptorI>(std::move(servant)); // give the servant to the interceptor
adapter->add(interceptor, Ice::stringToIdentity("ExampleServant"));
ExampleIPtr servant = new ExampleI;
Ice::DispatchInterceptorPtr interceptor = new InterceptorI(servant);
adapter->add(interceptor, Ice::stringToIdentity("ExampleServant"));

Dispatch Interceptor with Asynchronous Method Dispatch (AMD)

When the real servant dispatches a request asynchronously, the result (or exception) is typically not available "on the way out" in the dispatching thread. This creates two challenges:

  • The dispatch interceptor may want to be notified when the dispatch completes.
  • The dispatch interceptor may want to retry the initial dispatch and prevent earlier dispatch attempts from returning a response or exception to the client.

The solution depends on the language mapping:

Using Callbacks (C++11, C++98, Java Compat)

With C++11, ice_dispatch on servants accepts two optional function parameters:

C++11
namespace Ice
{
    class Object
    {
    public:
         virtual bool ice_dispatch(Ice::Request& request, 
                                   std::function<bool()> response = nullptr,
                                   std::function<bool(std::exception_ptr)> error = nullptr);
         ...
 
    };
} 

When a servant dispatches a request asynchronously, ice_dispatch returns false and then response or error is called when the dispatch completes. Both response and error (that you supply) return a bool parameter that indicates whether Ice should send the response or exception to the client (when the return value is true), or just ignore this result (when the return value is false). A null response or error function is equivalent to a function that returns true.

Occasionally, you may attempt to re-dispatch a request that has already completedmeaning Ice has already sent the response or exception from a previous attempt back to the client. In this case, your dispatch interceptor will receive a ResponseSentException: it should then rethrow this exception or return true (like for a synchronous dispatch).

With C++98 and Java Compat, ice_dispatch on servants accepts a comparable DispatchInterceptorAsyncCallback parameter:

namespace Ice
{
    class DispatchInterceptorAsyncCallback : public virtual IceUtil::Shared
    {
    public:

        virtual ~DispatchInterceptorAsyncCallback();

        virtual bool response() = 0;
        virtual bool exception(const std::exception&) = 0;
        virtual bool exception() = 0;
    };
    typedef IceUtil::Handle<DispatchInterceptorAsyncCallback> DispatchInterceptorAsyncCallbackPtr;
 
    class Object : public virtual IceUtil::Shared
    {
    public:
         virtual bool ice_dispatch(Ice::Request& request, const DispatchInterceptorAsyncCallbackPtr& = 0);
         ...
    };
} 
package Ice;
public interface DispatchInterceptorAsyncCallback
{
    boolean response();
    boolean exception(java.lang.Exception ex);
}
 
public interface Object
{
    boolean ice_dispatch(Request request, DispatchInterceptorAsyncCallback cb)
        throws Ice.UserException;
 
    boolean ice_dispatch(Request request)
        throws Ice.UserException;
  
    ...
}

Using Futures (C# and Java)

In C# and Java, ice_dispatch returns a non-null Task<Ice.OutputStream> (C#) or CompletionStage<OutputStream> (Java) for asynchronous dispatches:

namespace Ice
{
    public interface Object : ICloneable
    {
        Task<OutputStream> ice_dispatch(Request request);
        ....
    }
} 
package com.zeroc.Ice;
 
public interface Object
{
    CompletionStage<OutputStream> ice_dispatch(Request request)
        throws Ice.UserException;
  
    ...
}

Your dispatch interceptor can be notified of the completion of an asynchronous dispatch by registering an action with this Task or CompletionStage (using ContinueWith in C# or for example whenComplete in Java).

Your dispatch interceptor can also forward several times the same request to the real servant and only return the Task or CompletionStage provided by the "good" dispatch back to Ice; the Task or CompletionStage returned by the other dispatches are simply discarded.

See Also
  1. The Current Object
  2. The Active Servant Map
  3. Servant Locators
  4. Default Servants