Suspendable Request Proposal


Overview

This proposal aims to meet some of the Asynchronous Servlets requirements of JSR315
by allowing request handling to be suspended and resumed.

Use-Cases

This proposal does not address the use-cases of asynchronous IO, which are addressed in the
Async Content Handlers proposal.

Waiting for Resources.

After processing a request, a filter/servlet needs to wait for a resource before generating a response. The waited for resource may be a JMS message, a JDBC connection from a Datasource, a
response to a remote web services request or some other asynchronous event.

Quality of Service

In order to provide quality of service, A web application may wish to be able to give priority to some request over others. Request of lower priority are suspended until there are no higher priority requests to be dispatched/handled.

Suspended Requests

The request handling lifecycle is
extended so that the handling of a single request can span multiple
dispatches to the filter chain and the servlet service method. This
allows the handling of a request and the completion of a response to
be delayed until resources are available or asynchronous events occur
or complete.
A request life cycle is started when
a request/response pair is first dispatched. Normally the request
life cycle will complete when a the dispatch returns and the response
will be committed, flushed and completed. However, the extension
allows for a request to be suspended, so that when the dispatch
returns to the container, the request lifecycle is not completed.
A suspended request is held by the
container until either it is resumed or a timeout expires. The
request is then retried by being dispatched to the normal filter
chain and servlet service method.

This cycle can repeat more that once
and the suspension effectively turns the existing dispatch mechanism
into an asynchronous callback.
To allow this mechanism to work with
existing frameworks and code that is unaware of suspension, when a
request is suspended, the response object is disabled so that headers
and content may not be written.

Java API

ServletRequest

The existing
ServletRequest
interface is extended with methods to
suspend request handling:

public interface ServletRequest 
{
 /**
  * Suspend the processing of the request and associated {@link ServletResponse}.
  * 
  * <p>After this method has been called, the lifecycle of the request 
  * will be extended beyond the return to the container from the 
  * {@link Servlet#service(ServletRequest, ServletResponse)}  method and 
  * {@link Filter#doFilter(ServletRequest, ServletResponse, FilterChain)} calls. If a 
  * request is suspended, then the container will not commit the associated response 
  * when the call to the filter chain and/or servlet service method returns to the 
  * container. Instead the container will wait until either 
  * {@link ServletRequest#complete()} is called, {@link ServletRequest#resume()is
  * called or the passed timeout expires.  If resume is called or the timeout expires
  * then the request will be redispatched via the filter and servlet processing.
  * </p>
  * 
  * <p>If a request is already suspended, any subsequent calls to suspend will set
  * the timeout to the minimum of the previous timeout and the newly passed 
  * timeout</p>
  * 
  * <p>Suspend may only be called by a thread that is within the service calling 
  * stack of {@link Filter#doFilter(ServletRequest, ServletResponse, FilterChain)}
  * and/or {@link Servlet#service(ServletRequest, ServletResponse)}. A request that has    
  * been dispatched for error handling may not be suspended.
  * </p>
  * 
  * @see {@link #resume()}
  * @see {@link #complete()}
  * @since 3.0
  * 
  * @param timeoutMs The time in milliseconds to wait before retrying this request.
  * 
  * @exception IllegalStateException If the calling thread is not within the calling 
  * stack of  {@link Filter#doFilter(ServletRequest, ServletResponse, FilterChain)}
  * and/or {@link Servlet#service(ServletRequest, ServletResponse)} or if the request 
  * has been dispatched for error handling.
  */
  void suspend(long timeoutMs);
    
 /**
  * Resume a suspended request.
  * 
  * <p>This method can be called by any thread that has been passed a reference to 
  * a suspended request. When called the request is redispatched to the normal filter 
  * chain and servlet processing.</p>
  * 
  * <p>If resume is called before a suspended request is returned to the container 
  * (ie the thread that called {@link #suspend()} is still within the filter
  * chain and/or servlet service method), then the resume does not take effect until
  * the call to the filter chain and/or servlet returns to the container. In this 
  * case both {@link #isSuspended()} and {@link isResumed()} continue to return true.
  * until the request is returned to the container.</p>
  * 
  * <p>Multiple calls to resume are ignored</p>
  * 
  * @see {@link #suspend()}
  * @since 3.0
  * @exception IllegalStateException if the request is not suspended.
  * 
  */
  void resume();

