Thursday, January 3, 2008

Declarative Asynchronous Web Service Proposal

I have recently spent quite a bit of time pondering asynchronous web services. By this I mean in the simplest case a service where the response is returned on a separate connection and not the API level asynchronicity that was added in JAX-WS. It is entirely possible to implement this kind of service using the APIs provided by JAX-WS but it is not as fun or easy as it could be.

With my colleague Manoj Kumar, who to be fair does all the hard core server side bits, we have been thinking about have to make this process far less complicated for the developer by using declarative annotations. In particular we wanted to make sure that any process handling is entirely dealt with by the container. Unfortunately the implementation for this didn't make its way into the most recent TP3 release of JDeveloper 11; but the associated UI and annotations are in there. Of course we cannot promise that this feature will ever make it into a future product; but we would welcome your feedback on our thoughts so far.

Currently this proposal uses oracle specific annotations; but we have been working on them with a mind to ensuring that perhaps one day this could become part of the JAX-WS standard. This seems to be a reasonably useful scenario and therefore worthwhile including in the standard.

Lets look at a simple hello world example so you can have an idea of what I am talking about:

@WebService
@AsyncWebService
public class Hello
{
   public String sayHello(String name)
   {
      return "Hello " + name;
   }
}

So when this is deployed the generated WSDL will look rather different from the case without the @AsyncWebService. There will be two port types elements rather than the single would you would normally expect. The first will be the "Hello" port type which has one operation that takes the name as a parameter but doesn't have a return value. The second port type will take a name something like "HelloResponse" and will again have one operation, called perhaps "sayHelloResponse", which with have the return value as its parameter. (Note we haven't nailed down the name mapping rules yet hence I am being a little bit vague)

There will be of course bindings for each port type; but only the "Hello" port type will be surfaced as a port on a service as the server will only implement the initiation part of the process. The response port type will of course need to be implemented and published by the client.

So the resultant WSDL will look something like this the following. Notice that there is a partnerLinkType at the end of the WSDL. This is useful as it makes it much easier for tools to associate the two parts of the service. (Particularly when interacting with BPEL processes)

<?xml version="1.0" encoding="UTF-8" ?>
<definitions
     name="HelloService"
     targetNamespace="http://model/"
     xmlns="http://schemas.xmlsoap.org/wsdl/"
     xmlns:tns="http://model/"
     xmlns:xsd="http://www.w3.org/2001/XMLSchema"
     xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"
     xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
     xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
    >
    <types>
        <schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://model/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
             xmlns:tns="http://model/">
            <xsd:complexType name="sayHello">
                <xsd:sequence>
                    <xsd:element name="arg0" type="xsd:string"/>
                </xsd:sequence>
            </xsd:complexType>
            <xsd:element name="sayHello" type="tns:sayHello"/>
            <xsd:complexType name="response">
                <xsd:sequence>
                    <xsd:element name="arg0" type="xsd:string"/>
                </xsd:sequence>
            </xsd:complexType>
            <xsd:element name="response" type="tns:response"/>
        </schema>
    </types>

    <message name="sayHelloInput">
        <part name="parameters" element="tns:sayHello"/>
    </message>
    <message name="responseInput">
        <part name="parameters" element="tns:response"/>
    </message>    

    <portType name="Hello">
        <operation name="sayHello">
            <input message="tns:sayHelloInput" xmlns:ns1="http://www.w3.org/2006/05/addressing/wsdl"
                 ns1:Action=""/>
        </operation>
    </portType>
    <portType name="HelloResponse">
        <operation name="sayHelloResponse">
            <input message="tns:responseInput" xmlns:ns1="http://www.w3.org/2006/05/addressing/wsdl"
                 ns1:Action=""/>
        </operation>
    </portType>
    
    <binding name="HelloSoap" type="tns:Hello">
        <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
        <operation name="sayHello">
            <soap:operation soapAction=""/>
            <input>
                <soap:body use="literal"/>
            </input>
        </operation>
    </binding>
    

    <binding name="HelloResponseSoap" type="tns:HelloResponse">
        <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
        <operation name="sayHelloResponse">
            <soap:operation soapAction=""/>
            <input>
                <soap:body use="literal"/>
            </input>
        </operation>
    </binding>
    
    
    <service name="Hello">
        <port name="HelloPort" binding="tns:HelloSoap">
            <soap:address location="http://localhost:8988/Application1-Model-context-root/mathsserviceport"/>
        </port>
    </service>
    
    
    <partnerLinkType name="HelloService"
                     xmlns="http://schemas.xmlsoap.org/ws/2003/05/partner-link/">
        <role name="In">
            <portType name="tns:Hello"/>
        </role>
        <role name="Out">
            <portType name="tns:HelloResponse"/>
        </role>
    </partnerLinkType>
    
