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, Objective-C and Swift. They are defined through a native API in each programming language.

On this page:

Dispatch Interceptor Position

Once the object adapter finds the servant identified as the target of a request, it logically calls dispatch on this servant, and dispatch selects the servant's method that corresponds to the operation name carried by the request and later unmarshals the request parameters for this operation.

In all programming languages except Swift, it is the servant itself that provides this dispatch method, and this method is called ice_dispatch to avoid conflicts with your own methods. In Swift, dispatch is provided by a separate request dispatcher object (typically a struct) that adopts protocol Disp and holds the actual servant. We will refer to the object that provides the dispatch method as the request dispatcher–a synonym for servants in all programming languages except Swift. 

A dispatch interceptor is simply a request dispatcher that you insert in front of another request dispatcher. This other request dispatcher can be the real request dispatcher that unmarshals the parameters carried by the request and executes the operation. It could also be another dispatch interceptor: this way, you can create a chain of dispatch interceptors, with the real request dispatcher at the end of this chain.

You register a dispatch interceptor with an object adapter like any other request dispatcher: 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 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. (Do not confuse dispatchers with request dispatchers).
  3. The Ice run time locates the target request dispatcher (this may call locate on a servant locator). In all programming languages except Swift, this request dispatcher is the servant.
  4. Assuming the target request dispatcher is a dispatch interceptor, it calls dispatch on the next request dispatcher (which could be another dispatch interceptor); eventually a dispatch interceptor calls dispatch on the real request dispatcher.
  5. The real servant eventually 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 request dispatcher (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

In programming languages other than Swift, 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 
public typealias Request = ... // no public API

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.

Swift

In Swift, Request should be considered opaque, and you not call any of its methods.

The native class DispatchInterceptor provides an abstract method or function dispatch that you need to implement. In Swift, the base protocol for dispatch interceptors is Disp.

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.

public protocol Disp {
    func dispatch(request: Request, current: Current) throws -> Promise<OutputStream>?
}


Using a Dispatch Interceptor

Your implementation of the dispatch function or method must dispatch the request to the actual request dispatcher. Here is a very simple example implementation of an interceptor that dispatches the request to the request dispatcher 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;
};
struct InterceptorI: Disp {
    private let target: Disp

    init(_ target: Disp) {
        self.target = target
    }

    func dispatch(request: Request, current: Current) throws -> Promise<OutputStream>? {
        try return target.dispatch(request: request, current: current)
    }
}

Note that our implementation of dispatch calls ice_dispatch on the target request dispatcher 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"));
let servant = ExampleI()
let interceptor = InterceptorI(ExampleDisp(servant))
adapter.add(servant: interceptor, id: 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 completed–meaning 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 or Promises (C#, Java and Swift)

In C#, Java and Swift, ice_dispatch (dispatch in Swift) returns a non-null Task<Ice.OutputStream> (C#), CompletionStage<OutputStream> (Java) or Promise<OutputStream> 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;
  
    ...
}
public protocol Disp {
    func dispatch(request: Request, current: Current) throws -> Promise<OutputStream>?
}

Your dispatch interceptor can be notified of the completion of an asynchronous dispatch by registering an action with this TaskCompletionStage or Promise (using ContinueWith in C#, whenComplete in Java and then, map or done in Swift).

Your dispatch interceptor can also forward several times the same request to the real request dispatcher and only return the Task, CompletionStage or Promise provided by the "good" dispatch back to Ice; the TaskCompletionStage or Promise 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