Table of Contents Previous Next
Logo
Asynchronous Programming : 33.3 Using AMI
Copyright © 2003-2007 ZeroC, Inc.

33.3 Using AMI

In this section, we describe the Ice implementation of AMI and how to use it. We begin by discussing a way to (partially) simulate AMI using oneway invocations. This is not a technique that we recommend, but it is an informative exercise that highlights the benefits of AMI and illustrates how it works. Next, we explain the AMI mapping and illustrate its use with examples.

33.3.1 Simulating AMI using Oneways

As we discussed at the beginning of the chapter, synchronous invocations are not appropriate for certain types of applications. For example, an application with a graphical user interface typically must avoid blocking the window system’s event dispatch thread because blocking makes the application unresponsive to user commands. In this situation, making a synchronous remote invocation is asking for trouble.
The application could avoid this situation using oneway invocations (see Section 32.13), which by definition cannot return a value or have any out parameters. Since the Ice run time does not expect a reply, the invocation blocks only as long as it takes to marshal and copy the message into the local transport buffer. However, the use of oneway invocations may require unacceptable changes to the interface definitions. For example, a twoway invocation that returns results or raises user exceptions must be converted into at least two operations: one for the client to invoke with oneway semantics that contains only in parameters, and one (or more) for the server to invoke to notify the client of the results.
To illustrate these changes, suppose that we have the following Slice definition:
interface I {
  int op(string s, out long l);
};
In its current form, the operation op is not suitable for a oneway invocation because it has an out parameter and a non-void return type. In order to accommodate a oneway invocation of op, we can change the Slice definitions as shown below:
interface ICallback {
  void opResults(int result, long l);
};

interface I {
  void op(ICallback* cb, string s);
};
We made several modifications to the original definition:
• We added interface ICallback, containing an operation opResults whose arguments represent the results of the original twoway operation. The server invokes this operation to notify the client of the completion of the operation.
• We modified I::op to be compliant with oneway semantics: it now has a void return type, and takes only in parameters.
• We added a parameter to I::op that allows the client to supply a proxy for its callback object.
As you can see, we have made significant changes to our interface definitions to accommodate the implementation requirements of the client. One ramification of these changes is that the client must now also be a server, because it must create an instance of ICallback and register it with an object adapter in order to receive notifications of completed operations.
A more severe ramification, however, is the impact these changes have on the type system, and therefore on the server. Whether a client invokes an operation synchronously or asynchronously should be irrelevant to the server; this is an artifact of behavior that should have no impact on the type system. By changing the type system as shown above, we have tightly coupled the server to the client, and eliminated the ability for op to be invoked synchronously.
To make matters even worse, consider what would happen if op could raise user exceptions. In this case, ICallback would have to be expanded with additional operations that allow the server to notify the client of the occurrence of each exception. Since exceptions cannot be used as parameter or member types in Slice, this quickly becomes a difficult endeavor, and the results are likely to be equally difficult to use.
At this point, you will hopefully agree that this technique is flawed in many ways, so why do we bother describing it in such detail? The reason is that the Ice implementation of AMI uses a strategy similar to the one described above, with several important differences:
1. No changes to the type system are required in order to use AMI. The on-the-wire representation of the data is identical, therefore synchronous and asynchronous clients and servers can coexist in the same system, using the same operations.
2. The AMI solution accommodates exceptions in a reasonable way.
3. Using AMI does not require the client to also be a server.

33.3.2 Language Mappings

An operation for which the AMI metadata has been specified supports both synchronous and asynchronous invocation models. In addition to the proxy method for synchronous invocation, the code generator creates a proxy method for asynchronous invocation, plus a supporting callback class. The generated code uses a pattern similar to the Slice modifications we made to the example in Section 33.3.1: the out parameters and return value are removed, leaving only in parameters for the invocation; the application supplies a callback object that is invoked with the results of the operation. In this case, however, the callback object is a purely local entity that is invoked by the Ice run time in the client. The name of the callback class is constructed so that it cannot conflict with a user-defined Slice identifier.

C++ Mapping