</definitions>

So how is this implemented under the covers? In order to allow the initial call to Hello.sayHello() to return immediately we need to defer the processing of the call to the web service to another process / thread. Starting a native java thread is an obvious no-no when running in a managed JEE container but there are a number of alternatives that do work better in the EE world.

The implementation that we have been working on publishes the request onto a JMS queue. This request is then "consumed" by a message driven bean at some later time. The MDB is then responsible for calling the business logic and generating a suitable response message and sending it to the client. This is all transparent to the user as they only need to declare that the service is asynchronous. There might be specific features for a given implementation, for example you might want to specify a more secure and reliable queue that the default one we provide, but the most basic case would just work. In our implementation we provide a "@AsynchronousWebServiceQueue" annotation that allows the developer to specify the JNDI name of the JMS queue or connection factory.

Lets take a look at a sequence diagram show the full life cycle of a particular invocation. The client publishes the callback service first of all. It then invokes the initiation service and provides correlation and reply-to information in the form of WS-Addressing headers. This then causes a message to be posted on to JMS queue which is than consumed by a MDB. The MDB goes on to call the business logic in the normal way. When the process is finished the MDB uses the information stored in the WS-Addressing "Reply-To" header to post a response to the correct location.

Of course one message in and one message out is a rather simplistic example. For example you might want to update the client with some kind of progress update during a process. In our proposal you can do this using using a separate callback interface.

@WebService
@AsyncWebService
public class InsuranceClient
{
   @CallbackRef
   InsuranceCallback callback;


   public Result processClaim(Claim claim)
   {
      callback.updateStatus("Stared Processing");

      // Do something

      callback.updateStatus("Done something");

      // Some something else

      callback.updateStatus("Finished Processing");

      // Finally return

      return result;
   }


   @WebService
   public interface InsuranceCallback
   {
      public void updateStatus(String status);
   }
}

In this case the generated WSDL with have a response port type that will contain both the "processClaimReponse" and "updateStatus" operations. The callback field will be injected with the correct service instance based on the contents of the WS-Addressing headers.

You can take this example one step further and make the business logic method return void. In this case the implementor is wholly responsible for managing the callbacks. This can be useful in the case where the response depends on the output of the process.

@WebService
@AsyncWebService
public class InsuranceClient
{
   @CallbackRef
   InsuranceCallback callback;


   public void processClaim(Claim claim)
   {
      // Process claim

      if (success)
      {
         callback.claimRejected(...);
      }
      else
      {
         callback.claimAccepted(...);
      }
   }


   @WebService
   public interface InsuranceCallback
   {
      public void claimAccepted(...);
      public void claimRejected(...);
   }
}

As you can see this simple annotation model can cover a variety of different interaction models. There are some important areas that we are still working on though: in particular how we handle WS-* policies on the response/callback messages and how we deal with faults are still works in progress. We also have a range of annotation for controlling the response port type and operations names but I have left them out here for reasons of clarity but can be easily found using the property inspector on the code in JDeveloper.

Finally it is worth noting that BEA has a similar design in there WebLogic product. From what I understand though it appears that there implementation only provides the callback injection and doesn't appear to offer the same process management as in our proposal. This might be due to a misunderstanding from myself of there documentation. I do personally think that hiding the process details is the most powerful aspect of our proposal.

I would be interested to hear about other implementations and feedback on this proposal.

No comments: