diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java index dfba6a381371..7ab9a9126303 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java @@ -20,6 +20,7 @@ import java.util.function.Supplier; import org.eclipse.jetty.http.HttpTokens.EndOfContent; +import org.eclipse.jetty.io.Content; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Index; import org.eclipse.jetty.util.StringUtil; @@ -246,7 +247,7 @@ public Result generateRequest(MetaData.Request info, ByteBuffer header, ByteBuff case COMMITTED: { - return committed(chunk, content, last); + return committed(info, chunk, content, last); } case COMPLETING: @@ -268,11 +269,14 @@ public Result generateRequest(MetaData.Request info, ByteBuffer header, ByteBuff } } - private Result committed(ByteBuffer chunk, ByteBuffer content, boolean last) + private Result committed(MetaData info, ByteBuffer chunk, ByteBuffer content, boolean last) { - int len = BufferUtil.length(content); + long len = BufferUtil.length(content); + Content.Source source = info.getContentSource(); - // handle the content. + // Handle the content. + if (len == 0 && source != null) + len = source.getLength(); if (len > 0) { if (isChunking()) @@ -401,15 +405,18 @@ else if (status == HttpStatus.NO_CONTENT_204 || status == HttpStatus.NOT_MODIFIE generateHeaders(header, content, last); - // handle the content. - int len = BufferUtil.length(content); + // Handle the given content. + long len = BufferUtil.length(content); + Content.Source source = info.getContentSource(); + if (len == 0 && source != null) + len = source.getLength(); if (len > 0) { _contentPrepared += len; if (isChunking() && !head) prepareChunk(header, len); } - _state = last ? State.COMPLETING : State.COMMITTED; + _state = last && source == null ? State.COMPLETING : State.COMMITTED; } catch (BufferOverflowException e) { @@ -432,7 +439,7 @@ else if (status == HttpStatus.NO_CONTENT_204 || status == HttpStatus.NOT_MODIFIE case COMMITTED: { - return committed(chunk, content, last); + return committed(info, chunk, content, last); } case COMPLETING_1XX: @@ -474,7 +481,7 @@ public void servletUpgrade() startTunnel(); } - private void prepareChunk(ByteBuffer chunk, int remaining) + private void prepareChunk(ByteBuffer chunk, long remaining) { // if we need CRLF add this to header if (_needCRLF) @@ -483,7 +490,8 @@ private void prepareChunk(ByteBuffer chunk, int remaining) // Add the chunk size to the header if (remaining > 0) { - BufferUtil.putHexInt(chunk, remaining); + // TODO: we need a long as required by RFC 9110. + BufferUtil.putHexInt(chunk, (int)remaining); BufferUtil.putCRLF(chunk); _needCRLF = true; } diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java index b7e3799a078c..8ce7d623417f 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java @@ -17,6 +17,7 @@ import java.util.Objects; import java.util.function.Supplier; +import org.eclipse.jetty.io.Content; import org.eclipse.jetty.util.NanoTime; /** @@ -25,7 +26,7 @@ *

Specific HTTP response information is captured by {@link Response}.

*

HTTP trailers information is captured by {@link MetaData}.

*/ -public class MetaData implements Iterable +public class MetaData implements Iterable, Content.Source.Seekable.Aware { /** *

Returns whether the given HTTP request method and HTTP response status code @@ -44,6 +45,7 @@ public static boolean isTunnel(String method, int status) private final HttpFields _httpFields; private final long _contentLength; private final Supplier _trailers; + private Content.Source.Seekable _source; public MetaData(HttpVersion version, HttpFields fields) { @@ -105,6 +107,18 @@ public Supplier getTrailersSupplier() return _trailers; } + @Override + public Content.Source.Seekable getContentSource() + { + return _source; + } + + @Override + public void setContentSource(Content.Source.Seekable source) + { + _source = source; + } + /** * Get the length of the content in bytes. * @return the length of the content in bytes diff --git a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java index 7b1573bf1225..cfc95cb6a809 100644 --- a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java +++ b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java @@ -1574,7 +1574,7 @@ public Frame frame() public abstract int getFrameBytesGenerated(); - public int getDataBytesRemaining() + public long getDataBytesRemaining() { return 0; } @@ -1747,7 +1747,7 @@ private class DataEntry extends Entry { private int frameBytes; private int dataBytes; - private int dataRemaining; + private long dataRemaining; private DataEntry(DataFrame frame, HTTP2Stream stream, Callback callback) { @@ -1757,7 +1757,7 @@ private DataEntry(DataFrame frame, HTTP2Stream stream, Callback callback) // of data frames that cannot be completely written due to // the flow control window exhausting, since in that case // we would have to count the padding only once. - dataRemaining = frame.remaining(); + dataRemaining = frame.bytesLeft(); } @Override @@ -1767,7 +1767,7 @@ public int getFrameBytesGenerated() } @Override - public int getDataBytesRemaining() + public long getDataBytesRemaining() { return dataRemaining; } @@ -1775,7 +1775,7 @@ public int getDataBytesRemaining() @Override public boolean generate(RetainableByteBuffer.Mutable accumulator) { - int dataRemaining = getDataBytesRemaining(); + long dataRemaining = getDataBytesRemaining(); int sessionSendWindow = getSendWindow(); int streamSendWindow = stream.updateSendWindow(0); @@ -1783,9 +1783,9 @@ public boolean generate(RetainableByteBuffer.Mutable accumulator) if (window <= 0 && dataRemaining > 0) return false; - int length = Math.min(dataRemaining, window); + int length = (int)Math.min(dataRemaining, window); - // Only one DATA frame is generated. + // Only one DATA frame is generated to support interleaving. DataFrame dataFrame = (DataFrame)frame; int frameBytes = generator.data(accumulator, dataFrame, length); this.frameBytes += frameBytes; diff --git a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/frames/DataFrame.java b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/frames/DataFrame.java index 561edf51169b..172c822c48f6 100644 --- a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/frames/DataFrame.java +++ b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/frames/DataFrame.java @@ -15,9 +15,12 @@ import java.nio.ByteBuffer; +import org.eclipse.jetty.io.Content; + public class DataFrame extends StreamFrame { private final ByteBuffer data; + private final Content.Source.Seekable source; private final boolean endStream; private final int length; private final int padding; @@ -29,15 +32,26 @@ public DataFrame(ByteBuffer data, boolean endStream) public DataFrame(int streamId, ByteBuffer data, boolean endStream) { - this(streamId, data, endStream, 0); + this(streamId, data, null, endStream, 0); + } + + public DataFrame(int streamId, Content.Source.Seekable source, boolean endStream) + { + this(streamId, Content.Sink.TRANSFER_TO, source, endStream, 0); } public DataFrame(int streamId, ByteBuffer data, boolean endStream, int padding) + { + this(streamId, data, null, endStream, padding); + } + + private DataFrame(int streamId, ByteBuffer data, Content.Source.Seekable source, boolean endStream, int padding) { super(FrameType.DATA, streamId); this.data = data; + this.source = source; this.endStream = endStream; - this.length = data.remaining(); + this.length = remaining(); this.padding = padding; } @@ -46,6 +60,11 @@ public ByteBuffer getByteBuffer() return data; } + public Content.Source.Seekable getContentSource() + { + return source; + } + public boolean isEndStream() { return endStream; @@ -56,7 +75,15 @@ public boolean isEndStream() */ public int remaining() { - return data.remaining(); + return Math.toIntExact(bytesLeft()); + } + + /** + * @return the number of data bytes remaining, as a {@code long} + */ + public long bytesLeft() + { + return data == Content.Sink.TRANSFER_TO ? source.remaining() : data.remaining(); } /** diff --git a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/generator/DataGenerator.java b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/generator/DataGenerator.java index fe894eae2ace..dccb0d0bf75d 100644 --- a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/generator/DataGenerator.java +++ b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/generator/DataGenerator.java @@ -13,13 +13,16 @@ package org.eclipse.jetty.http2.generator; +import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import org.eclipse.jetty.http2.Flags; import org.eclipse.jetty.http2.frames.DataFrame; import org.eclipse.jetty.http2.frames.Frame; import org.eclipse.jetty.http2.frames.FrameType; +import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.RetainableByteBuffer; +import org.eclipse.jetty.util.Callback; public class DataGenerator { @@ -32,14 +35,19 @@ public DataGenerator(HeaderGenerator headerGenerator) public int generate(RetainableByteBuffer.Mutable accumulator, DataFrame frame, int maxLength) { - return generateData(accumulator, frame.getStreamId(), frame.getByteBuffer(), frame.isEndStream(), maxLength); + int streamId = frame.getStreamId(); + if (streamId < 0) + throw new IllegalArgumentException("Invalid stream id: " + streamId); + + ByteBuffer byteBuffer = frame.getByteBuffer(); + if (byteBuffer == Content.Sink.TRANSFER_TO) + return generateData(accumulator, streamId, frame.getContentSource(), frame.isEndStream(), maxLength); + else + return generateData(accumulator, streamId, byteBuffer, frame.isEndStream(), maxLength); } public int generateData(RetainableByteBuffer.Mutable accumulator, int streamId, ByteBuffer data, boolean last, int maxLength) { - if (streamId < 0) - throw new IllegalArgumentException("Invalid stream id: " + streamId); - int dataLength = data.remaining(); int maxFrameSize = headerGenerator.getMaxFrameSize(); int length = Math.min(dataLength, Math.min(maxFrameSize, maxLength)); @@ -49,12 +57,9 @@ public int generateData(RetainableByteBuffer.Mutable accumulator, int streamId, } else { - int limit = data.limit(); - int newLimit = data.position() + length; - data.limit(newLimit); - ByteBuffer slice = data.slice(); - data.position(newLimit); - data.limit(limit); + int position = data.position(); + ByteBuffer slice = data.slice(position, length); + data.position(position + length); generateFrame(accumulator, streamId, slice, false); } return Frame.HEADER_LENGTH + length; @@ -73,4 +78,60 @@ private void generateFrame(RetainableByteBuffer.Mutable accumulator, int streamI if (data.remaining() > 0) accumulator.add(data); } + + public int generateData(RetainableByteBuffer.Mutable accumulator, int streamId, Content.Source.Seekable source, boolean last, int maxLength) + { + long dataLength = source.remaining(); + int maxFrameSize = headerGenerator.getMaxFrameSize(); + int length = (int)Math.min(dataLength, Math.min(maxFrameSize, maxLength)); + + last = last && length == dataLength; + + int flags = Flags.NONE; + if (last) + flags |= Flags.END_STREAM; + + headerGenerator.generate(accumulator, FrameType.DATA, Frame.HEADER_LENGTH + length, length, flags, streamId); + Content.Source.Seekable slice = source.slice(source.position(), length); + source.position(source.position() + length); + accumulator.add(new TransferableRetainableByteBuffer(slice)); + + return Frame.HEADER_LENGTH + length; + } + + private static class TransferableRetainableByteBuffer implements RetainableByteBuffer + { + private final Content.Source.Seekable source; + + public TransferableRetainableByteBuffer(Content.Source.Seekable source) + { + this.source = source; + } + + @Override + public ByteBuffer getByteBuffer() throws BufferOverflowException + { + return Content.Sink.TRANSFER_TO; + } + + @Override + public long size() + { + return source.remaining(); + } + + @Override + public int remaining() + { + return Math.toIntExact(size()); + } + + @Override + public void writeTo(Content.Sink sink, boolean last, Callback callback) + { + // The "last" parameter is not used here, since "last-ness" has + // already been encoded by the generator in DATA frame header bytes. + Content.transfer(source, sink, callback); + } + } } diff --git a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/internal/HTTP2Flusher.java b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/internal/HTTP2Flusher.java index 16f143843a20..5c4256d82bf4 100644 --- a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/internal/HTTP2Flusher.java +++ b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/internal/HTTP2Flusher.java @@ -63,7 +63,7 @@ public HTTP2Flusher(HTTP2Session session) this.session = session; EndPoint endPoint = session.getEndPoint(); boolean direct = endPoint != null && endPoint.getConnection() instanceof HTTP2Connection http2Connection && http2Connection.isUseOutputDirectByteBuffers(); - this.accumulator = new RetainableByteBuffer.DynamicCapacity(session.getGenerator().getByteBufferPool(), direct, -1); + this.accumulator = new RetainableByteBuffer.DynamicCapacity(session.getGenerator().getByteBufferPool(), direct, -1, -1, 0); } @Override diff --git a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HttpStreamOverHTTP2.java b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HttpStreamOverHTTP2.java index 9b0e42ccc491..136ba62bef4b 100644 --- a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HttpStreamOverHTTP2.java +++ b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HttpStreamOverHTTP2.java @@ -300,13 +300,13 @@ public void prepareResponse(HttpFields.Mutable headers) public void send(MetaData.Request request, MetaData.Response response, boolean last, ByteBuffer byteBuffer, Callback callback) { ByteBuffer content = byteBuffer != null ? byteBuffer : BufferUtil.EMPTY_BUFFER; - if (response != null) + if (_responseMetaData == null) sendHeaders(request, response, content, last, callback); else - sendContent(request, content, last, callback); + sendContent(request, response, content, last, callback); } - private void sendHeaders(MetaData.Request request, MetaData.Response response, ByteBuffer content, boolean last, Callback callback) + private void sendHeaders(MetaData.Request request, MetaData.Response response, ByteBuffer byteBuffer, boolean last, Callback callback) { _responseMetaData = response; @@ -315,7 +315,9 @@ private void sendHeaders(MetaData.Request request, MetaData.Response response, B HeadersFrame trailersFrame = null; boolean isHeadRequest = HttpMethod.HEAD.is(request.getMethod()); - boolean hasContent = BufferUtil.hasContent(content) && !isHeadRequest; + boolean transferable = byteBuffer == Content.Sink.TRANSFER_TO; + long contentLength = transferable ? response.getContentSource().getLength() : BufferUtil.length(byteBuffer); + boolean hasContent = contentLength > 0 && !isHeadRequest; int streamId = _stream.getId(); if (HttpStatus.isInterim(response.getStatus())) { @@ -334,20 +336,19 @@ private void sendHeaders(MetaData.Request request, MetaData.Response response, B committed = true; if (last) { - long realContentLength = BufferUtil.length(content); - long contentLength = response.getContentLength(); - if (contentLength < 0) + long responseContentLength = response.getContentLength(); + if (responseContentLength < 0) { _responseMetaData = new MetaData.Response( response.getStatus(), response.getReason(), response.getHttpVersion(), response.getHttpFields(), - realContentLength, + contentLength, response.getTrailersSupplier() ); } - else if (hasContent && contentLength != realContentLength) + else if (hasContent && responseContentLength != contentLength) { - callback.failed(new HttpException.RuntimeException(HttpStatus.INTERNAL_SERVER_ERROR_500, String.format("Incorrect Content-Length %d!=%d", contentLength, realContentLength))); + callback.failed(new HttpException.RuntimeException(HttpStatus.INTERNAL_SERVER_ERROR_500, String.format("Incorrect Content-Length %d!=%d", responseContentLength, contentLength))); return; } } @@ -360,17 +361,17 @@ else if (hasContent && contentLength != realContentLength) HttpFields trailers = retrieveTrailers(); if (trailers == null) { - dataFrame = new DataFrame(streamId, content, true); + dataFrame = transferable ? new DataFrame(streamId, response.getContentSource(), true) : new DataFrame(streamId, byteBuffer, true); } else { - dataFrame = new DataFrame(streamId, content, false); + dataFrame = transferable ? new DataFrame(streamId, response.getContentSource(), false) : new DataFrame(streamId, byteBuffer, false); trailersFrame = new HeadersFrame(streamId, new MetaData(HttpVersion.HTTP_2, trailers), null, true); } } else { - dataFrame = new DataFrame(streamId, content, false); + dataFrame = transferable ? new DataFrame(streamId, response.getContentSource(), false) : new DataFrame(streamId, byteBuffer, false); } } else @@ -413,27 +414,30 @@ else if (hasContent && contentLength != realContentLength) _stream.send(new HTTP2Stream.FrameList(headersFrame, dataFrame, trailersFrame), callback); } - private void sendContent(MetaData.Request request, ByteBuffer content, boolean last, Callback callback) + private void sendContent(MetaData.Request request, MetaData.Response response, ByteBuffer byteBuffer, boolean last, Callback callback) { boolean isHeadRequest = HttpMethod.HEAD.is(request.getMethod()); - boolean hasContent = BufferUtil.hasContent(content) && !isHeadRequest; - if (hasContent || (last && !isTunnel(request, _responseMetaData))) + boolean transferable = byteBuffer == Content.Sink.TRANSFER_TO; + Content.Source.Seekable source = response.getContentSource(); + long contentLength = transferable ? source.getLength() : BufferUtil.length(byteBuffer); + boolean hasContent = contentLength > 0 && !isHeadRequest; + if (hasContent || (last && !isTunnel(request, response))) { if (!hasContent) - content = BufferUtil.EMPTY_BUFFER; + byteBuffer = BufferUtil.EMPTY_BUFFER; if (last) { HttpFields trailers = retrieveTrailers(); if (trailers == null) { - sendDataFrame(content, true, true, callback); + sendDataFrame(byteBuffer, source, true, true, callback); } else { if (hasContent) { SendTrailers sendTrailers = new SendTrailers(callback, trailers); - sendDataFrame(content, true, false, sendTrailers); + sendDataFrame(byteBuffer, source, true, false, sendTrailers); } else { @@ -443,7 +447,7 @@ private void sendContent(MetaData.Request request, ByteBuffer content, boolean l } else { - sendDataFrame(content, false, false, callback); + sendDataFrame(byteBuffer, source, false, false, callback); } } else @@ -553,15 +557,17 @@ public Runnable onPushRequest(MetaData.Request request) } } - private void sendDataFrame(ByteBuffer content, boolean lastContent, boolean endStream, Callback callback) + private void sendDataFrame(ByteBuffer byteBuffer, Content.Source.Seekable source, boolean lastContent, boolean endStream, Callback callback) { + boolean transferable = byteBuffer == Content.Sink.TRANSFER_TO; if (LOG.isDebugEnabled()) { LOG.debug("HTTP2 Response #{}/{}: {} content bytes{}", _stream.getId(), Integer.toHexString(_stream.getSession().hashCode()), - content.remaining(), lastContent ? " (last chunk)" : ""); + transferable ? source.remaining() : byteBuffer.remaining(), + lastContent ? " (last chunk)" : ""); } - DataFrame frame = new DataFrame(_stream.getId(), content, endStream); + DataFrame frame = transferable ? new DataFrame(_stream.getId(), source, endStream) : new DataFrame(_stream.getId(), byteBuffer, endStream); _stream.data(frame, callback); } diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Content.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Content.java index b46b6bca36a5..1cc8cafe7768 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Content.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Content.java @@ -21,7 +21,9 @@ import java.nio.channels.AsynchronousByteChannel; import java.nio.channels.ByteChannel; import java.nio.channels.CompletionHandler; +import java.nio.channels.FileChannel; import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -45,6 +47,8 @@ import org.eclipse.jetty.io.internal.ContentSourceRange; import org.eclipse.jetty.io.internal.ContentSourceRetainableByteBuffer; import org.eclipse.jetty.io.internal.ContentSourceString; +import org.eclipse.jetty.io.internal.PathContentSource; +import org.eclipse.jetty.io.internal.Transferable; import org.eclipse.jetty.util.Blocker; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; @@ -71,12 +75,15 @@ private Content() * the given callback when the copy is complete (either succeeded or failed).

*

In case of {@link Chunk#getFailure() failure chunks}, * the content source is {@link Source#fail(Throwable) failed}.

+ *

When the last chunk is read, the last write is performed; use + * {@link #copy(Source, boolean, Sink, Callback)} to customize the + * last write behavior.

* * @param source the source to copy from * @param sink the sink to copy to * @param callback the callback to notify when the copy is complete - * @see #copy(Source, Sink, Chunk.Processor, Callback) to allow processing of individual {@code Content.Chunk}s, including - * the ability to ignore transient failures. + * @see #copy(Source, Sink, Chunk.Processor, Callback) to allow processing of individual + * {@code Content.Chunk}s, including the ability to ignore transient failures. */ public static void copy(Source source, Sink sink, Callback callback) { @@ -97,6 +104,9 @@ public static void copy(Source source, Sink sink, Callback callback) *

In case of {@link Chunk#getFailure() failure chunks} not handled by any {@code chunkHandler}, * the content source is {@link Source#fail(Throwable) failed} if the failure * chunk is {@link Chunk#isLast() last}, else the failure is transient and is ignored.

+ *

When the last chunk is read, the last write is performed; use + * {@link #copy(Source, boolean, Sink, Callback)} to customize the + * last write behavior.

* * @param source the source to copy from * @param sink the sink to copy to @@ -108,6 +118,46 @@ public static void copy(Source source, Sink sink, Chunk.Processor chunkProcessor new ContentCopier(source, sink, chunkProcessor, callback).iterate(); } + /** + *

Copies the given content source to the given content sink, notifying + * the given callback when the copy is complete (either succeeded or failed).

+ *

When the last chunk is read, the final write is performed, and it is the + * last write only if the given parameter is {@code true}.

+ * + * @param source the source to copy from + * @param last whether the final write is the last write + * @param sink the sink to copy to + * @param callback the callback to notify when the copy is complete + */ + public static void copy(Source source, boolean last, Sink sink, Callback callback) + { + new ContentCopier(source, last, sink, null, callback).iterate(); + } + + /** + *

Attempts to transfer the given content source to the given content + * sink using zero-copy primitives such as + * {@link FileChannel#transferTo(long, long, WritableByteChannel)} if possible.

+ *

If the transfer cannot be performed, falls back to a regular call + * to {@link #copy(Source, boolean, Sink, Callback) copy(source, false, sink, callback)}.

+ * + * @param source the source to transfer from + * @param sink the sink to transfer to + * @param callback the callback to notify when the transfer is complete + * @return whether the transfer was performed + */ + public static boolean transfer(Source.Seekable source, Sink sink, Callback callback) + { + if (source instanceof Transferable.From from) + { + if (from.transferTo(sink, callback)) + return true; + } + // Cannot transfer, fall back to regular copy. + Content.copy(source, false, sink, callback); + return false; + } + /** *

A source of content that can be read with a read/demand model.

*

To avoid leaking its resources, a source must either:

@@ -181,6 +231,62 @@ interface Factory Content.Source newContentSource(ByteBufferPool.Sized bufferPool, long offset, long length); } + /** + *

A {@link Content.Source} that maintains a position and allows the position to be changed.

+ *

The position is updated in every read, but can be changed without reading, for example to + * {@link #slice(long, int) slice} this source into smaller sources.

+ */ + interface Seekable extends Source + { + /** + * @return the current position, or -1 if unknown + */ + default long position() + { + return -1; + } + + /** + * @param position the new position + */ + default void position(long position) + { + } + + /** + * @return the number of bytes remaining, or -1 if the length is unknown + */ + default long remaining() + { + return -1; + } + + /** + *

Creates a new slice from this source, from the given absolute position, for the given length.

+ * + * @param position the position to slice from + * @param length the length of the slice + * @return a new slice + */ + Seekable slice(long position, int length); + + /** + *

Implementations are made aware of a {@link Seekable} instance.

+ */ + interface Aware + { + /** + * @return the content source associated with this instance + */ + Seekable getContentSource(); + + /** + * @param source the content source to associate to this instance + */ + void setContentSource(Seekable source); + } + } + /** * Create a {@code Content.Source} from zero or more {@link ByteBuffer}s * @param byteBuffers The {@link ByteBuffer}s to use as the source. @@ -264,7 +370,7 @@ static Content.Source from(ByteBufferPool.Sized byteBufferPool, Path path) */ static Content.Source from(ByteBufferPool.Sized byteBufferPool, Path path, long offset, long length) { - return new ByteChannelContentSource.PathContentSource(byteBufferPool, path, offset, length); + return new PathContentSource(byteBufferPool, path, offset, length); } /** @@ -701,6 +807,11 @@ default boolean rewind() */ public interface Sink { + /** + *

A special {@link ByteBuffer} used to implement {@link #write(Sink, boolean, Source, Callback)}.

+ */ + ByteBuffer TRANSFER_TO = ByteBuffer.allocate(0); + /** *

Wraps the given {@link OutputStream} as a {@link Sink}. * @param out The stream to wrap @@ -910,6 +1021,69 @@ static void write(Sink sink, boolean last, String utf8Content, Callback callback sink.write(last, ByteBuffer.wrap(utf8Content.getBytes(StandardCharsets.UTF_8)), callback); } + /** + *

Writes the given {@link Source}, trying to optimize for zero-copy of bytes + * between the source and the sink.

+ *

For the zero-copy optimization to happen, the sink or one of its wrapped + * sinks must implement {@link Source.Seekable.Aware}, so that the source can + * be associated with the sink. + * This call is then converted to {@code sink.write(last, TRANSFER_TO, callback)} + * and sink implementation should check whether the {@link ByteBuffer} is + * {@link #TRANSFER_TO}, and if so they can retrieve the source via + * {@link #findSourceSeekable(Sink)}. + * Eventually, the {@code sink.write(last, TRANSFER_TO, callback)} call + * arrives to a sink implementation that supports the zero-copy optimization, + * and can therefore call {@link #transfer(Source.Seekable, Sink, Callback)}. + * + * @param sink the sink to write to + * @param last whether the write should be last + * @param source the source to read from + * @param callback – the callback to notify when the write is complete + */ + static void write(Sink sink, boolean last, Content.Source source, Callback callback) + { + Source.Seekable.Aware aware = findContentSourceAware(sink); + if (aware != null && source instanceof Source.Seekable seekable) + { + // Optimization to enable zero-copy. + aware.setContentSource(seekable); + sink.write(last, TRANSFER_TO, callback); + } + else + { + // Normal source.read() + sink.write() full copy. + Content.copy(source, last, sink, callback); + } + } + + /** + *

Utility method to be used by sink wrappers to find the source + * associated with the given sink, or one of its wrapped sinks, by + * {@link #write(Sink, boolean, Source, Callback)}.

+ * + * @param sink the sink to probe + * @return the associated sink, or {@code null} if the sink is not found + */ + static Source.Seekable findSourceSeekable(Sink sink) + { + Source.Seekable.Aware aware = findContentSourceAware(sink); + return aware == null ? null : aware.getContentSource(); + } + + private static Source.Seekable.Aware findContentSourceAware(Sink sink) + { + while (true) + { + if (sink instanceof Source.Seekable.Aware aware) + return aware; + if (sink instanceof Wrapper wrapper) + sink = wrapper.getWrapped(); + else + break; + } + return null; + } + /** *

Writes the given {@link ByteBuffer}, notifying the {@link Callback} * when the write is complete.

@@ -922,6 +1096,30 @@ static void write(Sink sink, boolean last, String utf8Content, Callback callback * @param callback the callback to notify when the write operation is complete */ void write(boolean last, ByteBuffer byteBuffer, Callback callback); + + /** + * A {@link Sink} wrapper. + */ + class Wrapper implements Sink + { + private final Sink wrapped; + + public Wrapper(Sink wrapped) + { + this.wrapped = wrapped; + } + + public Sink getWrapped() + { + return wrapped; + } + + @Override + public void write(boolean last, ByteBuffer byteBuffer, Callback callback) + { + getWrapped().write(last, byteBuffer, callback); + } + } } /** diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/IOResources.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/IOResources.java index 55fda771af82..ada90f17f404 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/IOResources.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/IOResources.java @@ -25,8 +25,6 @@ import org.eclipse.jetty.util.Blocker; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.IO; -import org.eclipse.jetty.util.IteratingNestedCallback; import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.resource.MemoryResource; import org.eclipse.jetty.util.resource.Resource; @@ -241,7 +239,7 @@ public static void copy(Resource resource, Content.Sink sink, ByteBufferPool.Siz if (resource instanceof Content.Source.Factory factory) { Content.Source source = factory.newContentSource(bufferPool, offset, length); - Content.copy(source, sink, callback); + Content.Sink.write(sink, true, source, callback); return; } @@ -251,7 +249,8 @@ public static void copy(Resource resource, Content.Sink sink, ByteBufferPool.Siz Path path = resource.getPath(); if (path != null) { - new PathToSinkCopier(path, sink, bufferPool, offset, length, callback).iterate(); + Content.Source source = Content.Source.from(bufferPool, path, offset, length); + Content.Sink.write(sink, true, source, callback); return; } @@ -268,111 +267,11 @@ public static void copy(Resource resource, Content.Sink sink, ByteBufferPool.Siz if (inputStream == null) throw new IllegalArgumentException("Resource does not support InputStream: " + resource); Content.Source source = Content.Source.from(bufferPool, inputStream, offset, length); - Content.copy(source, sink, callback); + Content.Sink.write(sink, true, source, callback); } catch (Throwable x) { callback.failed(x); } } - - private static class PathToSinkCopier extends IteratingNestedCallback - { - private final SeekableByteChannel channel; - private final Content.Sink sink; - private final ByteBufferPool.Sized pool; - private long remainingLength; - private RetainableByteBuffer retainableByteBuffer; - private boolean terminated; - - public PathToSinkCopier(Path path, Content.Sink sink, ByteBufferPool.Sized pool, long offset, long length, Callback callback) throws IOException - { - super(callback); - this.sink = sink; - this.pool = pool == null ? ByteBufferPool.SIZED_NON_POOLING : pool; - this.remainingLength = length; - this.channel = Files.newByteChannel(path); - skipToOffset(channel, offset, length, this.pool); - } - - private static void skipToOffset(SeekableByteChannel channel, long offset, long length, ByteBufferPool.Sized pool) - { - if (offset > 0L && length != 0L) - { - RetainableByteBuffer.Mutable byteBuffer = pool.acquire(1); - try - { - channel.position(offset - 1); - if (channel.read(byteBuffer.getByteBuffer().limit(1)) == -1) - throw new IllegalArgumentException("Offset out of range"); - } - catch (IOException e) - { - throw new UncheckedIOException(e); - } - finally - { - byteBuffer.release(); - } - } - } - - @Override - public InvocationType getInvocationType() - { - return InvocationType.NON_BLOCKING; - } - - @Override - protected Action process() throws Throwable - { - if (terminated) - return Action.SUCCEEDED; - - if (retainableByteBuffer == null) - retainableByteBuffer = pool.acquire(); - - ByteBuffer byteBuffer = retainableByteBuffer.getByteBuffer(); - BufferUtil.clearToFill(byteBuffer); - if (remainingLength >= 0 && remainingLength < Integer.MAX_VALUE) - byteBuffer.limit((int)Math.min(byteBuffer.capacity(), remainingLength)); - boolean eof = false; - while (byteBuffer.hasRemaining() && !eof) - { - int read = channel.read(byteBuffer); - if (read == -1) - eof = true; - else if (remainingLength >= 0) - remainingLength -= read; - } - BufferUtil.flipToFlush(byteBuffer, 0); - terminated = eof || remainingLength == 0; - sink.write(terminated, byteBuffer, this); - return Action.SCHEDULED; - } - - @Override - protected void onCompleteSuccess() - { - if (retainableByteBuffer != null) - retainableByteBuffer.release(); - IO.close(channel); - super.onCompleteSuccess(); - } - - @Override - protected void onFailure(Throwable x) - { - IO.close(channel); - super.onFailure(x); - } - - @Override - protected void onCompleteFailure(Throwable cause) - { - if (retainableByteBuffer != null) - retainableByteBuffer.release(); - super.onCompleteFailure(cause); - } - } } diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java index db2e9780dabd..c8d3416efae6 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java @@ -2406,20 +2406,27 @@ public void writeTo(Content.Sink sink, boolean last, Callback callback) // Can we do a gather write? if (!last && sink instanceof EndPoint endPoint) { + boolean canGather = true; ByteBuffer[] buffers = new ByteBuffer[_buffers.size()]; - int i = 0; - for (RetainableByteBuffer rbb : _buffers) - buffers[i++] = rbb.getByteBuffer(); - endPoint.write(Callback.from(this::clear, callback), buffers); - return; + for (int i = 0; i < _buffers.size(); ++i) + { + RetainableByteBuffer rbb = _buffers.get(i); + ByteBuffer byteBuffer = buffers[i] = rbb.getByteBuffer(); + canGather &= byteBuffer != Content.Sink.TRANSFER_TO; + } + if (canGather) + { + endPoint.write(Callback.from(this::clear, callback), buffers); + return; + } } - // write buffer by buffer - new IteratingNestedCallback(callback) + // Write buffer by buffer. + IteratingNestedCallback flusher = new IteratingNestedCallback(callback) { - int _index; - RetainableByteBuffer _buffer; - boolean _lastWritten; + private int _index; + private RetainableByteBuffer _buffer; + private boolean _lastWritten; @Override protected Action process() @@ -2458,7 +2465,8 @@ protected void onCompleteFailure(Throwable x) // release the last buffer written _buffer = Retainable.release(_buffer); } - }.iterate(); + }; + flusher.iterate(); } } } diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/SelectableChannelEndPoint.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/SelectableChannelEndPoint.java index 1eb0a151d40d..d43b73fec7f0 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/SelectableChannelEndPoint.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/SelectableChannelEndPoint.java @@ -253,20 +253,22 @@ public Runnable onSelected() if (LOG.isDebugEnabled()) LOG.debug("onSelected {}->{} r={} w={} for {}", oldInterestOps, newInterestOps, fillable, flushable, this); - // return task to complete the job - Runnable task = fillable - ? (flushable - ? _runCompleteWriteFillable - : _runFillable) - : (flushable - ? _runCompleteWrite - : null); + Runnable task = taskForSelected(fillable, flushable); if (LOG.isDebugEnabled()) LOG.debug("task {}", task); return task; } + protected Runnable taskForSelected(boolean fillable, boolean flushable) + { + if (fillable) + return flushable ? _runCompleteWriteFillable : _runFillable; + if (flushable) + return _runCompleteWrite; + return null; + } + private void updateKeyAction(Selector selector) { updateKey(); diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java index 0218bcb25fcf..dcd097d0d66f 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java @@ -16,10 +16,17 @@ import java.io.IOException; import java.net.SocketAddress; import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import org.eclipse.jetty.io.internal.Transferable; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.thread.Invocable; import org.eclipse.jetty.util.thread.Scheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,10 +34,12 @@ /** *

An {@link EndPoint} implementation based on {@link SocketChannel}.

*/ -public class SocketChannelEndPoint extends SelectableChannelEndPoint +public class SocketChannelEndPoint extends SelectableChannelEndPoint implements Transferable.To { private static final Logger LOG = LoggerFactory.getLogger(SocketChannelEndPoint.class); + private final AtomicReference transferCallback = new AtomicReference<>(); + public SocketChannelEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) { super(scheduler, channel, selector, key); @@ -42,6 +51,54 @@ public SocketChannel getChannel() return (SocketChannel)super.getChannel(); } + @Override + public boolean transferFrom(FileChannel fileChannel, long offset, long length, Callback callback) + { + Transferable.transfer(fileChannel, offset, length, this, callback); + return true; + } + + public void onIncompleteTransfer(Callback callback) + { + if (transferCallback.compareAndSet(null, callback)) + onIncompleteFlush(); + else + throw new IllegalStateException("Transfer callback already present"); + } + + @Override + protected Runnable taskForSelected(boolean fillable, boolean flushable) + { + Callback callback = transferCallback.getAndSet(null); + if (callback == null) + return super.taskForSelected(fillable, flushable); + + // For the transfer case, only flushable must be true. + assert !fillable && flushable; + + return new Invocable.ReadyTask(callback.getInvocationType(), callback::succeeded); + } + + @Override + public void onClose(Throwable cause) + { + Callback callback = transferCallback.getAndSet(null); + if (callback != null) + callback.failed(cause == null ? new ClosedChannelException() : cause); + super.onClose(cause); + } + + @Override + protected void onIdleExpired(TimeoutException timeout) + { + Callback callback = transferCallback.getAndSet(null); + if (callback != null) + callback.failed(timeout); + super.onIdleExpired(timeout); + } + + // TODO: override onFail() + @Override public SocketAddress getRemoteSocketAddress() { diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/PathContentSource.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/PathContentSource.java index 2b6cea5b35e4..9948f41fa07a 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/PathContentSource.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/PathContentSource.java @@ -21,65 +21,20 @@ /** *

A {@link Content.Source} that provides the file content of the passed {@link Path}.

*/ -public class PathContentSource implements Content.Source +public class PathContentSource extends org.eclipse.jetty.io.internal.PathContentSource { - private final Path _path; - private final Content.Source _source; - public PathContentSource(Path path) { - this(path, null); + super(path); } public PathContentSource(Path path, ByteBufferPool byteBufferPool) { - this(path, byteBufferPool instanceof ByteBufferPool.Sized sized ? sized : new ByteBufferPool.Sized(byteBufferPool)); + super(byteBufferPool instanceof ByteBufferPool.Sized sized ? sized : new ByteBufferPool.Sized(byteBufferPool), path); } public PathContentSource(Path path, ByteBufferPool.Sized sizedBufferPool) { - _path = path; - _source = Content.Source.from(sizedBufferPool, path); - } - - public Path getPath() - { - return _path; - } - - @Override - public void demand(Runnable demandCallback) - { - _source.demand(demandCallback); - } - - @Override - public void fail(Throwable failure) - { - _source.fail(failure); - } - - @Override - public void fail(Throwable failure, boolean last) - { - _source.fail(failure, last); - } - - @Override - public long getLength() - { - return _source.getLength(); - } - - @Override - public Content.Chunk read() - { - return _source.read(); - } - - @Override - public boolean rewind() - { - return _source.rewind(); + super(sizedBufferPool, path); } } diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ByteChannelContentSource.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ByteChannelContentSource.java index 152d2e2259bd..273d69a9b35b 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ByteChannelContentSource.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ByteChannelContentSource.java @@ -17,10 +17,6 @@ import java.nio.ByteBuffer; import java.nio.channels.ByteChannel; import java.nio.channels.ClosedChannelException; -import java.nio.channels.SeekableByteChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import java.util.Objects; import org.eclipse.jetty.io.ByteBufferPool; @@ -52,7 +48,8 @@ public class ByteChannelContentSource implements Content.Source private Content.Chunk _terminal; /** - * Create a {@link ByteChannelContentSource} which reads from a {@link ByteChannel}. + * Create a new instance that reads from a {@link ByteChannel}. + * * @param byteBufferPool The {@link org.eclipse.jetty.io.ByteBufferPool.Sized} to use for any internal buffers. * @param byteChannel The {@link ByteChannel}s to use as the source. */ @@ -62,15 +59,14 @@ public ByteChannelContentSource(ByteBufferPool.Sized byteBufferPool, ByteChannel } /** - * Create a {@link ByteChannelContentSource} which reads from a {@link ByteChannel}. - * If the {@link ByteChannel} is an instance of {@link SeekableByteChannel} the implementation will use - * {@link SeekableByteChannel#position(long)} to navigate to the starting offset. + * Create a new instance that reads from a {@link ByteChannel}. + * * @param byteBufferPool The {@link org.eclipse.jetty.io.ByteBufferPool.Sized} to use for any internal buffers. * @param byteChannel The {@link ByteChannel}s to use as the source. * @param offset the offset byte of the content to start from. - * Must be greater than or equal to 0 and less than the content length (if known). + * Must be greater than or equal to 0 and less than the content length (if known). * @param length the length of the content to make available, -1 for the full length. - * If the size of the content is known, the length may be truncated to the content size minus the offset. + * If the size of the content is known, the length may be truncated to the content size minus the offset. * @throws IndexOutOfBoundsException if the offset or length are out of range. * @see TypeUtil#checkOffsetLengthSize(long, long, long) */ @@ -83,15 +79,44 @@ public ByteChannelContentSource(ByteBufferPool.Sized byteBufferPool, ByteChannel _offsetRemaining = offset; } + protected AutoLock lock() + { + return lock.lock(); + } + + public ByteBufferPool.Sized getByteBufferPool() + { + return _byteBufferPool; + } + + public ByteChannel getByteChannel() + { + try (AutoLock ignored = lock()) + { + return _byteChannel; + } + } + + public long getOffset() + { + return _offset; + } + + @Override + public long getLength() + { + return _length; + } + protected ByteChannel open() throws IOException { - return _byteChannel; + return getByteChannel(); } @Override public void demand(Runnable demandCallback) { - try (AutoLock ignored = lock.lock()) + try (AutoLock ignored = lock()) { if (this.demandCallback != null) throw new IllegalStateException("demand pending"); @@ -103,7 +128,7 @@ public void demand(Runnable demandCallback) private void invokeDemandCallback() { Runnable demandCallback; - try (AutoLock ignored = lock.lock()) + try (AutoLock ignored = lock()) { demandCallback = this.demandCallback; this.demandCallback = null; @@ -125,7 +150,7 @@ protected void lockedSetTerminal(Content.Chunk terminal) _buffer = null; } - private void lockedEnsureOpenOrTerminal() + protected Content.Chunk lockedEnsureOpenOrTerminal() { assert lock.isHeldByCurrentThread(); if (_terminal == null && (_byteChannel == null || !_byteChannel.isOpen())) @@ -134,31 +159,25 @@ private void lockedEnsureOpenOrTerminal() { _byteChannel = open(); if (_byteChannel == null || !_byteChannel.isOpen()) - { lockedSetTerminal(Content.Chunk.from(new ClosedChannelException(), true)); - } - else if (_byteChannel instanceof SeekableByteChannel seekableByteChannel) - { - seekableByteChannel.position(_offset); - _offsetRemaining = 0; - } } catch (IOException e) { lockedSetTerminal(Content.Chunk.from(e, true)); } } + return _terminal; } @Override public Content.Chunk read() { - try (AutoLock ignored = lock.lock()) + try (AutoLock ignored = lock()) { - lockedEnsureOpenOrTerminal(); + Content.Chunk terminal = lockedEnsureOpenOrTerminal(); - if (_terminal != null) - return _terminal; + if (terminal != null) + return terminal; if (_length == 0) { @@ -178,147 +197,70 @@ else if (_buffer.isRetained()) try { - ByteBuffer byteBuffer = _buffer.getByteBuffer(); - if (_offsetRemaining > 0) - { - // Discard all bytes read until we reach the staring offset. - while (_offsetRemaining > 0) - { - BufferUtil.clearToFill(byteBuffer); - byteBuffer.limit((int)Math.min(_buffer.capacity(), _offsetRemaining)); - int read = _byteChannel.read(byteBuffer); - if (read < 0) - { - lockedSetTerminal(Content.Chunk.EOF); - return _terminal; - } - if (read == 0) - return null; - - _offsetRemaining -= read; - } - } + Content.Chunk skipped = skipToOffset(); + if (skipped != Content.Chunk.EMPTY) + return skipped; + ByteBuffer byteBuffer = _buffer.getByteBuffer(); BufferUtil.clearToFill(byteBuffer); if (_length > 0) byteBuffer.limit((int)Math.min(_buffer.capacity(), _length - _totalRead)); int read = _byteChannel.read(byteBuffer); BufferUtil.flipToFlush(byteBuffer, 0); - if (read == 0) - return null; if (read > 0) { _totalRead += read; _buffer.retain(); if (_length < 0 || _totalRead < _length) return Content.Chunk.asChunk(byteBuffer, false, _buffer); - Content.Chunk last = Content.Chunk.asChunk(byteBuffer, true, _buffer); lockedSetTerminal(Content.Chunk.EOF); return last; } + if (read == 0) + return null; lockedSetTerminal(Content.Chunk.EOF); + return Content.Chunk.EOF; } catch (Throwable t) { - lockedSetTerminal(Content.Chunk.from(t, true)); + Content.Chunk failure = Content.Chunk.from(t, true); + lockedSetTerminal(failure); + return failure; } } - return _terminal; - } - - @Override - public void fail(Throwable failure) - { - try (AutoLock ignored = lock.lock()) - { - lockedSetTerminal(Content.Chunk.from(failure, true)); - } - } - - @Override - public long getLength() - { - return _length; } - @Override - public boolean rewind() + protected Content.Chunk skipToOffset() throws IOException { - try (AutoLock ignored = lock.lock()) + ByteBuffer byteBuffer = _buffer.getByteBuffer(); + if (_offsetRemaining > 0) { - // We can only rewind if we have a SeekableByteChannel. - if (!(_byteChannel instanceof SeekableByteChannel)) - return false; - - // We can remove terminal condition for a rewind that is likely to occur - if (_terminal != null && !Content.Chunk.isFailure(_terminal) && (_byteChannel == null || _byteChannel instanceof SeekableByteChannel)) - _terminal = null; - - lockedEnsureOpenOrTerminal(); - if (_terminal != null || _byteChannel == null || !_byteChannel.isOpen()) - return false; - - try - { - ((SeekableByteChannel)_byteChannel).position(_offset); - _offsetRemaining = 0; - _totalRead = 0; - return true; - } - catch (Throwable t) + // Discard all bytes read until we reach the staring offset. + while (_offsetRemaining > 0) { - lockedSetTerminal(Content.Chunk.from(t, true)); + BufferUtil.clearToFill(byteBuffer); + byteBuffer.limit((int)Math.min(_buffer.capacity(), _offsetRemaining)); + int read = _byteChannel.read(byteBuffer); + if (read < 0) + { + lockedSetTerminal(Content.Chunk.EOF); + return Content.Chunk.EOF; + } + if (read == 0) + return null; + _offsetRemaining -= read; } - - return true; } + return Content.Chunk.EMPTY; } - /** - * A {@link ByteChannelContentSource} for a {@link Path} - */ - public static class PathContentSource extends ByteChannelContentSource + @Override + public void fail(Throwable failure) { - private final Path _path; - - public PathContentSource(Path path) - { - this(null, path, 0L, -1L); - } - - public PathContentSource(ByteBufferPool.Sized byteBufferPool, Path path) - { - this(byteBufferPool, path, 0L, -1L); - } - - public PathContentSource(ByteBufferPool.Sized byteBufferPool, Path path, long offset, long length) - { - super(byteBufferPool, null, offset, TypeUtil.checkOffsetLengthSize(offset, length, size(path))); - _path = path; - } - - public Path getPath() + try (AutoLock ignored = lock()) { - return _path; - } - - @Override - protected ByteChannel open() throws IOException - { - return Files.newByteChannel(_path, StandardOpenOption.READ); - } - - private static long size(Path path) - { - try - { - return Files.size(path); - } - catch (IOException e) - { - return -1L; - } + lockedSetTerminal(Content.Chunk.from(failure, true)); } } } diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentCopier.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentCopier.java index 43c04d056c3e..05b61842d929 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentCopier.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentCopier.java @@ -26,15 +26,22 @@ public class ContentCopier extends IteratingNestedCallback private static final Logger LOG = LoggerFactory.getLogger(ContentCopier.class); private final Content.Source source; + private final boolean last; private final Content.Sink sink; private final Content.Chunk.Processor chunkProcessor; private Content.Chunk chunk; private boolean terminated; public ContentCopier(Content.Source source, Content.Sink sink, Content.Chunk.Processor chunkProcessor, Callback callback) + { + this(source, true, sink, chunkProcessor, callback); + } + + public ContentCopier(Content.Source source, boolean last, Content.Sink sink, Content.Chunk.Processor chunkProcessor, Callback callback) { super(callback); this.source = source; + this.last = last; this.sink = sink; this.chunkProcessor = chunkProcessor; } @@ -64,7 +71,7 @@ protected Action process() throws Throwable return Action.SCHEDULED; } - sink.write(chunk.isLast(), chunk.getByteBuffer(), this); + sink.write(chunk.isLast() && last, chunk.getByteBuffer(), this); return Action.SCHEDULED; } diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/PathContentSource.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/PathContentSource.java new file mode 100644 index 000000000000..ca0901df083d --- /dev/null +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/PathContentSource.java @@ -0,0 +1,101 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.io.internal; + +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.thread.AutoLock; + +/** + * A {@link ByteChannelContentSource} for a {@link Path} + */ +public class PathContentSource extends SeekableByteChannelContentSource implements Transferable.From +{ + private final Path _path; + + public PathContentSource(Path path) + { + this(null, path, 0L, -1L); + } + + public PathContentSource(ByteBufferPool.Sized byteBufferPool, Path path) + { + this(byteBufferPool, path, 0L, -1L); + } + + public PathContentSource(ByteBufferPool.Sized byteBufferPool, Path path, long offset, long length) + { + super(byteBufferPool, null, offset, TypeUtil.checkOffsetLengthSize(offset, length, size(path))); + _path = path; + } + + public Path getPath() + { + return _path; + } + + @Override + public FileChannel getByteChannel() + { + return (FileChannel)super.getByteChannel(); + } + + @Override + protected SeekableByteChannel open() throws IOException + { + return Files.newByteChannel(_path, StandardOpenOption.READ); + } + + @Override + public Seekable slice(long position, int length) + { + // TODO: check position and length? + return new PathContentSource(getByteBufferPool(), getPath(), position, length); + } + + @Override + public boolean transferTo(Content.Sink sink, Callback callback) + { + try (AutoLock ignored = lock()) + { + Content.Chunk terminal = lockedEnsureOpenOrTerminal(); + if (Content.Chunk.isFailure(terminal)) + return false; + if (!(sink instanceof Transferable.To to)) + return false; + return to.transferFrom(getByteChannel(), getOffset(), getLength(), callback); + } + } + + private static long size(Path path) + { + try + { + return Files.size(path); + } + catch (IOException e) + { + return -1L; + } + } +} diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/SeekableByteChannelContentSource.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/SeekableByteChannelContentSource.java new file mode 100644 index 000000000000..3e5459856437 --- /dev/null +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/SeekableByteChannelContentSource.java @@ -0,0 +1,117 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.io.internal; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.channels.SeekableByteChannel; + +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.util.TypeUtil; + +/** + *

A {@link Content.Source} backed by a {@link SeekableByteChannel}. + */ +public class SeekableByteChannelContentSource extends ByteChannelContentSource implements Content.Source.Seekable +{ + private long _position; + + /** + * Create a new instance that reads from a {@link SeekableByteChannel}. + * + * @param byteBufferPool The {@link ByteBufferPool.Sized} to use for any internal buffers. + * @param byteChannel The {@link SeekableByteChannel}s to use as the source. + */ + public SeekableByteChannelContentSource(ByteBufferPool.Sized byteBufferPool, SeekableByteChannel byteChannel) + { + this(byteBufferPool, byteChannel, 0L, -1L); + } + + /** + * Create a new instance that reads from a {@link SeekableByteChannel}. + * + * @param byteBufferPool The {@link ByteBufferPool.Sized} to use for any internal buffers. + * @param byteChannel The {@link SeekableByteChannel}s to use as the source. + * @param offset the position to start reading from. + * Must be greater than or equal to 0 and less than the content length (if known). + * @param length the length of the content to make available, -1 for the full length. + * If the size of the content is known, the length may be truncated to the content size minus the position. + * @throws IndexOutOfBoundsException if the position or length are out of range. + * @see TypeUtil#checkOffsetLengthSize(long, long, long) + */ + public SeekableByteChannelContentSource(ByteBufferPool.Sized byteBufferPool, SeekableByteChannel byteChannel, long offset, long length) + { + super(byteBufferPool, byteChannel, offset, length); + _position = offset; + } + + @Override + public SeekableByteChannel getByteChannel() + { + return (SeekableByteChannel)super.getByteChannel(); + } + + @Override + protected Content.Chunk skipToOffset() + { + position(getOffset()); + return Content.Chunk.EMPTY; + } + + @Override + public long position() + { + return _position; + } + + @Override + public void position(long position) + { + try + { + if (position < 0) + throw new IllegalArgumentException("invalid position " + position); + _position = position; + SeekableByteChannel seekable = getByteChannel(); + if (seekable != null) + seekable.position(position); + } + catch (IOException x) + { + throw new UncheckedIOException(x); + } + } + + @Override + public long remaining() + { + long length = getLength(); + return length < 0 ? -1 : length - _position + getOffset(); + } + + @Override + public Seekable slice(long position, int length) + { + // TODO: check position and length + return new SeekableByteChannelContentSource(getByteBufferPool(), getByteChannel(), position, length); + } + + @Override + public boolean rewind() + { + // TODO + return false; + } +} diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/Transferable.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/Transferable.java new file mode 100644 index 000000000000..b243bc575846 --- /dev/null +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/Transferable.java @@ -0,0 +1,83 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.io.internal; + +import java.nio.channels.FileChannel; + +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.SocketChannelEndPoint; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.IteratingNestedCallback; + +public class Transferable +{ + private Transferable() + { + } + + public static void transfer(FileChannel sourceChannel, long offset, long length, SocketChannelEndPoint endPoint, Callback callback) + { + Transferrer transferrer = new Transferrer(sourceChannel, offset, length, endPoint, callback); + transferrer.iterate(); + } + + public interface From + { + boolean transferTo(Content.Sink sink, Callback callback); + } + + public interface To + { + boolean transferFrom(FileChannel fileChannel, long offset, long length, Callback callback); + } + + private static class Transferrer extends IteratingNestedCallback + { + private final FileChannel fileChannel; + private final long offset; + private final long length; + private final SocketChannelEndPoint endPoint; + private long transferred; + + private Transferrer(FileChannel fileChannel, long offset, long length, SocketChannelEndPoint endPoint, Callback callback) + { + super(callback); + this.fileChannel = fileChannel; + this.offset = offset; + this.length = length; + this.endPoint = endPoint; + } + + @Override + protected Action process() throws Throwable + { + long count = length - transferred; + if (count == 0) + return Action.SUCCEEDED; + + long transfer = fileChannel.transferTo(offset + transferred, count, endPoint.getChannel()); + transferred += transfer; + + if (transfer > 0) + { + endPoint.notIdle(); + succeeded(); + return Action.SCHEDULED; + } + + endPoint.onIncompleteTransfer(this); + return Action.SCHEDULED; + } + } +} diff --git a/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTest.java b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTest.java index 9b78c3fc4e02..0d522cf9b5e7 100644 --- a/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTest.java +++ b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTest.java @@ -152,9 +152,9 @@ public String toString() ByteChannelContentSource bccs2 = new ByteChannelContentSource(new ByteBufferPool.Sized(byteBufferPool, false, 8192), Files.newByteChannel(path0123, StandardOpenOption.READ), 4, 6); ByteChannelContentSource bccs3 = new ByteChannelContentSource(new ByteBufferPool.Sized(null, false, 3), Files.newByteChannel(path0123, StandardOpenOption.READ), 4, 6); - ByteChannelContentSource.PathContentSource pcs0 = new ByteChannelContentSource.PathContentSource(new ByteBufferPool.Sized(byteBufferPool, false, 1024), path12); - ByteChannelContentSource.PathContentSource pcs1 = new ByteChannelContentSource.PathContentSource(new ByteBufferPool.Sized(byteBufferPool, false, 1024), path0123, 4, 6); - ByteChannelContentSource.PathContentSource pcs2 = new ByteChannelContentSource.PathContentSource(new ByteBufferPool.Sized(null, false, 3), path12); + org.eclipse.jetty.io.internal.PathContentSource pcs0 = new org.eclipse.jetty.io.internal.PathContentSource(new ByteBufferPool.Sized(byteBufferPool, false, 1024), path12); + org.eclipse.jetty.io.internal.PathContentSource pcs1 = new org.eclipse.jetty.io.internal.PathContentSource(new ByteBufferPool.Sized(byteBufferPool, false, 1024), path0123, 4, 6); + org.eclipse.jetty.io.internal.PathContentSource pcs2 = new org.eclipse.jetty.io.internal.PathContentSource(new ByteBufferPool.Sized(null, false, 3), path12); return switch (mode) { @@ -298,7 +298,7 @@ public void run() public void testReadAllRewindReadAll(Content.Source source) throws Exception { // A raw BCCS cannot be rewound if fully consumed, as it is not able to re-open a passed in channel - Assumptions.assumeTrue(!(source instanceof ByteChannelContentSource) || source instanceof ByteChannelContentSource.PathContentSource); + Assumptions.assumeTrue(!(source instanceof ByteChannelContentSource) || source instanceof org.eclipse.jetty.io.internal.PathContentSource); String first = Content.Source.asString(source); assertThat(first, is("onetwo")); diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 6e1380623b47..fea9b3400b0d 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -748,20 +748,23 @@ static Content.Sink asBufferedSink(Request request, Response response) return Content.Sink.asBuffered(response, bufferPool, useOutputDirectByteBuffers, outputAggregationSize, bufferSize); } - class Wrapper implements Response + /** + * A {@link Response} wrapper. + */ + class Wrapper extends Content.Sink.Wrapper implements Response { private final Request _request; - private final Response _wrapped; public Wrapper(Request request, Response wrapped) { + super(wrapped); _request = request; - _wrapped = wrapped; } + @Override public Response getWrapped() { - return _wrapped; + return (Response)super.getWrapped(); } @Override @@ -829,11 +832,5 @@ public CompletableFuture writeInterim(int status, HttpFields headers) { return getWrapped().writeInterim(status, headers); } - - @Override - public void write(boolean last, ByteBuffer byteBuffer, Callback callback) - { - getWrapped().write(last, byteBuffer, callback); - } } } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java index d1dc73995380..15b3965b1584 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java @@ -1165,11 +1165,13 @@ public String toString() * method when calling * {@link HttpStream#send(MetaData.Request, MetaData.Response, boolean, ByteBuffer, Callback)} */ - public static class ChannelResponse implements Response, Callback + public static class ChannelResponse implements Response, Content.Source.Seekable.Aware, Callback { private final ChannelRequest _request; private final ResponseHttpFields _httpFields; - protected int _status; + private MetaData.Response _metaData; + private int _status; + private Content.Source.Seekable _source; private long _contentBytesWritten; private Supplier _trailers; private Callback _writeCallback; @@ -1264,12 +1266,11 @@ public void setTrailersSupplier(Supplier trailers) @Override public void write(boolean last, ByteBuffer content, Callback callback) { - long length = BufferUtil.length(content); + long length = content == Content.Sink.TRANSFER_TO ? _source.remaining() : BufferUtil.length(content); HttpChannelState httpChannelState; HttpStream stream; Throwable writeFailure; - MetaData.Response responseMetaData = null; try (AutoLock ignored = _request._lock.lock()) { httpChannelState = _request.lockedGetHttpChannelState(); @@ -1330,17 +1331,27 @@ else if (last && !(totalWritten == 0 && HttpMethod.HEAD.is(_request.getMethod()) return; } + Throwable dataFailure = null; + if (content == TRANSFER_TO && _source == null) + dataFailure = new IllegalStateException("No source for transferTo() operation"); + if (dataFailure != null) + { + Throwable failure = dataFailure; + httpChannelState._writeInvoker.run(() -> HttpChannelState.failed(callback, failure)); + return; + } + // No failure, do the actual stream send using the ChannelResponse as the callback. _writeCallback = callback; _contentBytesWritten = totalWritten; stream = httpChannelState._stream; if (_httpFields.commit()) - responseMetaData = lockedPrepareResponse(httpChannelState, last); + _metaData = lockedPrepareResponse(httpChannelState, last); } if (LOG.isDebugEnabled()) LOG.debug("writing last={} {} {}", last, BufferUtil.toDetailString(content), this); - stream.send(_request._metaData, responseMetaData, last, content, this); + stream.send(_request._metaData, _metaData, last, content, this); } /** @@ -1444,6 +1455,21 @@ public void reset() _request.getHttpChannelState().resetResponse(); } + @Override + public Content.Source.Seekable getContentSource() + { + return _source; + } + + @Override + public void setContentSource(Content.Source.Seekable source) + { + if (_metaData != null) + _metaData.setContentSource(source); + else + _source = source; + } + @Override public CompletableFuture writeInterim(int status, HttpFields headers) { @@ -1500,12 +1526,14 @@ MetaData.Response lockedPrepareResponse(HttpChannelState httpChannel, boolean la httpChannel._stream.prepareResponse(mutableHeaders); - return new MetaData.Response( + MetaData.Response response = new MetaData.Response( _status, null, httpChannel.getConnectionMetaData().getHttpVersion(), _httpFields, httpChannel._committedContentLength, getTrailersSupplier() ); + response.setContentSource(getContentSource()); + return response; } @Override @@ -1723,7 +1751,7 @@ private static class ErrorResponse extends ChannelResponse public ErrorResponse(ChannelRequest request) { super(request); - _status = HttpStatus.INTERNAL_SERVER_ERROR_500; + setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500); } @Override @@ -1747,7 +1775,7 @@ MetaData.Response lockedPrepareResponse(HttpChannelState httpChannelState, boole { assert httpChannelState._request._lock.isHeldByCurrentThread(); MetaData.Response httpFields = super.lockedPrepareResponse(httpChannelState, last); - httpChannelState._response._status = _status; + httpChannelState._response.setStatus(getStatus()); HttpFields.Mutable originalResponseFields = httpChannelState._responseHeaders.getMutableHttpFields(); originalResponseFields.clear(); originalResponseFields.add(getResponseHttpFields()); @@ -1829,7 +1857,7 @@ public void failed(Throwable x) { failure = _failure; httpChannelState = _request.lockedGetHttpChannelState(); - httpChannelState._response._status = _errorResponse._status; + httpChannelState._response.setStatus(_errorResponse.getStatus()); } ExceptionUtil.addSuppressedIfNotAssociated(failure, x); HttpChannelState.failed(httpChannelState._lastWriteCallback, failure); diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index a6d63cbebd67..28264ea2e6ae 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -944,7 +944,11 @@ public Action process() throws Exception getEndPoint().write(this, _content); break; default: - succeeded(); + Content.Source.Seekable source = _info.getContentSource(); + if (source != null) + Content.transfer(source, getEndPoint(), this); + else + succeeded(); } return Action.SCHEDULED; @@ -1238,6 +1242,7 @@ protected class HttpStreamOverHTTP1 implements HttpStream private long _contentLength = -1; private HostPortHttpField _hostField; private MetaData.Request _request; + private MetaData.Response _response; private HttpField _upgrade = null; private Content.Chunk _chunk; private boolean _connectionClose = false; @@ -1534,41 +1539,54 @@ public void prepareResponse(HttpFields.Mutable headers) @Override public void send(MetaData.Request request, MetaData.Response response, boolean last, ByteBuffer content, Callback callback) { - if (response == null) - { - if (!last && BufferUtil.isEmpty(content)) - { - callback.succeeded(); - return; - } - } - else if (_generator.isCommitted()) + if (_response == null) + sendHeaders(request, response, last, content, callback); + else + sendContent(request, response, last, content, callback); + } + + private void sendHeaders(MetaData.Request request, MetaData.Response response, boolean last, ByteBuffer content, Callback callback) + { + _response = response; + + if (_generator.isCommitted()) { callback.failed(new IllegalStateException("Committed")); return; } - else + + _responses.incrementAndGet(); + if (_expects100Continue) { - _responses.incrementAndGet(); - if (_expects100Continue) + if (response.getStatus() == HttpStatus.CONTINUE_100) { - if (response.getStatus() == HttpStatus.CONTINUE_100) - { - _expects100Continue = false; - } - else - { - // Expecting to send a 100 Continue response, but it's a different response, - // then cannot be persistent because likely the client did not send the content. - _generator.setPersistent(false); - } + _expects100Continue = false; + } + else + { + // Expecting to send a 100 Continue response, but it's a different response, + // then cannot be persistent because likely the client did not send the content. + _generator.setPersistent(false); } } - if (_sendCallback.reset(_request, response, content, last, callback)) + if (_sendCallback.reset(request, response, content, last, callback)) _sendCallback.iterate(); } + private void sendContent(MetaData.Request request, MetaData.Response response, boolean last, ByteBuffer content, Callback callback) + { + if (!last && BufferUtil.isEmpty(content)) + { + callback.succeeded(); + } + else + { + if (_sendCallback.reset(request, response, content, last, callback)) + _sendCallback.iterate(); + } + } + @Override public Runnable cancelSend(Throwable cause, Callback appCallback) { diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ResponseContentSourceTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ResponseContentSourceTest.java new file mode 100644 index 000000000000..529380bd7faa --- /dev/null +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ResponseContentSourceTest.java @@ -0,0 +1,104 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.test.client.transport; + +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.CompletableResponseListener; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.eclipse.jetty.util.Blocker; +import org.eclipse.jetty.util.Callback; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ResponseContentSourceTest extends AbstractTest +{ + @ParameterizedTest + @MethodSource("transportsNoFCGI") + public void testResponseContentSource(TransportType transportType) throws Exception + { + // Prepare a "small" file. TODO: also use a >2GiB file. + int contentLength = 1024 * 1024; + Path dir = Files.createDirectories(MavenPaths.targetTestDir(getClass().getSimpleName())); + Path file = Files.createTempFile(dir, "file-", ".bin"); + try (var channel = Files.newByteChannel(file, StandardOpenOption.WRITE)) + { + channel.write(ByteBuffer.allocateDirect(contentLength)); + } + + start(transportType, new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Content.Source source = Content.Source.from(file); + // TODO: possible alternative? +// source.writeTo(response, true, callback); + Content.Sink.write(response, true, source, callback); + return true; + } + }); + + ContentResponse response = new CompletableResponseListener(client.newRequest(newURI(transportType)), contentLength) + .send() + .get(555, TimeUnit.SECONDS); + + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertEquals(contentLength, response.getContent().length); + } + + @ParameterizedTest + @MethodSource("transportsNoFCGI") + public void testResponseContentSourceInChunks(TransportType transportType) throws Exception + { + start(transportType, new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + int contentLength = 1024 * 1024; + Path dir = Files.createDirectories(MavenPaths.targetTestDir(getClass().getSimpleName())); + Path file = Files.createTempFile(dir, "file-", ".bin"); + try (var channel = Files.newByteChannel(file, StandardOpenOption.WRITE)) + { + channel.write(ByteBuffer.allocateDirect(contentLength)); + } + + // Write first chunk. + int length1 = contentLength / 2; + try (Blocker.Callback blocker = Blocker.callback()) + { + Content.Sink.write(response, false, Content.Source.from(file, 0, length1), blocker); + blocker.block(); + } + + // Write last chunk. + Content.Sink.write(response, true, Content.Source.from(file, length1, contentLength - length1), callback); + return true; + } + }); + } +}