The C++ mapping emits the following code for each AMI operation:
1. An abstract callback class used by the Ice run time to notify the application about the completion of an operation. The name of the class is formed using the pattern AMI_class_op. For example, an operation named foo defined in interface I results in a class named AMI_I_foo. The class is generated in the same scope as the interface or class containing the operation. Two methods are provided:
void ice_response(<params>);
Indicates that the operation completed successfully. The parameters represent the return value and out parameters of the operation. If the operation has a non-void return type, then the first parameter of the ice_response method supplies the return value of the operation. Any out parameters present in the operation follow the return value, in the order of declaration.
void ice_exception(const Ice::Exception &);
Indicates that a local or user exception was raised.
Neither ice_response nor ice_exception throw any exceptions to the caller.
2. An additional proxy method, having the mapped name of the operation with the suffix _async. This method has a void return type. The first parameter is a smart pointer to an instance of the callback class described above. The remaining parameters comprise the in parameters of the operation, in the order of declaration.
For example, suppose we have defined the following operation:
interface I {
  ["ami"] int foo(short s, out long l);
};
The callback class generated for operation foo is shown below:
namespace Demo {
    class AMI_I_foo : public ... {
    public:
        virtual void ice_response(Ice::Int, Ice::Long) = 0;
        virtual void ice_exception(const Ice::Exception&) = 0;
    };

    typedef IceUtil::Handle<AMI_I_foo> AMI_I_fooPtr;
}
The proxy method for asynchronous invocation of operation foo is generated as follows:
void foo_async(const AMI_I_fooPtr&, Ice::Short);

Java Mapping

The Java mapping emits the following code for each AMI operation:
1. An abstract callback class used by the Ice run time to notify the application about the completion of an operation. The name of the class is formed using the pattern AMI_class_op. For example, an operation named foo defined in interface I results in a class named AMI_I_foo. The class is generated in the same scope as the interface or class containing the operation. Three methods are provided:
public void ice_response(<params>);
Indicates that the operation completed successfully. The parameters represent the return value and out parameters of the operation. If the operation has a non-void return type, then the first parameter of the ice_response method supplies the return value of the operation. Any out parameters present in the operation follow the return value, in the order of declaration.
public void ice_exception(Ice.LocalException ex);
Indicates that a local exception was raised.
public void ice_exception(Ice.UserException ex);
Indicates that a user exception was raised.
Neither ice_response nor ice_exception throw any exceptions to the caller.
2. An additional proxy method, having the mapped name of the operation with the suffix _async. This method has a void return type. The first parameter is a reference to an instance of the callback class described above. The remaining parameters comprise the in parameters of the operation, in the order of declaration.
For example, suppose we have defined the following operation:
interface I {
  ["ami"] int foo(short s, out long l);
};
The callback class generated for operation foo is shown below:
public abstract class AMI_I_foo extends ... {
    public abstract void ice_response(int __ret, long l);
    public abstract void ice_exception(Ice.LocalException ex);
    public abstract void ice_exception(Ice.UserException ex);
}
The proxy method for asynchronous invocation of operation foo is generated as follows:
public void foo_async(AMI_I_foo __cb, short s);

C# Mapping

The C# mapping emits the following code for each AMI operation:
1. An abstract callback class used by the Ice run time to notify the application about the completion of an operation. The name of the class is formed using the pattern AMI_class_op. For example, an operation named foo defined in interface I results in a class named AMI_I_foo. The class is generated in the same scope as the interface or class containing the operation. Two methods are provided:
public abstract void ice_response(<params>);
Indicates that the operation completed successfully. The parameters represent the return value and out parameters of the operation. If the operation has a non-void return type, then the first parameter of the ice_response method supplies the return value of the operation. Any out parameters present in the operation follow the return value, in the order of declaration.
public abstract void ice_exception(Ice.Exception ex);
Indicates that an exception was raised.
Neither ice_response nor ice_exception throw any exceptions to the caller.
2. An additional proxy method, having the mapped name of the operation with the suffix _async. This method has a void return type. The first parameter is a reference to an instance of the callback class described above. The remaining parameters comprise the in parameters of the operation, in the order of declaration.
For example, suppose we have defined the following operation:
interface I {
  ["ami"] int foo(short s, out long l);
};
The callback class generated for operation foo is shown below:
namespace IceInternal
{
    public abstract class OutgoingAsync
    {
        public abstract void ice_exception(Ice.Exception ex);

        // ...
    }
}

namespace Demo
{
    public abstract class AMI_I_foo : IceInternal.OutgoingAsync
    {
        public abstract void ice_response(int __ret, long l);

