Concepts of asynchronous RESTful operations were already doing rounds (check here), even before JAX-RS 2.0 introduced the server side asynchronous processing API. The very basic principle behind asynchronous RESTful API is pretty simple. This is an article describing how Jersey has implemented JAX-RS 2.0, and some information on the specifications.
Taking a cue from the concepts, we propose a prototype architecture for an asynchronous and distributed JAX-RS provider:
- Instead of dispatching the request to a JAX-RS annotated service method, suspend the operation somehow. Then, instead of returning a 201 or 200 HTTP response, issue a 202 (Accepted) response.
- Set a redirection URL with a 'Location' header in response.
- The suspended operation should be handled by a delegate in an asynchronous manner. Once complete, the response resource should be created at the above mentioned URL.
- Client would then need to do a HTTP GET on the redirection URL to get the actual response. The GET would either return the response resource or, either a 204 or 404, depending on the request processing state.
Suspension of the invocation would imply, dispatching the processing asynchronously to some other component of the provider. From JAX-RS perspective, this processing component would need to invoke a (annotated) method on a plain Java class via introspection, and probably pass the query/path params and/or POST content as arguments to it.
If we build the provider on top of a data distribution platform (datagrid), then we can distribute the request data (arguments, request details, correlationid). A receiving member would then need to dispatch the request data to the processor component, which would actually execute the invocation. The response would be stored in the datagrid, corresponding to the correlationid.
This correlationid would have been sent back to the client, as a part of the redirection URL.
Hazelcast
Hazelcast was used as the datagrid provider in this project. The distributed IMap, provided by Hazelcast can have local entry listeners attached to it. Entry listeners receive callbacks on map entry events like put/get/remove etc. Thus, whenever a request data is added (put) to a distributed map, an entry listener on the data owning node will be notified. The received data would be delegated to the processing component, to perform the dispatch to JAX-RS annotated service methods.
I have tried to represent it pictorially using a sequence diagram. Well, this may not be the best example of a SD usage, however, it will probably bring out the basic idea.
Design
The following logical components would be required in order to design the platform:
- A HTTP server component that would be able to map RESTful uri to method invocation (REST Listener)
- Intercept HTTP requests and parse request parameters/body
- Prepare serializable request data structure
- Generate HTTP response
- A discovery component to auto discover and register JAX-RS annotated elements (RequestDispatcher)
- Auto discover JAX-RS annotated elements and prepare method invocation metadata
- Map the method invocation metadata to uri pattern
- Execute method invocation
- A Hazelcast component capable of registering local entry listeners and perform distributed map operations (HazelcastService)
- Register local entry listener callback that gets notified on distributed map entry event
- Provide a facade for map operations on Hazelcast datagrid
Code samples
A Spring Boot project is available on Github. The REST Listener is built on top of a lightweight, opensource Netty based library Webbit.
Being a prototype project, it has some limitations as of now. Basically the project implements a small subset of the JAX RS 2.0 specs.
- Only JSON resquest/response is consumed/produced.
- Only GET/POST/DELETE is supported.
- For the asynchronous server API AsyncResponse, not all operations are supported. Details documented in code.
On invocation of asyncResponse.resume(), the response is made available at resource URI, as shared in the initial response.
@GET
@Path("/async")
//the first argument has to be @Suspended AsyncResponse type
//an optional second argument, if present, is the deserialized JSON
//content of the request (for POST)
public void helloAsync(@Suspended AsyncResponse asynResponse)
{
//No separate asynchronous strategy is required here.
//This method will automatically be executed in a separate worker (thread/node)
Object returned = invokeMySuperHeavyService();
asynResponse.resume(returned);
//Now the response is committed and made available as JSON
}