Author: remm Date: Tue Oct 31 17:45:30 2017 New Revision: 1813918 URL: http://svn.apache.org/viewvc?rev=1813918&view=rev Log: Add sendfile support for HTTP/2 NIO2 using mapped files. Performance seems to be improved with h2load.
Modified: tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java tomcat/trunk/java/org/apache/coyote/http2/Stream.java tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java tomcat/trunk/webapps/docs/changelog.xml Modified: tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java?rev=1813918&r1=1813917&r2=1813918&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java (original) +++ tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java Tue Oct 31 17:45:30 2017 @@ -16,9 +16,14 @@ */ package org.apache.coyote.http2; +import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; import java.nio.channels.CompletionHandler; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -27,8 +32,12 @@ import org.apache.coyote.Adapter; import org.apache.coyote.ProtocolException; import org.apache.coyote.Request; import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.net.SendfileState; import org.apache.tomcat.util.net.SocketWrapperBase; import org.apache.tomcat.util.net.SocketWrapperBase.BlockingMode; +import org.apache.tomcat.util.net.SocketWrapperBase.CompletionCheck; +import org.apache.tomcat.util.net.SocketWrapperBase.CompletionHandlerCall; +import org.apache.tomcat.util.net.SocketWrapperBase.CompletionState; public class Http2AsyncUpgradeHandler extends Http2UpgradeHandler { @@ -69,6 +78,11 @@ public class Http2AsyncUpgradeHandler ex } @Override + boolean hasAsyncIO() { + return true; + } + + @Override protected void writeSettings() { // Send the initial settings frame socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), @@ -230,6 +244,159 @@ public class Http2AsyncUpgradeHandler ex } } + @Override + protected void processWrites() throws IOException { + if (socketWrapper.isWritePending()) { + socketWrapper.registerWriteInterest(); + } + } + + @Override + protected SendfileState processSendfile(Stream stream) { + String fileName = (String) stream.getCoyoteRequest().getAttribute( + org.apache.coyote.Constants.SENDFILE_FILENAME_ATTR); + if (fileName != null) { + java.nio.file.Path path = new File(fileName).toPath(); + SendfileData sendfile = new SendfileData(); + sendfile.pos = ((Long) stream.getCoyoteRequest().getAttribute( + org.apache.coyote.Constants.SENDFILE_FILE_START_ATTR)).longValue(); + sendfile.end = ((Long) stream.getCoyoteRequest().getAttribute( + org.apache.coyote.Constants.SENDFILE_FILE_END_ATTR)).longValue(); + sendfile.left = sendfile.end - sendfile.pos; + try { + try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) { + sendfile.mappedBuffer = channel.map(MapMode.READ_ONLY, sendfile.pos, sendfile.end - sendfile.pos); + sendfile.stream = stream; + } + // Reserve as much as possible right away + int reservation = (sendfile.end - sendfile.pos > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) (sendfile.end - sendfile.pos); + sendfile.streamReservation = stream.reserveWindowSize(reservation, true); + sendfile.connectionReservation = reserveWindowSize(stream, sendfile.streamReservation); + } catch (IOException e) { + return SendfileState.ERROR; + } + // Actually perform the write + int frameSize = Integer.min(getMaxFrameSize(), sendfile.connectionReservation); + boolean finished = (frameSize == sendfile.left) && stream.getCoyoteResponse().getTrailerFields() == null; + + // Need to check this now since sending end of stream will change this. + boolean writeable = stream.canWrite(); + byte[] header = new byte[9]; + ByteUtil.setThreeBytes(header, 0, frameSize); + header[3] = FrameType.DATA.getIdByte(); + if (finished) { + header[4] = FLAG_END_OF_STREAM; + stream.sentEndOfStream(); + if (!stream.isActive()) { + activeRemoteStreamCount.decrementAndGet(); + } + } + if (writeable) { + ByteUtil.set31Bits(header, 5, stream.getIdentifier().intValue()); + sendfile.mappedBuffer.limit(sendfile.mappedBuffer.position() + frameSize); + socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), + TimeUnit.MILLISECONDS, sendfile, COMPLETE_WRITE_WITH_COMPLETION, + new SendfileCompletionHandler(), ByteBuffer.wrap(header), sendfile.mappedBuffer); + try { + handleAsyncException(); + } catch (IOException e) { + return SendfileState.ERROR; + } + } + return SendfileState.PENDING; + } else { + return SendfileState.DONE; + } + } + + private static final CompletionCheck COMPLETE_WRITE_WITH_COMPLETION = new CompletionCheck() { + @Override + public CompletionHandlerCall callHandler(CompletionState state, ByteBuffer[] buffers, + int offset, int length) { + for (int i = 0; i < length; i++) { + if (buffers[offset + i].remaining() > 0) { + return CompletionHandlerCall.CONTINUE; + } + } + return CompletionHandlerCall.DONE; + } + }; + + protected class SendfileCompletionHandler implements CompletionHandler<Long, SendfileData> { + @Override + public void completed(Long nBytes, SendfileData sendfile) { + long bytesWritten = nBytes.longValue() - 9; + sendfile.left -= bytesWritten; + if (sendfile.left == 0) { + try { + sendfile.stream.getOutputBuffer().close(); + } catch (IOException e) { + failed(e, sendfile); + } + return; + } + sendfile.streamReservation -= bytesWritten; + sendfile.connectionReservation -= bytesWritten; + sendfile.pos += bytesWritten; + try { + if (sendfile.connectionReservation == 0) { + if (sendfile.streamReservation == 0) { + int reservation = (sendfile.end - sendfile.pos > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) (sendfile.end - sendfile.pos); + sendfile.streamReservation = sendfile.stream.reserveWindowSize(reservation, true); + } + sendfile.connectionReservation = reserveWindowSize(sendfile.stream, sendfile.streamReservation); + } + } catch (IOException e) { + failed (e, sendfile); + return; + } + int frameSize = Integer.min(getMaxFrameSize(), sendfile.streamReservation); + boolean finished = (frameSize == sendfile.left) && sendfile.stream.getCoyoteResponse().getTrailerFields() == null; + + // Need to check this now since sending end of stream will change this. + boolean writeable = sendfile.stream.canWrite(); + byte[] header = new byte[9]; + ByteUtil.setThreeBytes(header, 0, frameSize); + header[3] = FrameType.DATA.getIdByte(); + if (finished) { + header[4] = FLAG_END_OF_STREAM; + sendfile.stream.sentEndOfStream(); + if (!sendfile.stream.isActive()) { + activeRemoteStreamCount.decrementAndGet(); + } + } + if (writeable) { + ByteUtil.set31Bits(header, 5, sendfile.stream.getIdentifier().intValue()); + sendfile.mappedBuffer.limit(sendfile.mappedBuffer.position() + frameSize); + socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), + TimeUnit.MILLISECONDS, sendfile, COMPLETE_WRITE_WITH_COMPLETION, + this, ByteBuffer.wrap(header), sendfile.mappedBuffer); + try { + handleAsyncException(); + } catch (IOException e) { + failed(e, sendfile); + } + } + } + + @Override + public void failed(Throwable t, SendfileData sendfile) { + applicationErrorCompletion.failed(t, null); + } + } + + protected class SendfileData { + protected Stream stream; + // Note: a mapped buffer is a special construct with an underlying file + // that doesn't need to be closed + protected MappedByteBuffer mappedBuffer; + protected int frameSize; + protected long left; + protected int streamReservation; + protected int connectionReservation; + protected long pos; + protected long end; + } protected class AsyncPingManager extends PingManager { @Override Modified: tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java?rev=1813918&r1=1813917&r2=1813918&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java (original) +++ tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java Tue Oct 31 17:45:30 2017 @@ -50,6 +50,7 @@ import org.apache.tomcat.util.codec.bina import org.apache.tomcat.util.http.MimeHeaders; import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SendfileState; import org.apache.tomcat.util.net.SocketEvent; import org.apache.tomcat.util.net.SocketWrapperBase; import org.apache.tomcat.util.res.StringManager; @@ -699,7 +700,12 @@ class Http2UpgradeHandler extends Abstra } - private void processWrites() throws IOException { + boolean hasAsyncIO() { + return false; + } + + + protected void processWrites() throws IOException { synchronized (socketWrapper) { if (socketWrapper.flush(false)) { socketWrapper.registerWriteInterest(); @@ -799,6 +805,10 @@ class Http2UpgradeHandler extends Abstra } + protected SendfileState processSendfile(Stream stream) { + return SendfileState.DONE; + } + private synchronized Set<AbstractStream> releaseBackLog(int increment) { Set<AbstractStream> result = new HashSet<>(); if (backLogSize < increment) { Modified: tomcat/trunk/java/org/apache/coyote/http2/Stream.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/Stream.java?rev=1813918&r1=1813917&r2=1813918&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http2/Stream.java (original) +++ tomcat/trunk/java/org/apache/coyote/http2/Stream.java Tue Oct 31 17:45:30 2017 @@ -104,8 +104,7 @@ class Stream extends AbstractStream impl // TODO Assuming the body has been read at this point is not valid state.receivedEndOfStream(); } - // No sendfile for HTTP/2 (it is enabled by default in the request) - this.coyoteRequest.setSendfile(false); + this.coyoteRequest.setSendfile(handler.hasAsyncIO()); this.coyoteResponse.setOutputBuffer(outputBuffer); this.coyoteRequest.setResponse(coyoteResponse); this.coyoteRequest.protocol().setString("HTTP/2.0"); @@ -198,7 +197,7 @@ class Stream extends AbstractStream impl } - private final synchronized int reserveWindowSize(int reservation, boolean block) + final synchronized int reserveWindowSize(int reservation, boolean block) throws IOException { long windowSize = getWindowSize(); while (windowSize < 1) { Modified: tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java?rev=1813918&r1=1813917&r2=1813918&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java (original) +++ tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java Tue Oct 31 17:45:30 2017 @@ -30,6 +30,7 @@ import org.apache.juli.logging.LogFactor import org.apache.tomcat.util.buf.ByteChunk; import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; import org.apache.tomcat.util.net.DispatchType; +import org.apache.tomcat.util.net.SendfileState; import org.apache.tomcat.util.net.SocketEvent; import org.apache.tomcat.util.net.SocketWrapperBase; import org.apache.tomcat.util.res.StringManager; @@ -41,6 +42,7 @@ class StreamProcessor extends AbstractPr private final Http2UpgradeHandler handler; private final Stream stream; + private SendfileState sendfileState = null; StreamProcessor(Http2UpgradeHandler handler, Stream stream, Adapter adapter, @@ -102,7 +104,10 @@ class StreamProcessor extends AbstractPr @Override protected final void finishResponse() throws IOException { - stream.getOutputBuffer().close(); + sendfileState = handler.processSendfile(stream); + if (!(sendfileState == SendfileState.PENDING)) { + stream.getOutputBuffer().close(); + } } @@ -261,7 +266,9 @@ class StreamProcessor extends AbstractPr setErrorState(ErrorState.CLOSE_NOW, e); } - if (getErrorState().isError()) { + if (sendfileState == SendfileState.PENDING) { + return SocketState.SENDFILE; + } else if (getErrorState().isError()) { action(ActionCode.CLOSE, null); request.updateCounters(); return SocketState.CLOSED; Modified: tomcat/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1813918&r1=1813917&r2=1813918&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/changelog.xml (original) +++ tomcat/trunk/webapps/docs/changelog.xml Tue Oct 31 17:45:30 2017 @@ -77,6 +77,9 @@ <fix> Improve NIO2 syncing for async IO operations. (remm) </fix> + <add> + Sendfile support for HTTP/2 and NIO2. (remm) + </add> </changelog> </subsection> <subsection name="Jasper"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org