        // Mappinginternal code here...
    }
}
A concrete implementation of the AMI_I_foo class must provide implementations for both the generated ice_response and the inherited ice_exception abstract methods.
The proxy method for asynchronous invocation of operation foo is generated as follows:
void foo_async(AMI_I_foo __cb, short s);
void foo_async(AMI_I_foo __cb, short s, Ice.Context __ctx);
As usual, the version of the operation without a context parameter forwards an empty context to the version with a context parameter.

Visual Basic Mapping

The Visual Basic mapping emits the following code for each AMI operation:
1. An abstract callback class used by the Ice run time to notify the application about the completion of an operation. The name of the class is formed using the pattern AMI_class_op. For example, an operation named foo defined in interface I results in a class named AMI_I_foo. The class is generated in the same scope as the interface or class containing the operation. Two methods are provided:
Public MustOverride Sub ice_response(<params>)
Indicates that the operation completed successfully. The parameters represent the return value and out parameters of the operation. If the operation has a non-void return type, then the first parameter of the ice_response method supplies the return value of the operation. Any out parameters present in the operation follow the return value, in the order of declaration.
Public MustOverride Sub ice_exception(ByVal ex As Ice.Exception)
Indicates that an exception was raised.
Neither ice_response nor ice_exception throw any exceptions to the caller.
2. An additional proxy method, having the mapped name of the operation with the suffix _async. This method has a void return type. The first parameter is a reference to an instance of the callback class described above. The remaining parameters comprise the in parameters of the operation, in the order of declaration.
For example, suppose we have defined the following operation:
interface I {
  ["ami"] int foo(short s, out long l);
};
The callback class generated for operation foo is shown below:
Namespace IceInternal

    Public MustInherit Class OutgoingAsync

        Public MustOverride Sub ice_exception( _
                                    ByVal ex As Ice.Exception)
        ' ...

    End Class

End Namespace

namespace Demo

    Public MustInherit Class AMI_I_foo
        Inherits IceInternal.OutgoingAsync

        Public MustOverride Sub ice_response( _
                                        ByVal __ret As Integer, _
                                        ByVal l As Long)
        ' Mappinginternal code here...
    End Class

End Namespace
A concrete implementation of the AMI_I_foo class must provide implementations for both the generated ice_response and the inherited ice_exception abstract methods.
The proxy method for asynchronous invocation of operation foo is generated as follows:
Sub foo_async(ByVal __cb As AMI_I_foo, ByVal s As Short)

Sub foo_async(ByVal __cb As AMI_I_foo, ByVal s As Short, _
              ByVal __ctx As Ice.Context)
As usual, the version of the operation without a context parameter forwards an empty context to the version with a context parameter.

Python Mapping

For each AMI operation, the Python mapping emits an additional proxy method having the mapped name of the operation with the suffix _async. This method has a void return type. The first parameter is a reference to a callback object, as described below. The remaining parameters comprise the in parameters of the operation, in the order of declaration.
The asynchronous proxy method requires its callback object to define two methods:
def ice_response(self, <params>)
Indicates that the operation completed successfully. The parameters represent the return value and out parameters of the operation. If the operation has a non-void return type, then the first parameter of the ice_response method supplies the return value of the operation. Any out parameters present in the operation follow the return value, in the order of declaration.
def ice_exception(self, ex)
Indicates that a local or user exception was raised.
Neither ice_response nor ice_exception throw any exceptions to the caller.
For example, suppose we have defined the following operation:
interface I {
  ["ami"] int foo(short s, out long l);
};
The method signatures required for the callback object of operation foo are shown below:
class ...
    #
    # Operation signatures:
    #
    # def ice_response(self, _result, l)
    # def ice_exception(self, ex)
The proxy method for asynchronous invocation of operation foo is generated as follows:
def foo_async(self, __cb, s)

33.3.3 Example

To demonstrate the use of AMI in Ice, let us define the Slice interface for a simple computational engine:
module Demo {
    sequence<float> Row;
    sequence<Row> Grid;

    exception RangeError {};

    interface Model {
        ["ami"] Grid interpolate(Grid data, float factor)
            throws RangeError;
    };
};
Given a two-dimensional grid of floating point values and a factor, the interpolate operation returns a new grid of the same size with the values interpolated in some interesting (but unspecified) way. In the sections below, we present C++, Java, C#, Visual Basic, and Python clients that invoke interpolate using AMI.

C++ Client

