Skip to content
This repository has been archived by the owner on Nov 9, 2020. It is now read-only.

Commit

Permalink
Downgrade to Netty 4.1.0.Beta8
Browse files Browse the repository at this point in the history
We are downgrading from Netty 5.0alpha 2 to Netty 4.1.0.Beta8 for two
reasons:

1. There is a serious blocking bug in the HTTP/2 implementation in
5.0a2. It's fixed in 5.0a3, but there are no plans to release that any
time soon. I asked the Netty maintainers, and they recommended
downgrading to 4.1.b8: it's stable and fixes our HTTP/2 problems.

Bug: netty/netty@44ee2ca
Discussion about version: netty/netty#4586

2. Netty 4.1 is getting more attention. 5.0alpha2 was released in
March with no updates since and none planned. Netty 4.1 had updates in
September, October, and November.

Included in this change:
* Downgrade to Netty 4.1.0.Beta8
* Adjustments to code: the Netty API is slightly different
* Removed use of WebCompression for websockets in websocket test
  because it's not in Netty 4.1. Test still passes, and we don't
  support compression on the server side.
* Added test that sends more HTTP/2 data. This exposes the bug in
  Netty 5.0alpha2 and it passes with Netty 4.1.Beta8

Testing:
* Passed in jenkins
* Ran dcp-local-tests three times against this change (each with four
Jenkins workers) and it passed.

Performance:
I did a sanity check to validate that this didn't regress HTTP/1.1
performance. I ran NettyHttpServiceClientTest.throughputGetRemote
against http://127.0.0.1:8000/core/examples/stats using the
ExampleHost with 100,000 operations. The median peak operations/second
was 54112. That's slightly higher than my previous tests with
5.0alpha2 (which were about ~53000), but within the realm of our test
variance.

Caveat:
One test intermittently fails: validateStreamExhaustion, so it has
been disabled. This test constantly opens new connections. Eventually
one of them fails because the client believes it has sent the HTTP/2
SETTINGS frame, but the server does not receive it. We work around
this by retrying the connection if it fails. This is worrisome: until
we have a real understanding. Until we have a real solution, we should
consider HTTP/2 support experimental.

Change-Id: I8d23c64cf68d0a2536e953d96f4664bd74d659d5
  • Loading branch information
Alain Roy committed Dec 21, 2015
1 parent a4b28c1 commit 975d61e
Show file tree
Hide file tree
Showing 19 changed files with 365 additions and 338 deletions.
7 changes: 4 additions & 3 deletions LICENSE
Expand Up @@ -237,7 +237,7 @@ SECTION 2: Apache License, V2.0

>>> com.google.code.gson:gson-2.3.1
>>> com.twitter:hpack-v1.0.1
>>> io.netty:netty-all-5.0.0.alpha2
>>> io.netty:netty-all-4.1.0.Beta8
>>> org.apache.lucene:lucene-analyzers-common-5.2.1
>>> org.apache.lucene:lucene-core-5.2.1
>>> org.apache.lucene:lucene-misc-5.2.1
Expand Down Expand Up @@ -340,12 +340,13 @@ Copyright 2013 Twitter, Inc.

Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0

>>> io.netty:netty-all-5.0.0.alpha2
>>> io.netty:netty-all-4.1.0.Beta8

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:
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:

http:www.apache.org/licenses/LICENSE-2.0

Expand Down
2 changes: 1 addition & 1 deletion xenon-common/pom.xml
Expand Up @@ -14,7 +14,7 @@
<gson.version>2.3.1</gson.version>
<kryo.version>3.0.2</kryo.version>
<lucene.version>5.3.1</lucene.version>
<netty.version>5.0.0.Alpha2</netty.version>
<netty.version>4.1.0.Beta8</netty.version>
<hpack.version>v1.0.1</hpack.version>
</properties>

Expand Down
Expand Up @@ -22,7 +22,7 @@
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ConcurrentSkipListSet;

import io.netty.handler.codec.http.Cookie;
import io.netty.handler.codec.http.cookie.Cookie;

/**
* The cookie jar keeps all cookies for a client. Upon receiving a Set-Cookie response header, the cookie jar may store
Expand Down
Expand Up @@ -46,6 +46,7 @@ public class NettyChannelContext extends SocketContext {
public static final int MAX_HEADER_SIZE = 65536;
public static final int MAX_CHUNK_SIZE = 65536;
public static final int MAX_HTTP2_FRAME_SIZE = 65536;
public static final int INITIAL_HTTP2_WINDOW_SIZE = 65536;

// An HTTP/2 connection uses a unique stream for each request/response
// The stream ID is 31 bits longs and the client can only use odd-numbered
Expand Down
Expand Up @@ -71,6 +71,7 @@ public static String toConnectionKey(String host, int port) {
}

private final ExecutorService executor;
private ExecutorService nettyExecutorService;
private EventLoopGroup eventGroup;
private String threadTag = NettyChannelPool.class.getSimpleName();
private int threadCount;
Expand Down Expand Up @@ -117,10 +118,9 @@ public void start() {
return;
}

this.eventGroup = new NioEventLoopGroup(this.threadCount, (t) -> {
return Executors.newFixedThreadPool(t,
r -> new Thread(r, this.threadTag));
});
this.nettyExecutorService = Executors.newFixedThreadPool(this.threadCount, r -> new Thread(r, this.threadTag));
this.eventGroup = new NioEventLoopGroup(this.threadCount, this.nettyExecutorService);

this.bootStrap = new Bootstrap();
this.bootStrap.group(this.eventGroup)
.channel(NioSocketChannel.class)
Expand Down Expand Up @@ -200,6 +200,23 @@ public void connectOrReuse(String host, int port, Operation request) {
return;
}

// Sometimes when an HTTP/2 connection is exhausted and we open
// a new connection, the connection fails: it appears that the client
// believes it has sent the HTTP/2 settings frame, but the server has
// not received it. After hours of debugging, I don't believe the cause
// is our fault, but haven't isolated an underlying bug in Netty either.
//
// The workaround is that retry the connection. I haven't yet seen a failure
// when we retry.
//
// This doesn't make me completely comfortable. Is it really the case that
// we just occasionally lose the SETTINGS frame on a new connection, or can
// other frames be lost? Until we are sure, HTTP/2 support should be considered
// experimental.
if (this.isHttp2Only && request.getRetryCount() == 0) {
request.setRetryCount(2);
}

// Connect, then wait for the connection to complete before either
// sending data (HTTP/1.1) or negotiating settings (HTTP/2)
ChannelFuture connectFuture = this.bootStrap.connect(context.host, context.port);
Expand Down Expand Up @@ -516,26 +533,18 @@ private void returnOrCloseDirectHttp2(NettyChannelContext context, NettyChannelG
return;
}

// If we're closing the connection, we only send one request in the new connection
// If we're not closing it, we queue up multiple pending requests, because
// HTTP/2 allows us to multiplex requests.
Operation pendingOp = null;
if (isClose) {
if (isClose || !context.isValid()) {
synchronized (group) {
pendingOp = group.pendingRequests.remove(group.pendingRequests.size() - 1);
}
connectOrReuse(context.host, context.port, pendingOp);
} else {
int i = 0;
synchronized (group) {
while (havePending && i < this.connectionLimit) {
pendingOp = group.pendingRequests.remove(0);
havePending = !group.pendingRequests.isEmpty();
pendingOp.setSocketContext(context);
pendingOp.complete();
i++;
}
pendingOp = group.pendingRequests.remove(0);
}
pendingOp.setSocketContext(context);
pendingOp.complete();
}
}

Expand All @@ -558,6 +567,7 @@ public void stop() {
}
}
this.eventGroup.shutdownGracefully();
this.nettyExecutorService.shutdown();
} catch (Throwable e) {
// ignore exception
}
Expand Down
Expand Up @@ -15,8 +15,8 @@

import java.util.logging.Level;

import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.HttpClientUpgradeHandler;

import com.vmware.xenon.common.Utils;
Expand All @@ -27,7 +27,7 @@
* attempts (that's interesting) unless debugLogging is enabled, in which case
* we log all events.
*/
public class NettyHttp2UserEventLogger extends ChannelHandlerAdapter {
public class NettyHttp2UserEventLogger extends ChannelInboundHandlerAdapter {

private boolean debugLogging = false;

Expand Down
Expand Up @@ -29,19 +29,19 @@
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.HttpUtil;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.AsciiString;

import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.Operation.AuthorizationContext;
Expand Down Expand Up @@ -78,7 +78,7 @@ public boolean acceptInboundMessage(Object msg) throws Exception {
}

@Override
protected void messageReceived(ChannelHandlerContext ctx, Object msg) {
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
Operation request = null;
Integer streamId = null;
try {
Expand All @@ -95,7 +95,7 @@ protected void messageReceived(ChannelHandlerContext ctx, Object msg) {

// The streamId will be null for HTTP/1.1 connections, and valid for HTTP/2 connections
streamId = nettyRequest.headers().getInt(
HttpUtil.ExtensionHeaderNames.STREAM_ID.text());
HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
if (streamId == null) {
ctx.channel().attr(NettyChannelContext.OPERATION_KEY).set(request);
}
Expand Down Expand Up @@ -151,7 +151,7 @@ private void parseRequestHeaders(ChannelHandlerContext ctx, Operation request,
HttpRequest nettyRequest) {
HttpHeaders headers = nettyRequest.headers();

String referer = headers.getAndRemoveAndConvert(HttpHeaderNames.REFERER);
String referer = getAndRemove(headers, HttpHeaderNames.REFERER);
if (referer != null) {
try {
request.setReferer(new URI(referer));
Expand All @@ -166,24 +166,24 @@ private void parseRequestHeaders(ChannelHandlerContext ctx, Operation request,
return;
}

request.setKeepAlive(HttpHeaderUtil.isKeepAlive(nettyRequest));
if (HttpHeaderUtil.isContentLengthSet(nettyRequest)) {
request.setContentLength(HttpHeaderUtil.getContentLength(nettyRequest));
request.setKeepAlive(HttpUtil.isKeepAlive(nettyRequest));
if (HttpUtil.isContentLengthSet(nettyRequest)) {
request.setContentLength(HttpUtil.getContentLength(nettyRequest));
}

request.setContextId(headers.getAndRemoveAndConvert(Operation.CONTEXT_ID_HEADER));
request.setContextId(getAndRemove(headers, Operation.CONTEXT_ID_HEADER));

String contentType = headers.getAndRemoveAndConvert(HttpHeaderNames.CONTENT_TYPE);
String contentType = getAndRemove(headers, HttpHeaderNames.CONTENT_TYPE);
if (contentType != null) {
request.setContentType(contentType);
}

String cookie = headers.getAndRemoveAndConvert(HttpHeaderNames.COOKIE);
String cookie = getAndRemove(headers, HttpHeaderNames.COOKIE);
if (cookie != null) {
request.setCookies(CookieJar.decodeCookies(cookie));
}

for (Entry<String, String> h : headers.entriesConverted()) {
for (Entry<String, String> h : headers) {
String key = h.getKey();
String value = h.getValue();
request.addRequestHeader(key, value);
Expand All @@ -204,6 +204,18 @@ private void parseRequestHeaders(ChannelHandlerContext ctx, Operation request,
}
}

private String getAndRemove(HttpHeaders headers, AsciiString headerName) {
String headerValue = headers.get(headerName.toString());
headers.remove(headerName);
return headerValue;
}

private String getAndRemove(HttpHeaders headers, String headerName) {
String headerValue = headers.get(headerName);
headers.remove(headerName);
return headerValue;
}

private void submitRequest(ChannelHandlerContext ctx, Operation request, Integer streamId) {
request.nestCompletion((o, e) -> {
request.setBodyNoCloning(o.getBodyRaw());
Expand Down Expand Up @@ -301,7 +313,8 @@ private void writeResponseUnsafe(ChannelHandlerContext ctx, Operation request, I
HttpResponseStatus.INTERNAL_SERVER_ERROR,
Unpooled.wrappedBuffer(data), false, false);
if (streamId != null) {
response.headers().setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), streamId);
response.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(),
streamId);
}
response.headers().set(HttpHeaderNames.CONTENT_TYPE, Operation.MEDIA_TYPE_TEXT_HTML);
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH,
Expand All @@ -322,7 +335,8 @@ private void writeResponseUnsafe(ChannelHandlerContext ctx, Operation request, I
// This is the stream ID from the incoming request: we need to use it for our
// response so the client knows this is the response. If we don't set the stream
// ID, Netty assigns a new, unused stream, which would be bad.
response.headers().setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), streamId);
response.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(),
streamId);
}
response.headers().set(HttpHeaderNames.CONTENT_TYPE,
request.getContentType());
Expand Down

0 comments on commit 975d61e

Please sign in to comment.