-
Notifications
You must be signed in to change notification settings - Fork 25.9k
Add http request content stream support #111438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2ff3afa
07f97cc
5d44698
839e575
83bec15
14a06ce
fbf5d28
116e802
d34d47f
90c8b5d
cd53cd7
7b66986
e0899b5
6dc8e46
f140b7d
660e4ca
aada8ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
| * in compliance with, at your election, the Elastic License 2.0 or the Server | ||
| * Side Public License, v 1. | ||
| */ | ||
|
|
||
| package org.elasticsearch.http.netty4; | ||
|
|
||
| import io.netty.handler.codec.http.HttpObjectAggregator; | ||
| import io.netty.handler.codec.http.HttpRequest; | ||
|
|
||
| import org.elasticsearch.http.HttpPreRequest; | ||
| import org.elasticsearch.http.netty4.internal.HttpHeadersAuthenticatorUtils; | ||
|
|
||
| import java.util.function.Predicate; | ||
|
|
||
| public class Netty4HttpAggregator extends HttpObjectAggregator { | ||
| private static final Predicate<HttpPreRequest> IGNORE_TEST = (req) -> req.uri().startsWith("/_test/request-stream") == false; | ||
|
|
||
| private final Predicate<HttpPreRequest> decider; | ||
| private boolean shouldAggregate; | ||
|
|
||
| public Netty4HttpAggregator(int maxContentLength) { | ||
| this(maxContentLength, IGNORE_TEST); | ||
| } | ||
|
|
||
| public Netty4HttpAggregator(int maxContentLength, Predicate<HttpPreRequest> decider) { | ||
| super(maxContentLength); | ||
| this.decider = decider; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean acceptInboundMessage(Object msg) throws Exception { | ||
| if (msg instanceof HttpRequest request) { | ||
| var preReq = HttpHeadersAuthenticatorUtils.asHttpPreRequest(request); | ||
| shouldAggregate = decider.test(preReq); | ||
| } | ||
| if (shouldAggregate) { | ||
| return super.acceptInboundMessage(msg); | ||
| } else { | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,8 +19,11 @@ | |
| import io.netty.handler.codec.http.DefaultHttpResponse; | ||
| import io.netty.handler.codec.http.DefaultLastHttpContent; | ||
| import io.netty.handler.codec.http.FullHttpRequest; | ||
| import io.netty.handler.codec.http.HttpContent; | ||
| import io.netty.handler.codec.http.HttpObject; | ||
| import io.netty.handler.codec.http.HttpRequest; | ||
| import io.netty.handler.codec.http.HttpResponse; | ||
| import io.netty.handler.codec.http.LastHttpContent; | ||
| import io.netty.handler.ssl.SslCloseCompletionEvent; | ||
| import io.netty.util.ReferenceCountUtil; | ||
| import io.netty.util.concurrent.Future; | ||
|
|
@@ -70,6 +73,12 @@ private record ChunkedWrite(PromiseCombiner combiner, ChannelPromise onDone, Chu | |
| @Nullable | ||
| private ChunkedWrite currentChunkedWrite; | ||
|
|
||
| /** | ||
| * HTTP request content stream for current request, it's null if there is no current request or request is fully-aggregated | ||
| */ | ||
| @Nullable | ||
| private Netty4HttpRequestBodyStream currentRequestStream; | ||
|
|
||
| /* | ||
| * The current read and write sequence numbers. Read sequence numbers are attached to requests in the order they are read from the | ||
| * channel, and then transferred to responses. A response is not written to the channel context until its sequence number matches the | ||
|
|
@@ -109,23 +118,38 @@ public Netty4HttpPipeliningHandler( | |
| public void channelRead(final ChannelHandlerContext ctx, final Object msg) { | ||
| activityTracker.startActivity(); | ||
| try { | ||
| assert msg instanceof FullHttpRequest : "Should have fully aggregated message already but saw [" + msg + "]"; | ||
| final FullHttpRequest fullHttpRequest = (FullHttpRequest) msg; | ||
| final Netty4HttpRequest netty4HttpRequest; | ||
| if (fullHttpRequest.decoderResult().isFailure()) { | ||
| final Throwable cause = fullHttpRequest.decoderResult().cause(); | ||
| final Exception nonError; | ||
| if (cause instanceof Error) { | ||
| ExceptionsHelper.maybeDieOnAnotherThread(cause); | ||
| nonError = new Exception(cause); | ||
| if (msg instanceof HttpRequest request) { | ||
| final Netty4HttpRequest netty4HttpRequest; | ||
| if (request.decoderResult().isFailure()) { | ||
| final Throwable cause = request.decoderResult().cause(); | ||
| final Exception nonError; | ||
| if (cause instanceof Error) { | ||
| ExceptionsHelper.maybeDieOnAnotherThread(cause); | ||
| nonError = new Exception(cause); | ||
| } else { | ||
| nonError = (Exception) cause; | ||
| } | ||
| netty4HttpRequest = new Netty4HttpRequest(readSequence++, (FullHttpRequest) request, nonError); | ||
| } else { | ||
| nonError = (Exception) cause; | ||
| assert currentRequestStream == null : "current stream must be null for new request"; | ||
| if (request instanceof FullHttpRequest fullHttpRequest) { | ||
| netty4HttpRequest = new Netty4HttpRequest(readSequence++, fullHttpRequest); | ||
| currentRequestStream = null; | ||
| } else { | ||
| var contentStream = new Netty4HttpRequestBodyStream(ctx.channel()); | ||
| currentRequestStream = contentStream; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similarly, can we assert
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It can be non-null if previous request was stream too. When we see HttpRequest that means we received all parts of previous request - all HttpContent's and single LastHttpContent. At this point previous parts should be either in previous stream queue or processed by rest handler.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We set
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, sorry I lost track of my own changes :) In normal circumstances should be null. If request is not properly terminated (no last content) and there is new request then
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ++ to a test, but could we also |
||
| netty4HttpRequest = new Netty4HttpRequest(readSequence++, request, contentStream); | ||
| } | ||
| } | ||
| netty4HttpRequest = new Netty4HttpRequest(readSequence++, fullHttpRequest, nonError); | ||
| handlePipelinedRequest(ctx, netty4HttpRequest); | ||
| } else { | ||
| netty4HttpRequest = new Netty4HttpRequest(readSequence++, fullHttpRequest); | ||
| assert msg instanceof HttpContent : "expect HttpContent got " + msg; | ||
| assert currentRequestStream != null : "current stream must exists before handling http content"; | ||
| currentRequestStream.handleNettyContent((HttpContent) msg); | ||
| if (msg instanceof LastHttpContent) { | ||
| currentRequestStream = null; | ||
| } | ||
| } | ||
| handlePipelinedRequest(ctx, netty4HttpRequest); | ||
| } finally { | ||
| activityTracker.stopActivity(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
| * in compliance with, at your election, the Elastic License 2.0 or the Server | ||
| * Side Public License, v 1. | ||
| */ | ||
|
|
||
| package org.elasticsearch.http.netty4; | ||
|
|
||
| import io.netty.channel.Channel; | ||
| import io.netty.handler.codec.http.HttpContent; | ||
| import io.netty.handler.codec.http.LastHttpContent; | ||
|
|
||
| import org.elasticsearch.http.HttpBody; | ||
| import org.elasticsearch.transport.netty4.Netty4Utils; | ||
|
|
||
| import java.util.ArrayDeque; | ||
| import java.util.Queue; | ||
|
|
||
| /** | ||
| * Netty based implementation of {@link HttpBody.Stream}. | ||
| * This implementation utilize {@link io.netty.channel.ChannelConfig#setAutoRead(boolean)} | ||
| * to prevent entire payload buffering. But sometimes upstream can send few chunks of data despite | ||
| * autoRead=off. In this case chunks will be queued until downstream calls {@link Stream#next()} | ||
| */ | ||
| public class Netty4HttpRequestBodyStream implements HttpBody.Stream { | ||
|
|
||
| private final Channel channel; | ||
| private final Queue<HttpContent> chunkQueue = new ArrayDeque<>(); | ||
| private boolean requested = false; | ||
| private boolean hasLast = false; | ||
| private HttpBody.ChunkHandler handler; | ||
|
|
||
| public Netty4HttpRequestBodyStream(Channel channel) { | ||
| this.channel = channel; | ||
| channel.closeFuture().addListener((f) -> releaseQueuedChunks()); | ||
| channel.config().setAutoRead(false); | ||
| } | ||
|
|
||
| @Override | ||
| public ChunkHandler handler() { | ||
| return handler; | ||
| } | ||
|
|
||
| @Override | ||
| public void setHandler(ChunkHandler chunkHandler) { | ||
| this.handler = chunkHandler; | ||
| } | ||
|
|
||
| private void sendQueuedOrRead() { | ||
| assert channel.eventLoop().inEventLoop(); | ||
| requested = true; | ||
| var chunk = chunkQueue.poll(); | ||
| if (chunk == null) { | ||
| channel.read(); | ||
| } else { | ||
| sendChunk(chunk); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void next() { | ||
| assert handler != null : "handler must be set before requesting next chunk"; | ||
| if (channel.eventLoop().inEventLoop()) { | ||
| sendQueuedOrRead(); | ||
| } else { | ||
| channel.eventLoop().submit(this::sendQueuedOrRead); | ||
| } | ||
| } | ||
|
|
||
| public void handleNettyContent(HttpContent httpContent) { | ||
| assert handler != null : "handler must be set before processing http content"; | ||
| if (requested && chunkQueue.isEmpty()) { | ||
| sendChunk(httpContent); | ||
| } else { | ||
| chunkQueue.add(httpContent); | ||
| } | ||
| if (httpContent instanceof LastHttpContent) { | ||
| hasLast = true; | ||
| channel.config().setAutoRead(true); | ||
| } | ||
| } | ||
|
|
||
| // visible for test | ||
| Channel channel() { | ||
| return channel; | ||
| } | ||
|
|
||
| // visible for test | ||
| Queue<HttpContent> chunkQueue() { | ||
| return chunkQueue; | ||
| } | ||
|
|
||
| // visible for test | ||
| boolean hasLast() { | ||
| return hasLast; | ||
| } | ||
|
|
||
| private void sendChunk(HttpContent httpContent) { | ||
| assert requested; | ||
| requested = false; | ||
| var bytesRef = Netty4Utils.toReleasableBytesReference(httpContent.content()); | ||
| var isLast = httpContent instanceof LastHttpContent; | ||
| handler.onNext(bytesRef, isLast); | ||
| } | ||
|
|
||
| private void releaseQueuedChunks() { | ||
| while (chunkQueue.isEmpty() == false) { | ||
| chunkQueue.poll().release(); | ||
| } | ||
| } | ||
|
|
||
| } |
Uh oh!
There was an error while loading. Please reload this page.