Skip to content

Commit

Permalink
Override HttpRequestDecoder.createMessage() for perfomance (#4863)
Browse files Browse the repository at this point in the history
Motivation:

By overriding `HttpRequestDecoder.createMessage()`, performance can be
improved because there is no need to copy original netty header.

Result:

- Closes #4853 
- Performance is improved

<!--
Visit this URL to learn more about how to write a pull request
description:

https://armeria.dev/community/developer-guide#how-to-write-pull-request-description
-->

---------

Co-authored-by: jrhee17 <guins_j@guins.org>
Co-authored-by: minwoox <songmw725@gmail.com>
Co-authored-by: Ikhun Um <ih.pert@gmail.com>
  • Loading branch information
4 people committed Apr 11, 2024
1 parent a20911a commit 8a99f23
Show file tree
Hide file tree
Showing 12 changed files with 712 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -670,21 +670,18 @@ public static HttpHeaders toArmeria(Http2Headers http2Headers, boolean request,
* {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}.
*/
public static RequestHeaders toArmeria(
ChannelHandlerContext ctx, HttpRequest in,
ChannelHandlerContext ctx, HttpRequest in, RequestHeadersBuilder out,
ServerConfig cfg, String scheme, RequestTarget reqTarget) throws URISyntaxException {

final io.netty.handler.codec.http.HttpHeaders inHeaders = in.headers();
final RequestHeadersBuilder out = RequestHeaders.builder();
out.sizeHint(inHeaders.size());
out.method(firstNonNull(HttpMethod.tryParse(in.method().name()), HttpMethod.UNKNOWN))
.scheme(scheme)
.path(reqTarget.toString());

// Add the HTTP headers which have not been consumed above
toArmeria(inHeaders, out);
if (!out.contains(HttpHeaderNames.HOST)) {
out.add(HttpHeaderNames.HOST, defaultAuthority(ctx, cfg));
}
purgeHttp1OnlyHeaders(inHeaders, out);
return out.build();
}

Expand Down Expand Up @@ -717,6 +714,8 @@ public static HttpHeaders toArmeria(io.netty.handler.codec.http.HttpHeaders inHe

/**
* Converts the specified Netty HTTP/1 headers into Armeria HTTP/2 headers.
* Functionally, this method is expected to behavior in the same way as
* {@link #purgeHttp1OnlyHeaders(io.netty.handler.codec.http.HttpHeaders, HttpHeadersBuilder)}.
*/
public static void toArmeria(io.netty.handler.codec.http.HttpHeaders inHeaders, HttpHeadersBuilder out) {
final Iterator<Entry<CharSequence, CharSequence>> iter = inHeaders.iteratorCharSequence();
Expand Down Expand Up @@ -762,6 +761,90 @@ public static void toArmeria(io.netty.handler.codec.http.HttpHeaders inHeaders,
maybeSetContentLengthUnknown(inHeaders.contains(HttpHeaderNames.CONTENT_LENGTH), out);
}

/**
* Removes HTTP/1 specified headers from a mutable headers map.
* Functionally this method is expected to behave the same as
* {@link #toArmeria(io.netty.handler.codec.http.HttpHeaders, HttpHeadersBuilder)}.
* This method should be preferred going forward as we continue implementing zero-copy
* for HTTP1 en/decoders.
*/
private static void purgeHttp1OnlyHeaders(io.netty.handler.codec.http.HttpHeaders inHeaders,
HttpHeadersBuilder out) {
//TODO(minwoox): dedup the logic between these method and toArmeria
maybeSetTeHeader(inHeaders, out);
maybeRemoveConnectionHeaders(inHeaders, out);
maybeSetCookie(inHeaders, out);
maybeSetContentLengthUnknown(inHeaders.contains(HttpHeaderNames.CONTENT_LENGTH), out);
}

private static void maybeRemoveConnectionHeaders(io.netty.handler.codec.http.HttpHeaders inHeaders,
HttpHeadersBuilder out) {
final CaseInsensitiveMap connectionDisallowedList =
toLowercaseMap(inHeaders.valueCharSequenceIterator(HttpHeaderNames.CONNECTION), 8);
final boolean isWebSocketUpgrade = isWebSocketUpgrade(inHeaders);
connectionDisallowedList.forEach(entry -> out.remove(entry.getKey()));
HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST.forEach(entry -> out.remove(entry.getKey()));
if (isWebSocketUpgrade) {
out.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE.toString());
out.set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET.toString());
}
}

private static void maybeSetCookie(io.netty.handler.codec.http.HttpHeaders inHeaders,
HttpHeadersBuilder out) {
// Cookies must be concatenated into a single octet string.
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.5
if (out.contains(HttpHeaderNames.COOKIE)) {
final StringJoiner cookieJoiner = new StringJoiner(COOKIE_SEPARATOR);
inHeaders.getAll(HttpHeaderNames.COOKIE).forEach(
value -> COOKIE_SPLITTER.split(value).forEach(cookieJoiner::add));
out.set(HttpHeaderNames.COOKIE, cookieJoiner.toString());
}
}

private static void maybeSetTeHeader(io.netty.handler.codec.http.HttpHeaders inHeaders,
HttpHeadersBuilder out) {
if (!inHeaders.contains(HttpHeaderNames.TE)) {
return;
}
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 makes a special exception for TE
final boolean hasTrailersTe = findDelimitedIgnoreCase(HttpHeaderNames.TE, HttpHeaderValues.TRAILERS,
inHeaders);
if (hasTrailersTe) {
out.set(HttpHeaderNames.TE, HttpHeaderValues.TRAILERS.toString());
} else {
out.remove(HttpHeaderNames.TE);
}
}

private static boolean isWebSocketUpgrade(io.netty.handler.codec.http.HttpHeaders inHeaders) {
final boolean isUpgrade = findDelimitedIgnoreCase(HttpHeaderNames.CONNECTION,
HttpHeaderValues.UPGRADE, inHeaders);
final boolean isWebsocket = findDelimitedIgnoreCase(HttpHeaderNames.UPGRADE,
HttpHeaderValues.WEBSOCKET, inHeaders);
return isUpgrade && isWebsocket;
}

private static boolean findDelimitedIgnoreCase(AsciiString targetName, AsciiString targetValue,
io.netty.handler.codec.http.HttpHeaders httpHeaders) {
final List<String> allValues = httpHeaders.getAll(targetName);
if (allValues.isEmpty()) {
return false;
}
for (String value: allValues) {
if (targetValue.contentEqualsIgnoreCase(value)) {
return true;
}
final List<CharSequence> values = StringUtil.unescapeCsvFields(value);
for (CharSequence field : values) {
if (targetValue.contentEqualsIgnoreCase(AsciiString.trim(field))) {
return true;
}
}
}
return false;
}

private static void maybeSetContentLengthUnknown(boolean hasContentLength, HttpHeadersBuilder out) {
if (hasContentLength) {
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2024 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.server;

import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpHeadersFactory;

enum ArmeriaHttpHeadersFactory implements HttpHeadersFactory {

INSTANCE;

@Override
public HttpHeaders newHeaders() {
return new NettyHttp1Headers();
}

@Override
public HttpHeaders newEmptyHeaders() {
return new NettyHttp1Headers();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,16 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
return;
}

// Convert the Netty HttpHeaders into Armeria RequestHeaders.
assert msg instanceof NettyHttp1Request;
// Precompute values that rely on CONNECTION related headers since they will be cleaned
// after ArmeriaHttpUtil#toArmeria is called
final boolean keepAlive = HttpUtil.isKeepAlive(nettyReq);
final boolean transferEncodingChunked = HttpUtil.isTransferEncodingChunked(nettyReq);

final NettyHttp1Headers nettyHttp1Headers = (NettyHttp1Headers) nettyReq.headers();
final RequestHeaders headers =
ArmeriaHttpUtil.toArmeria(ctx, nettyReq, cfg, scheme.toString(), reqTarget);
ArmeriaHttpUtil.toArmeria(ctx, nettyReq, nettyHttp1Headers.delegate(),
cfg, scheme.toString(), reqTarget);
// Do not accept unsupported methods.
final HttpMethod method = headers.method();
switch (method) {
Expand Down Expand Up @@ -268,8 +275,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
}
}

final boolean keepAlive = HttpUtil.isKeepAlive(nettyReq);
final boolean endOfStream = contentEmpty && !HttpUtil.isTransferEncodingChunked(nettyReq);
final boolean endOfStream = contentEmpty && !transferEncodingChunked;
this.req = req = DecodedHttpRequest.of(endOfStream, eventLoop, id, 1, headers,
keepAlive, inboundTrafficController, routingCtx);

Expand Down
145 changes: 145 additions & 0 deletions core/src/main/java/com/linecorp/armeria/server/HttpServerCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright 2023 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
/*
* Copyright 2012 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.linecorp.armeria.server;

import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_CHUNK_SIZE;
import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_HEADER_SIZE;
import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_INITIAL_LINE_LENGTH;

import java.util.ArrayDeque;
import java.util.List;
import java.util.Queue;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.CombinedChannelDuplexHandler;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.codec.http.HttpServerUpgradeHandler;
import io.netty.handler.codec.http.HttpStatusClass;
import io.netty.handler.codec.http.HttpVersion;

/**
* Forked from {@link HttpClientCodec} to override {@code HttpServerRequestDecoder#createMessage(String[])}.
* A combination of {@link HttpRequestDecoder} and {@link HttpResponseEncoder}
* which enables easier server side HTTP implementation.
*/
final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder>
implements HttpServerUpgradeHandler.SourceCodec {

// Forked from https://github.com/netty/netty/blob/cf624c93c5f97097f1b13fe926ed50c32c8b1430/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java

/** A queue that is used for correlating a request and a response. */
private final Queue<HttpMethod> queue = new ArrayDeque<HttpMethod>();

/**
* Creates a new instance with the default decoder options
* ({@code maxInitialLineLength (4096}}, {@code maxHeaderSize (8192)}, and
* {@code maxChunkSize (8192)}).
*/
HttpServerCodec() {
this(DEFAULT_MAX_INITIAL_LINE_LENGTH, DEFAULT_MAX_HEADER_SIZE, DEFAULT_MAX_CHUNK_SIZE);
}

/**
* Creates a new instance with the specified decoder options.
*/
HttpServerCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) {
init(new HttpServerRequestDecoder(maxInitialLineLength, maxHeaderSize, maxChunkSize),
new HttpServerResponseEncoder());
}

/**
* Upgrades to another protocol from HTTP. Removes the {@link HttpRequestDecoder} and
* {@link HttpResponseEncoder} from the pipeline.
*/
@Override
public void upgradeFrom(ChannelHandlerContext ctx) {
ctx.pipeline().remove(this);
}

private final class HttpServerRequestDecoder extends HttpRequestDecoder {

HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) {
super(maxInitialLineLength, maxHeaderSize, maxChunkSize);
}

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
final int oldSize = out.size();
super.decode(ctx, buffer, out);
final int size = out.size();
for (int i = oldSize; i < size; i++) {
final Object obj = out.get(i);
if (obj instanceof HttpRequest) {
queue.add(((HttpRequest) obj).method());
}
}
}

@Override
protected HttpMessage createMessage(String[] initialLine) throws Exception {
return new NettyHttp1Request(
HttpVersion.valueOf(initialLine[2]),
HttpMethod.valueOf(initialLine[0]), initialLine[1]);
}
}

private final class HttpServerResponseEncoder extends HttpResponseEncoder {

private HttpMethod method;

@Override
protected void sanitizeHeadersBeforeEncode(HttpResponse msg, boolean isAlwaysEmpty) {
if (!isAlwaysEmpty && HttpMethod.CONNECT.equals(method) &&
msg.status().codeClass() == HttpStatusClass.SUCCESS) {
// Stripping Transfer-Encoding:
// See section 3.3.1 of https://datatracker.ietf.org/doc/rfc7230
msg.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
return;
}

super.sanitizeHeadersBeforeEncode(msg, isAlwaysEmpty);
}

@Override
protected boolean isContentAlwaysEmpty(@SuppressWarnings("unused") HttpResponse msg) {
method = queue.poll();
return HttpMethod.HEAD.equals(method) || super.isContentAlwaysEmpty(msg);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
import io.netty.handler.codec.haproxy.HAProxyMessage;
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol.AddressFamily;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http2.DefaultHttp2Connection;
import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder;
import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http2.Http2CodecUtil;
import io.netty.util.AsciiString;
Expand Down

0 comments on commit 8a99f23

Please sign in to comment.