We must first define our callback implementation class, which derives from the generated class AMI_Model_interpolate:
class AMI_Model_interpolateI : public Demo::AMI_Model_interpolate
{
public:
    virtual void ice_response(const Demo::Grid& result)
    {
        cout << "received the grid" << endl;
        // ... postprocessing ...
    }

    virtual void ice_exception(const Ice::Exception& ex)
    {
        try {
            ex.ice_throw();
        } catch (const Demo::RangeError& e) {
            cerr << "interpolate failed: range error" << endl;
        } catch (const Ice::LocalException& e) {
            cerr << "interpolate failed: " << e << endl;
        }
    }
};
The implementation of ice_response reports a successful result, and ice_exception displays a diagnostic if an exception occurs.
The code to invoke interpolate is equally straightforward:
Demo::ModelPrx model = ...;
AMI_Model_interpolatePtr cb = new AMI_Model_interpolateI;
Demo::Grid grid;
initializeGrid(grid);
model>interpolate_async(cb, grid, 0.5);
After obtaining a proxy for a Model object, the client instantiates a callback object, initializes a grid and invokes the asynchronous version of interpolate. When the Ice run time receives the response to this request, it invokes the callback object supplied by the client.

Java Client

We must first define our callback implementation class, which derives from the generated class AMI_Model_interpolate:
class AMI_Model_interpolateI extends Demo.AMI_Model_interpolate {
    public void ice_response(float[][] result)
    {
        System.out.println("received the grid");
        // ... postprocessing ...
    }

    public void ice_exception(Ice.UserException ex)
    {
        assert(ex instanceof Demo.RangeError);
        System.err.println("interpolate failed: range error");
    }

    public void ice_exception(Ice.LocalException ex)
    {
        System.err.println("interpolate failed: " + ex);
    }
}
The implementation of ice_response reports a successful result, and the ice_exception methods display a diagnostic if an exception occurs.
The code to invoke interpolate is equally straightforward:
Demo.ModelPrx model = ...;
AMI_Model_interpolate cb = new AMI_Model_interpolateI();
float[][] grid = ...;
initializeGrid(grid);
model.interpolate_async(cb, grid, 0.5);
After obtaining a proxy for a Model object, the client instantiates a callback object, initializes a grid and invokes the asynchronous version of interpolate. When the Ice run time receives the response to this request, it invokes the callback object supplied by the client.

C# Client

We must first define our callback implementation class, which derives from the generated class AMI_Model_interpolate:
using System;

class AMI_Model_interpolateI : Demo.AMI_Model_interpolate {
    public override void ice_response(float[][] result)
    {
        Console.WriteLine("received the grid");
        // ... postprocessing ...
    }

    public override void ice_exception(Ice.Exception ex)
    {
        Console.Error.WriteLine("interpolate failed: " + ex);
    }
}
The implementation of ice_response reports a successful result, and the ice_exception method displays a diagnostic if an exception occurs.
The code to invoke interpolate is equally straightforward:
Demo.ModelPrx model = ...;
AMI_Model_interpolate cb = new AMI_Model_interpolateI();
float[][] grid = ...;
initializeGrid(grid);
model.interpolate_async(cb, grid, 0.5);

Visual Basic Client

We must first define our callback implementation class, which derives from the generated class AMI_Model_interpolate:
imports System

Class AMI_Model_interpolateI
    Inherits Demo.AMI_Model_interpolate

    Public Overrides Sub ice_response(ByVal result As Single()())
        Console.WriteLine("received the grid")
        ' ... postprocessing ...
    End Sub


    Public Overrides Sub ice_exception(ByVal ex As Ice.Exception)
        Console.Error.WriteLine("interpolate failed: " & ex)
    End Sub

End Class
The implementation of ice_response reports a successful result, and the ice_exception method displays a diagnostic if an exception occurs.
The code to invoke interpolate is equally straightforward:
Dim model As Demo.ModelPrx = ...
Dim cb As AMI_Model_interpolate = New AMI_Model_interpolateI
Dim grid As Single()() = ...
initializeGrid(grid)
model.interpolate_async(cb, grid, 0.5)

Python Client

We must first define our callback implementation class:
class AMI_Model_interpolateI(object):
    def ice_response(self, result):
        print "received the grid"
        # ... postprocessing ...

  def ice_exception(self, ex):
      try:
          raise ex
      except Demo.RangeError, e:
          print "interpolate failed: range error"
      except Ice.LocalException, e:
          print "interpolate failed: " + str(e)
