/* * Copyright 2010 Richard Zschech. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package net.zschech.gwt.comet.server.impl; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.nio.channels.CancelledKeyException; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import com.sun.grizzly.comet.CometContext; import com.sun.grizzly.comet.CometEngine; import com.sun.grizzly.comet.CometEvent; import com.sun.grizzly.comet.CometHandler; import com.sun.grizzly.comet.CometWriter; public abstract class AbstractGrizzlyAsyncServlet extends NonBlockingAsyncServlet { private final Field socketChannelField; private CometEngine cometEngine; private CometContext<?> cometContext; private Selector selector; public AbstractGrizzlyAsyncServlet() throws SecurityException, NoSuchFieldException { socketChannelField = CometWriter.class.getDeclaredField("socketChannel"); socketChannelField.setAccessible(true); } @Override public void init(ServletContext context) throws ServletException { super.init(context); try { cometEngine = CometEngine.getEngine(); cometContext = cometEngine.register(context.getContextPath()); cometContext.setExpirationDelay(-1); cometContext.setBlockingNotification(true); } catch (IllegalStateException e) { throw new ServletException(e.getMessage()); } } @Override protected void shutdown() { cometEngine.unregister(getServletContext().getContextPath()); super.shutdown(); } @Override public OutputStream getOutputStream(OutputStream outputStream) { return new GrizzyOutputStream(outputStream); } @Override public Object suspend(CometServletResponseImpl response, CometSessionImpl session, HttpServletRequest request) throws IOException { assert Thread.holdsLock(response); assert session == null || !Thread.holdsLock(session); // Unfortunately we have to flush the response line and headers before switching to async mode :-( response.flush(); initSelector(response); CometHandlerImpl handler = new CometHandlerImpl(response, session); try { cometContext.addCometHandler(handler); } catch (IllegalStateException e) { throw new IOException(e.getMessage()); } if (session != null) { GrizzyOutputStream asyncOutputStream = (GrizzyOutputStream) response.getAsyncOutputStream(); asyncOutputStream.wrapped = new GrizzyAsyncBufferOutputStream(handler); } return handler; } @Override public void terminate(CometServletResponseImpl response, CometSessionImpl session, boolean serverInitiated, Object suspendInfo) { assert Thread.holdsLock(response); assert session == null || !Thread.holdsLock(session); final CometHandlerImpl handler = (CometHandlerImpl) suspendInfo; if (serverInitiated) { if (session != null) { handler.registerAsyncWrite(); } else { cometContext.resumeCometHandler(handler); } } } @Override public void invalidate(CometSessionImpl session) { enqueued(session); } @Override public void enqueued(CometSessionImpl session) { CometServletResponseImpl response = session.getResponse(); if (response != null) { if (response.setProcessing(true)) { synchronized (response) { if (!response.isTerminated()) { Object suspendInfo = response.getSuspendInfo(); if (suspendInfo != null) { CometHandlerImpl handler = (CometHandlerImpl) suspendInfo; handler.registerAsyncWrite(); } } } } } } private Selector initSelector(CometServletResponseImpl response) { if (selector != null) { return selector; } selector = getSelector(response); return selector; } protected abstract Selector getSelector(CometServletResponseImpl response); private class CometHandlerImpl implements CometHandler<Object> { private final CometServletResponseImpl response; private final CometSessionImpl session; private final boolean chunked; private volatile boolean active; private volatile ByteBuffer buffer; private volatile AtomicInteger activeFailureCount = new AtomicInteger(); private AtomicBoolean registered = new AtomicBoolean(); public CometHandlerImpl(CometServletResponseImpl response, CometSessionImpl session) { this.response = response; this.session = session; this.chunked = response.getResponse().containsHeader("Transfer-Encoding"); } @Override public void attach(Object attachment) { } public void registerAsyncWrite() { assert Thread.holdsLock(response); // Unfortunately Grizzly does not setup it CometContext with the CometHandler immediately so we have to schedule it // until it is active. See CometEngine.handle(AsyncProcessorTask) executeServlet() is called before cometContext.addActiveHandler(cometTask) if (!active && !response.isTerminated() && activeFailureCount.get() < 100) { cometEngine.getThreadPool().execute(new Runnable() { @Override public void run() { try { cometContext.registerAsyncWrite(CometHandlerImpl.this); selector.wakeup(); if (activeFailureCount.getAndSet(0) != 0) { log("Comet handler " + Integer.toHexString(hashCode()) + " active"); } } catch (IllegalStateException e) { // log("Comet handler " + Integer.toHexString(hashCode()) + " not active yet, retrying: " + e.getMessage()); activeFailureCount.incrementAndGet(); registerAsyncWrite(); } } }); } else { if (registered.compareAndSet(false, true)) { try { cometContext.registerAsyncWrite(this); selector.wakeup(); if (activeFailureCount.getAndSet(0) != 0) { log("Comet handler " + Integer.toHexString(hashCode()) + " active"); } } catch (CancelledKeyException e) { if (!response.isTerminated()) { response.setTerminated(true); } } catch (IllegalStateException e) { // log("Comet handler " + Integer.toHexString(hashCode()) + " not active yet, giving up: " + e.getMessage()); } } } } @Override public void onInitialize(CometEvent event) throws IOException { synchronized (response) { if (!session.isEmpty()) { registerAsyncWrite(); } } } @Override public void onInterrupt(CometEvent event) throws IOException { terminate(); } @Override public void onTerminate(CometEvent event) throws IOException { terminate(); } private void terminate() { synchronized (response) { if (!response.isTerminated()) { response.setTerminated(false); } } } @Override public void onEvent(CometEvent event) throws IOException { active = true; if (event.getType() == CometEvent.WRITE) { CometWriter writer = (CometWriter) event.attachment(); try { // Unfortunately Grizzly's CometWriter assumes that the transfer encoding is going to be chunked and that // only one chunk is going to be written to the response so we have to write to the SocketChanel directly. // https://grizzly.dev.java.net/issues/show_bug.cgi?id=791 SocketChannel socketChannel = (SocketChannel) socketChannelField.get(writer); GrizzyAsyncBufferOutputStream output = (GrizzyAsyncBufferOutputStream) ((GrizzyOutputStream) response.getAsyncOutputStream()).getWrapped(); while (true) { if (buffer == null || !buffer.hasRemaining()) { synchronized (response) { int count; while ((count = output.getCount()) == 0 && !session.isEmpty()) { session.writeQueue(response, true); } if (count == 0) { buffer = null; break; } byte[] bytes = output.getBytes(); if (chunked) { byte[] chunkLength = Integer.toHexString(count).getBytes("ASCII"); buffer = ByteBuffer.allocate(chunkLength.length + count + 4); buffer.put(chunkLength); buffer.put((byte) '\r'); buffer.put((byte) '\n'); buffer.put(bytes, 0, count); buffer.put((byte) '\r'); buffer.put((byte) '\n'); buffer.flip(); } else { buffer = ByteBuffer.wrap(bytes, 0, count); } output.clear(); } } int write = socketChannel.write(buffer); if (write <= 0) { // can't write any more cometContext.registerAsyncWrite(this); break; } else if (!buffer.hasRemaining()) { buffer = null; } } if (buffer == null) { if (response.isTerminated()) { cometContext.resumeCometHandler(this); } else { registered.set(false); response.setProcessing(false); } } } catch (IllegalArgumentException e) { log("Error accessing socket chanel", e); } catch (IllegalAccessException e) { log("Error accessing socket chanel", e); } } } } private static class GrizzyOutputStream extends OutputStream { private volatile OutputStream wrapped; public GrizzyOutputStream(OutputStream wrapped) { this.wrapped = wrapped; } @Override public void write(byte[] b) throws IOException { wrapped.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { wrapped.write(b, off, len); } @Override public void write(int b) throws IOException { wrapped.write(b); } @Override public void close() throws IOException { wrapped.close(); } @Override public void flush() throws IOException { wrapped.flush(); } public OutputStream getWrapped() { return wrapped; } } private static class GrizzyAsyncBufferOutputStream extends ByteArrayOutputStream { private final CometHandlerImpl handler; private GrizzyAsyncBufferOutputStream(CometHandlerImpl handler) { this.handler = handler; } @Override public void write(byte[] b) { write(b, 0, b.length); } @Override public void write(byte[] b, int off, int len) { super.write(b, off, len); handler.registerAsyncWrite(); } @Override public void write(int b) { super.write(b); handler.registerAsyncWrite(); } public byte[] getBytes() { return buf; } public int getCount() { return count; } public void clear() { count = 0; buf = new byte[buf.length]; } } }