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.
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.
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 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.
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:
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.
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:
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.
Neither ice_response nor
ice_exception throw any exceptions to the caller.
interface I {
["ami"] int foo(short s, out long l);
};
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;
}
void foo_async(const AMI_I_fooPtr&, Ice::Short);
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:
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.
Neither ice_response nor
ice_exception throw any exceptions to the caller.
interface I {
["ami"] int foo(short s, out long l);
};
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);
}
public void foo_async(AMI_I_foo __cb, short s);
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:
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.
Neither ice_response nor
ice_exception throw any exceptions to the caller.
interface I {
["ami"] int foo(short s, out long l);
};
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);
// Mapping‑internal 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.
void foo_async(AMI_I_foo __cb, short s);
void foo_async(AMI_I_foo __cb, short s, Ice.Context __ctx);
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:
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.
Neither ice_response nor
ice_exception throw any exceptions to the caller.
interface I {
["ami"] int foo(short s, out long l);
};
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)
' Mapping‑internal 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.
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)
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.
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.
Neither ice_response nor
ice_exception throw any exceptions to the caller.
interface I {
["ami"] int foo(short s, out long l);
};
class ...
#
# Operation signatures:
#
# def ice_response(self, _result, l)
# def ice_exception(self, ex)
def foo_async(self, __cb, s)
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.
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.
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.
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);
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)
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)
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:
•
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.
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.
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.
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.
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.