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 IterableReturns 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
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 AtomicReferenceA {@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