 /**
  * Complete a suspended request.
  * 
  * <p>This method can be called by any thread that has been passed a reference to 
  * a suspended request. When a request is completed, the associated response object 
  * commited and flushed. The request is not redispatched.</p>
  * 
  * <p>If complete is called before a suspended request is returned to the container 
  * (ie the thread that called {@link #suspend(long)} is still within the filter
  * chain and/or servlet service method), then the complete does not take effect until
  * the call to the filter chain and/or servlet returns to the container. In this 
  * case {@link #isSuspended()} continues to return true until the request is returned to the container.</p>
  * <p>Closing the response output stream or writer is equivalent to a call to complete()</p>
  * 
  * @see {@link #suspend()}
  * @since 3.0
  * @exception IllegalStateException if the request is not suspended.
  * 
  */
  void complete();

 /**
  * @return true after {@link #suspend(long)} has been called and before the request 
  * has been resumed or timed out.
  * @since 3.0
  */
  boolean isSuspended();

 /**
  * @return true if the request has been redispatched by a call to {@link #resume()} or 
  * by a timeout.   Returns false after any subsequent call to suspend
  * @since 3.0
  */
  boolean isResumed();

 /**
  * @return true after a request has been redispatched as the result of a timeout. 
  * Returns false after any subsequent call to suspend.
  * @since 3.0
  */
  boolean isTimeout();

 // existing methods not shown
}

ServletRequestListener

The existing
ServletRequesListener
interface is extended with methods to notify resumes and suspensions:

public interface ServletRequest 
{
 // existing methods not shown

 /**
  * A request has been suspended.
  * Called by the thread that dispatched the servlet when it
  * has returned to the container.
  */
 void requestSuspended(ServletRequestEvent rre);

 /**
  * A request has been resumed.
  * Called by the thread that will dispatch to the servlet 
  * immediately before dispatch. 
  */
 void requestResumed(ServletRequestEvent rre);

 /**
  * A request has been completed.
  * Called from a call to {@ServletRequest#complete()} 
  * @since 3.0
  */
 void requestCompleted(ServletRequestEvent rre);

}

Examples


Quality of Service Filter and Resource throttling.

This example shows how delayed
request handling can be applied to an existing web application using
a Filter. This filter ensures quality of service by ensuring that no
more than 20 requests are simultaneously being handled. Any
additional requests are suspended and delayed until previous requests
complete. Moreover, authentication and authorization are used to
maintain two priority queues of delayed requests. The requests in the
higher priority queue are preferentially resumed.
Because the delayed request handling
can be implemented in a standard filter, it would be possible for
this filter to be configured after application supplied filters. Thus
if the application provided it's own authentication and authorization
mechanisms in a filter, these would be able to be used by this
filter.

QosFilter.java:

public class QosFilter implements Filter
{
    private final static String PASS = "PASS";
    int _passes = 20;
    Queue<ServletRequest> _lowPriority = new LinkedList<ServletRequest>();
    Queue<ServletRequest> _highPriority = new LinkedList<ServletRequest>();
    
    public void init(FilterConfig filterConfig) {}
    public void destroy(){}

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
        throws IOException, ServletException
    {
        synchronized(this)
        {
            boolean has_pass=Boolean.TRUE.equals(request.getAttribute(PASS));
        
            if (!has_pass)
            {
                if (request.isResumed())
                {
                    _lowPriority.remove(request);
                    _highPriority.remove(request);
                    ((HttpServletResponse)response)
                        .sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
                    return;
                }
                
                if (_passes>0)
                {
                    has_pass=true;
                    _passes--;
                }
                else
                {
                    request.suspend();
                    if (((HttpServletRequest)request).isUserInRole("priority"))
                        _highPriority.add(request);
                    else
                        _lowPriority.add(request);
                    return;
                }
            }
        }
        
        
        try
        {
            assert has_pass;
            chain.doFilter(request,response);
        }
        finally
        {
            synchronized(this)
            {
                ServletRequest waiting = _highPriority.poll();
                if (waiting==null)
                    waiting = _lowPriority.poll();
                if (waiting==null)
                    _passes++;
                else
                {
                    waiting.setAttribute(PASS,Boolean.TRUE);
                    waiting.resume();
                }
            }
        }
    }
}