The implementation of ice_response reports a successful result, and the ice_exception method displays a diagnostic if an exception occurs.
The code to invoke interpolate is equally straightforward:
model = ...
cb = AMI_Model_interpolateI()
grid = ...
initializeGrid(grid)
model.interpolate_async(cb, grid, 0.5)

33.3.4 Concurrency Issues

Support for asynchronous invocations in Ice is enabled by the client thread pool (see Section 32.9), whose threads are primarily responsible for processing reply messages. It is important to understand the concurrency issues associated with asynchronous invocations:
• A callback object must not be used for multiple simultaneous invocations. An application that needs to aggregate information from multiple replies can create a separate object to which the callback objects delegate.
• Calls to the callback object are made by threads from the Ice run time’s client thread pool, therefore synchronization may be necessary if the application might interact with the callback object at the same time as the reply arrives.
• The number of threads in the client thread pool determines the maximum number of simultaneous callbacks possible for asynchronous invocations. The default size of the client thread pool is one, meaning invocations on callback objects are serialized. If the size of the thread pool is increased, the application may require synchronization, such as when multiple callback objects delegate to a single shared object.
• AMI invocations are always made without collocation optimization, regardless of the optimization setting of the proxy (see Section 32.20).

33.3.5 Timeouts

Supporting the use of timeouts for asynchronous invocations presents a special challenge for the Ice run time. Unlike synchronous invocations, for which the Ice run time simply blocks the calling thread for the specified timeout period, asynchronous invocations must return control to the calling thread as soon as possible.
Consequently, the Ice run time is forced to use another mechanism: a dedicated thread that, among other duties, periodically reviews pending asynchronous invocations. If an invocation has a timeout that has expired, the dedicated thread terminates the invocation by reporting Ice::TimeoutException to the ice_exception method of the invocation’s callback object. For example, we can handle this exception in C++ as shown below:
class AMI_Model_interpolateI : public Demo::AMI_Model_interpolate
{
public:
    // ...

    virtual void ice_exception(const Ice::Exception& ex)
    {
        try {
            ex.ice_throw();
        } catch (const Demo::RangeError& e) {
            cerr << "interpolate failed: range error" << endl;
        } catch (const Ice::TimeoutException&) {
            cerr << "interpolate failed: timeout" << endl;
        } catch (const Ice::LocalException& e) {
            cerr << "interpolate failed: " << e << endl;
        }
    }
};
The configuration property Ice.MonitorConnections, whose value is specified in seconds, determines the frequency with which the dedicated thread performs these reviews. If the property is set to zero, the thread is disabled and AMI timeouts do not occur.
The use of a dedicated thread to monitor pending asynchronous invocations means the precision of invocation timeouts is dependent on the value of the Ice.MonitorConnections property. For example, if you make an asynchronous invocation using a proxy with a timeout of 5000ms and Ice.MonitorConnections is set to 30, it can take up to thirty seconds for the timeout to be reported to the callback object. You can improve the accuracy of AMI timeouts by setting Ice.MonitorConnections to a value that is closer to your timeout, just be careful that you do not cause the dedicated thread to consume too much processing time.

33.3.6 Error Handling

It is important to remember that all errors encountered by an AMI invocation are reported back via the ice_exception callback, even if the error condition is encountered “on the way out”, when the operation is invoked. The reason for this is consistency: if an invocation, such as foo_async could throw exceptions, you would have to handle exceptions in two places in your code: at the point of call for exception that are encountered “on the way out”, and in ice_exception for error conditions that are detected after the call is initiated.
Where this matters is if you want to send off a number of AMI calls, each of which depends on the preceding call to have succeeded. For example:
p1>foo_async(cb1);
p2>bar_async(cb2);
If bar depends for its correct working on the successful completion of foo, this code will not work because the bar invocation will be sent regardless of whether foo failed or not.
In such cases, where you need to be sure that one call is dispatched only if a preceding call succeeds, you must instead invoke bar from within foo’s ice_response implementation, instead of from the main-line code.

33.3.7 Limitations

AMI invocations cannot be sent using collocated optimization. If you attempt to invoke an AMI operation using a proxy that is configured to use collocation optimization, the Ice run time raises CollocationOptimizationException. Section 32.20 provides more information about this optimization and describes how to disable it when necessary.
Table of Contents Previous Next
Logo