| // |
| // ======================================================================== |
| // Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.server; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.TimeUnit; |
| |
| import javax.servlet.AsyncListener; |
| import javax.servlet.RequestDispatcher; |
| import javax.servlet.ServletContext; |
| import javax.servlet.ServletResponse; |
| |
| import org.eclipse.jetty.server.handler.ContextHandler; |
| import org.eclipse.jetty.server.handler.ContextHandler.Context; |
| import org.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| import org.eclipse.jetty.util.thread.Scheduler; |
| |
| /** |
| * Implementation of AsyncContext interface that holds the state of request-response cycle. |
| */ |
| public class HttpChannelState |
| { |
| private static final Logger LOG = Log.getLogger(HttpChannelState.class); |
| |
| private final static long DEFAULT_TIMEOUT=Long.getLong("org.eclipse.jetty.server.HttpChannelState.DEFAULT_TIMEOUT",30000L); |
| |
| /** The dispatched state of the HttpChannel, used to control the overall livecycle |
| */ |
| public enum State |
| { |
| IDLE, // Idle request |
| DISPATCHED, // Request dispatched to filter/servlet |
| ASYNC_WAIT, // Suspended and parked |
| ASYNC_WOKEN, // A thread has been dispatch to handle from ASYNCWAIT |
| ASYNC_IO, // Has been dispatched for async IO |
| COMPLETING, // Request is completable |
| COMPLETED, // Request is complete |
| UPGRADED // Request upgraded the connection |
| } |
| |
| /** |
| * The actions to take as the channel moves from state to state. |
| */ |
| public enum Action |
| { |
| REQUEST_DISPATCH, // handle a normal request dispatch |
| ASYNC_DISPATCH, // handle an async request dispatch |
| ASYNC_EXPIRED, // handle an async timeout |
| WRITE_CALLBACK, // handle an IO write callback |
| READ_CALLBACK, // handle an IO read callback |
| WAIT, // Wait for further events |
| COMPLETE // Complete the channel |
| } |
| |
| /** |
| * The state of the servlet async API. This can lead or follow the |
| * channel dispatch state and also includes reasons such as expired, |
| * dispatched or completed. |
| */ |
| public enum Async |
| { |
| STARTED, |
| DISPATCH, |
| COMPLETE, |
| EXPIRING, |
| EXPIRED |
| } |
| |
| private final boolean DEBUG=LOG.isDebugEnabled(); |
| private final HttpChannel<?> _channel; |
| |
| private List<AsyncListener> _asyncListeners; |
| private State _state; |
| private Async _async; |
| private boolean _initial; |
| private boolean _asyncRead; |
| private boolean _asyncWrite; |
| private long _timeoutMs=DEFAULT_TIMEOUT; |
| private AsyncContextEvent _event; |
| |
| protected HttpChannelState(HttpChannel<?> channel) |
| { |
| _channel=channel; |
| _state=State.IDLE; |
| _async=null; |
| _initial=true; |
| } |
| |
| public State getState() |
| { |
| synchronized(this) |
| { |
| return _state; |
| } |
| } |
| |
| public void addListener(AsyncListener listener) |
| { |
| synchronized(this) |
| { |
| if (_asyncListeners==null) |
| _asyncListeners=new ArrayList<>(); |
| _asyncListeners.add(listener); |
| } |
| } |
| |
| public void setTimeout(long ms) |
| { |
| synchronized(this) |
| { |
| _timeoutMs=ms; |
| } |
| } |
| |
| public long getTimeout() |
| { |
| synchronized(this) |
| { |
| return _timeoutMs; |
| } |
| } |
| |
| public AsyncContextEvent getAsyncContextEvent() |
| { |
| synchronized(this) |
| { |
| return _event; |
| } |
| } |
| |
| @Override |
| public String toString() |
| { |
| synchronized (this) |
| { |
| return String.format("%s@%x{s=%s i=%b a=%s}",getClass().getSimpleName(),hashCode(),_state,_initial,_async); |
| } |
| } |
| |
| public String getStatusString() |
| { |
| synchronized (this) |
| { |
| return String.format("s=%s i=%b a=%s",_state,_initial,_async); |
| } |
| } |
| |
| /** |
| * @return Next handling of the request should proceed |
| */ |
| protected Action handling() |
| { |
| synchronized (this) |
| { |
| if(DEBUG) |
| LOG.debug("{} handling {}",this,_state); |
| switch(_state) |
| { |
| case IDLE: |
| _initial=true; |
| _state=State.DISPATCHED; |
| return Action.REQUEST_DISPATCH; |
| |
| case COMPLETING: |
| return Action.COMPLETE; |
| |
| case COMPLETED: |
| return Action.WAIT; |
| |
| case ASYNC_WOKEN: |
| if (_asyncRead) |
| { |
| _state=State.ASYNC_IO; |
| _asyncRead=false; |
| return Action.READ_CALLBACK; |
| } |
| if (_asyncWrite) |
| { |
| _state=State.ASYNC_IO; |
| _asyncWrite=false; |
| return Action.WRITE_CALLBACK; |
| } |
| |
| if (_async!=null) |
| { |
| Async async=_async; |
| switch(async) |
| { |
| case COMPLETE: |
| _state=State.COMPLETING; |
| return Action.COMPLETE; |
| case DISPATCH: |
| _state=State.DISPATCHED; |
| _async=null; |
| return Action.ASYNC_DISPATCH; |
| case EXPIRING: |
| break; |
| case EXPIRED: |
| _state=State.DISPATCHED; |
| _async=null; |
| return Action.ASYNC_EXPIRED; |
| case STARTED: |
| // TODO |
| if (DEBUG) |
| LOG.debug("TODO Fix this double dispatch",new IllegalStateException(this |
| .getStatusString())); |
| return Action.WAIT; |
| } |
| } |
| |
| return Action.WAIT; |
| |
| default: |
| throw new IllegalStateException(this.getStatusString()); |
| } |
| } |
| } |
| |
| public void startAsync(AsyncContextEvent event) |
| { |
| final List<AsyncListener> lastAsyncListeners; |
| |
| synchronized (this) |
| { |
| if (_state!=State.DISPATCHED || _async!=null) |
| throw new IllegalStateException(this.getStatusString()); |
| |
| _async=Async.STARTED; |
| _event=event; |
| lastAsyncListeners=_asyncListeners; |
| _asyncListeners=null; |
| } |
| |
| if (lastAsyncListeners!=null) |
| { |
| for (AsyncListener listener : lastAsyncListeners) |
| { |
| try |
| { |
| listener.onStartAsync(event); |
| } |
| catch(Exception e) |
| { |
| LOG.warn(e); |
| } |
| } |
| } |
| } |
| |
| protected void error(Throwable th) |
| { |
| synchronized (this) |
| { |
| if (_event!=null) |
| _event.setThrowable(th); |
| } |
| } |
| |
| /** |
| * Signal that the HttpConnection has finished handling the request. |
| * For blocking connectors, this call may block if the request has |
| * been suspended (startAsync called). |
| * @return next actions |
| * be handled again (eg because of a resume that happened before unhandle was called) |
| */ |
| protected Action unhandle() |
| { |
| synchronized (this) |
| { |
| if(DEBUG) |
| LOG.debug("{} unhandle {}",this,_state); |
| |
| switch(_state) |
| { |
| case DISPATCHED: |
| case ASYNC_IO: |
| break; |
| default: |
| throw new IllegalStateException(this.getStatusString()); |
| } |
| |
| if (_asyncRead) |
| { |
| _state=State.ASYNC_IO; |
| _asyncRead=false; |
| return Action.READ_CALLBACK; |
| } |
| |
| if (_asyncWrite) |
| { |
| _asyncWrite=false; |
| _state=State.ASYNC_IO; |
| return Action.WRITE_CALLBACK; |
| } |
| |
| if (_async!=null) |
| { |
| _initial=false; |
| switch(_async) |
| { |
| case COMPLETE: |
| _state=State.COMPLETING; |
| _async=null; |
| return Action.COMPLETE; |
| case DISPATCH: |
| _state=State.DISPATCHED; |
| _async=null; |
| return Action.ASYNC_DISPATCH; |
| case EXPIRED: |
| _state=State.DISPATCHED; |
| _async=null; |
| return Action.ASYNC_EXPIRED; |
| case EXPIRING: |
| case STARTED: |
| scheduleTimeout(); |
| _state=State.ASYNC_WAIT; |
| return Action.WAIT; |
| } |
| } |
| |
| _state=State.COMPLETING; |
| return Action.COMPLETE; |
| } |
| } |
| |
| public void dispatch(ServletContext context, String path) |
| { |
| boolean dispatch; |
| synchronized (this) |
| { |
| if (_async!=Async.STARTED && _async!=Async.EXPIRING) |
| throw new IllegalStateException("AsyncContext#dispath "+this.getStatusString()); |
| _async=Async.DISPATCH; |
| |
| if (context!=null) |
| _event.setDispatchContext(context); |
| if (path!=null) |
| _event.setDispatchPath(path); |
| |
| switch(_state) |
| { |
| case DISPATCHED: |
| case ASYNC_IO: |
| dispatch=false; |
| break; |
| case ASYNC_WAIT: |
| _state=State.ASYNC_WOKEN; |
| dispatch=true; |
| break; |
| case ASYNC_WOKEN: |
| dispatch=false; |
| break; |
| default: |
| LOG.warn("async dispatched when complete {}",this); |
| dispatch=false; |
| break; |
| } |
| } |
| |
| cancelTimeout(); |
| if (dispatch) |
| scheduleDispatch(); |
| } |
| |
| protected void expired() |
| { |
| final List<AsyncListener> aListeners; |
| AsyncContextEvent event; |
| synchronized (this) |
| { |
| if (_async!=Async.STARTED) |
| return; |
| _async=Async.EXPIRING; |
| event=_event; |
| aListeners=_asyncListeners; |
| } |
| |
| if (aListeners!=null) |
| { |
| for (AsyncListener listener : aListeners) |
| { |
| try |
| { |
| listener.onTimeout(event); |
| } |
| catch(Exception e) |
| { |
| LOG.debug(e); |
| event.setThrowable(e); |
| _channel.getRequest().setAttribute(RequestDispatcher.ERROR_EXCEPTION,e); |
| break; |
| } |
| } |
| } |
| |
| boolean dispatch=false; |
| synchronized (this) |
| { |
| if (_async==Async.EXPIRING) |
| { |
| _async=Async.EXPIRED; |
| if (_state==State.ASYNC_WAIT) |
| { |
| _state=State.ASYNC_WOKEN; |
| dispatch=true; |
| } |
| } |
| } |
| |
| if (dispatch) |
| scheduleDispatch(); |
| } |
| |
| public void complete() |
| { |
| // just like resume, except don't set _dispatched=true; |
| boolean handle=false; |
| synchronized (this) |
| { |
| if (_async!=Async.STARTED && _async!=Async.EXPIRING) |
| throw new IllegalStateException(this.getStatusString()); |
| _async=Async.COMPLETE; |
| if (_state==State.ASYNC_WAIT) |
| { |
| handle=true; |
| _state=State.ASYNC_WOKEN; |
| } |
| } |
| |
| cancelTimeout(); |
| if (handle) |
| { |
| ContextHandler handler=getContextHandler(); |
| if (handler!=null) |
| handler.handle(_channel); |
| else |
| _channel.handle(); |
| } |
| } |
| |
| public void errorComplete() |
| { |
| synchronized (this) |
| { |
| _async=Async.COMPLETE; |
| _event.setDispatchContext(null); |
| _event.setDispatchPath(null); |
| } |
| |
| cancelTimeout(); |
| } |
| |
| protected void completed() |
| { |
| final List<AsyncListener> aListeners; |
| final AsyncContextEvent event; |
| synchronized (this) |
| { |
| switch(_state) |
| { |
| case COMPLETING: |
| _state=State.COMPLETED; |
| aListeners=_asyncListeners; |
| event=_event; |
| break; |
| |
| default: |
| throw new IllegalStateException(this.getStatusString()); |
| } |
| } |
| |
| if (event!=null) |
| { |
| if (aListeners!=null) |
| { |
| if (event.getThrowable()!=null) |
| { |
| event.getSuppliedRequest().setAttribute(RequestDispatcher.ERROR_EXCEPTION,event.getThrowable()); |
| event.getSuppliedRequest().setAttribute(RequestDispatcher.ERROR_MESSAGE,event.getThrowable().getMessage()); |
| } |
| |
| for (AsyncListener listener : aListeners) |
| { |
| try |
| { |
| if (event.getThrowable()!=null) |
| listener.onError(event); |
| else |
| listener.onComplete(event); |
| } |
| catch(Exception e) |
| { |
| LOG.warn(e); |
| } |
| } |
| } |
| |
| event.completed(); |
| } |
| } |
| |
| protected void recycle() |
| { |
| synchronized (this) |
| { |
| switch(_state) |
| { |
| case DISPATCHED: |
| case ASYNC_IO: |
| throw new IllegalStateException(getStatusString()); |
| case UPGRADED: |
| return; |
| default: |
| break; |
| } |
| _asyncListeners=null; |
| _state=State.IDLE; |
| _async=null; |
| _initial=true; |
| _asyncRead=false; |
| _asyncWrite=false; |
| _timeoutMs=DEFAULT_TIMEOUT; |
| cancelTimeout(); |
| _event=null; |
| } |
| } |
| |
| public void upgrade() |
| { |
| synchronized (this) |
| { |
| switch(_state) |
| { |
| case IDLE: |
| case COMPLETED: |
| break; |
| default: |
| throw new IllegalStateException(getStatusString()); |
| } |
| _asyncListeners=null; |
| _state=State.UPGRADED; |
| _async=null; |
| _initial=true; |
| _asyncRead=false; |
| _asyncWrite=false; |
| _timeoutMs=DEFAULT_TIMEOUT; |
| cancelTimeout(); |
| _event=null; |
| } |
| } |
| |
| |
| protected void scheduleDispatch() |
| { |
| _channel.execute(_channel); |
| } |
| |
| protected void scheduleTimeout() |
| { |
| Scheduler scheduler = _channel.getScheduler(); |
| if (scheduler!=null && _timeoutMs>0) |
| _event.setTimeoutTask(scheduler.schedule(_event,_timeoutMs,TimeUnit.MILLISECONDS)); |
| } |
| |
| protected void cancelTimeout() |
| { |
| final AsyncContextEvent event; |
| synchronized (this) |
| { |
| event=_event; |
| } |
| if (event!=null) |
| event.cancelTimeoutTask(); |
| } |
| |
| public boolean isExpired() |
| { |
| synchronized (this) |
| { |
| return _async==Async.EXPIRED; |
| } |
| } |
| |
| public boolean isInitial() |
| { |
| synchronized(this) |
| { |
| return _initial; |
| } |
| } |
| |
| public boolean isSuspended() |
| { |
| synchronized(this) |
| { |
| return _state==State.ASYNC_WAIT || _state==State.DISPATCHED && _async==Async.STARTED; |
| } |
| } |
| |
| boolean isCompleting() |
| { |
| synchronized (this) |
| { |
| return _state==State.COMPLETING; |
| } |
| } |
| |
| boolean isCompleted() |
| { |
| synchronized (this) |
| { |
| return _state == State.COMPLETED; |
| } |
| } |
| |
| public boolean isAsyncStarted() |
| { |
| synchronized (this) |
| { |
| if (_state==State.DISPATCHED) |
| return _async!=null; |
| return _async==Async.STARTED || _async==Async.EXPIRING; |
| } |
| } |
| |
| public boolean isAsync() |
| { |
| synchronized (this) |
| { |
| return !_initial || _async!=null; |
| } |
| } |
| |
| public Request getBaseRequest() |
| { |
| return _channel.getRequest(); |
| } |
| |
| public HttpChannel<?> getHttpChannel() |
| { |
| return _channel; |
| } |
| |
| public ContextHandler getContextHandler() |
| { |
| final AsyncContextEvent event; |
| synchronized (this) |
| { |
| event=_event; |
| } |
| |
| if (event!=null) |
| { |
| Context context=((Context)event.getServletContext()); |
| if (context!=null) |
| return context.getContextHandler(); |
| } |
| return null; |
| } |
| |
| public ServletResponse getServletResponse() |
| { |
| final AsyncContextEvent event; |
| synchronized (this) |
| { |
| event=_event; |
| } |
| if (event!=null && event.getSuppliedResponse()!=null) |
| return event.getSuppliedResponse(); |
| return _channel.getResponse(); |
| } |
| |
| public Object getAttribute(String name) |
| { |
| return _channel.getRequest().getAttribute(name); |
| } |
| |
| public void removeAttribute(String name) |
| { |
| _channel.getRequest().removeAttribute(name); |
| } |
| |
| public void setAttribute(String name, Object attribute) |
| { |
| _channel.getRequest().setAttribute(name,attribute); |
| } |
| |
| public void onReadPossible() |
| { |
| boolean handle=false; |
| |
| synchronized (this) |
| { |
| _asyncRead=true; |
| if (_state==State.ASYNC_WAIT) |
| { |
| _state=State.ASYNC_WOKEN; |
| handle=true; |
| } |
| } |
| |
| if (handle) |
| _channel.execute(_channel); |
| } |
| |
| public void onWritePossible() |
| { |
| boolean handle=false; |
| |
| synchronized (this) |
| { |
| _asyncWrite=true; |
| if (_state==State.ASYNC_WAIT) |
| { |
| _state=State.ASYNC_WOKEN; |
| handle=true; |
| } |
| } |
| |
| if (handle) |
| _channel.execute(_channel); |
| } |
| |
| } |