Asynchronous Method Dispatch (AMD) in Java
The number of simultaneous synchronous requests a server is capable of supporting is determined by the number of threads in the server's thread pool. If all of the threads are busy dispatching long-running operations, then no threads are available to process new requests and therefore clients may experience an unacceptable lack of responsiveness.
Asynchronous Method Dispatch (AMD), the server-side equivalent of AMI, addresses this scalability issue. Using AMD, a server can receive a request but then suspend its processing in order to release the dispatch thread as soon as possible. When processing resumes and the results are available, the server can provide its results to the Ice runtime for delivery to the client.
AMD is transparent to the client, that is, there is no way for a client to distinguish a request that, in the server, is processed synchronously from a request that is processed asynchronously.
In practical terms, an AMD operation typically queues the request data for later processing by an application thread (or thread pool). In this way, the server minimizes the use of dispatch threads and becomes capable of efficiently supporting thousands of simultaneous clients.
Async Skeleton
The easiest way to use AMD in Java is to make your servant class implement the async skeleton interface generated by the Slice compiler. For example:
// This servant uses AMD
class Chatbot implements AsyncGreeter {
// your implementation here
}
Enabling AMD Piecemeal
If you prefer to implement some operations asynchronously (with AMD) and other operations synchronously, you can add the ["amd"] metadata directive to the operations you want to implement with AMD and use the default skeleton interface.
The metadata directive replaces synchronous dispatch on the default skeleton, that is, a particular operation implementation must use synchronous or asynchronous dispatch and cannot use both.
Consider the following Slice definitions:
interface Controller
{
["amd"] void startProcess();
int endProcess();
}
In this example, the startProcess of the default skeleton interface uses asynchronous dispatch while endProcess uses synchronous dispatch.
AMD Mapping
With AMD, the skeleton’s abstract method is named <operation-name>Async. This method returns an java.util.concurrent.CompletionStage<T> and accepts the operation’s in-parameters.
The implementation of the operation, which typically returns an instance of the derived class java.util.concurrent.CompletableFuture<T>, must eventually complete the future by supplying either the results or an exception.
For example, suppose we have defined the following operation:
interface Example
{
string op(short s, out long count);
}
Operation op is mapped as follows in the skeleton interface:
public interface Example extends com.zeroc.Ice.Object {
public static class OpResult {
public String returnValue;
public long count;
...
}
// synchronous dispatch methods
}
public interface AsyncExample extends com.zeroc.Ice.Object {
CompletionStage<Example.OpResult> opAsync(
short s, com.zeroc.Ice.Current current);
}
You would get the same opAsync method on the default skeleton (Example) if you decorate op with ["amd"].
AMD Exceptions
There are two processing contexts in which the logical implementation of an AMD operation may need to report an exception: the dispatch thread (the thread that receives the request), and the response thread (the thread that sends the response).
These are not necessarily two different threads: it is legal to send the response from the dispatch thread.
The implementation of the Async method in your servant class can throw an exception synchronously: it’s equivalent to returning a future completed with this exception.
Chaining AMI and AMD Invocations
Since the asynchronous proxy API and the asynchronous dispatch API both use CompletionStage, it is possible to implement an asynchronous dispatch by sending an asynchronous request to a proxy.
Continuing our example from the previous section, suppose our servant also holds a proxy to another object of the same type and derives its response from that of the other object:
class ExampleServant implements AsyncExample {
private final ExamplePrx _other;
@Override
public CompletionStage<Example.OpResult> opAsync(short s, Current current) {
var result = _other.opAsync(s);
// ... some other work while op is executing
return result;
}
}