Skip to content

Commit 888accf

Browse files
authored
Fix netty memory leak (#12003)
1 parent b231d34 commit 888accf

File tree

9 files changed

+106
-59
lines changed

9 files changed

+106
-59
lines changed

instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/Helpers.java

+3-5
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,11 @@
1818
import io.opentelemetry.context.Context;
1919
import io.opentelemetry.instrumentation.api.util.VirtualField;
2020
import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys;
21-
import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContext;
21+
import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContexts;
2222
import io.opentelemetry.instrumentation.netty.v4_1.internal.client.HttpClientTracingHandler;
2323
import io.opentelemetry.instrumentation.netty.v4_1.internal.server.HttpServerTracingHandler;
2424
import io.opentelemetry.javaagent.instrumentation.netty.v4_1.NettyHttpServerResponseBeforeCommitHandler;
2525
import io.opentelemetry.javaagent.instrumentation.netty.v4_1.NettyServerSingletons;
26-
import java.util.Deque;
2726

2827
public final class Helpers {
2928

@@ -42,9 +41,8 @@ protected void initChannel(C channel) throws Exception {
4241
// the parent channel is the original http/1.1 channel and has the contexts stored in it;
4342
// we assign to this new channel as the old one will not be evaluated in the upgraded h2c
4443
// chain
45-
Deque<ServerContext> serverContexts =
46-
channel.parent().attr(AttributeKeys.SERVER_CONTEXT).get();
47-
channel.attr(AttributeKeys.SERVER_CONTEXT).set(serverContexts);
44+
ServerContexts serverContexts = ServerContexts.get(channel.parent());
45+
channel.attr(AttributeKeys.SERVER_CONTEXTS).set(serverContexts);
4846

4947
// todo add way to propagate the protocol version override up to the netty instrumentation;
5048
// why: the netty instrumentation extracts the http protocol version from the HttpRequest

instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/AbstractChannelHandlerContextInstrumentation.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel;
1919
import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys;
2020
import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContext;
21+
import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContexts;
2122
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
2223
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
23-
import java.util.Deque;
2424
import net.bytebuddy.asm.Advice;
2525
import net.bytebuddy.description.type.TypeDescription;
2626
import net.bytebuddy.matcher.ElementMatcher;
@@ -61,8 +61,7 @@ public static void onEnter(
6161
instrumenter().end(clientContext, request, null, throwable);
6262
return;
6363
}
64-
Deque<ServerContext> serverContexts = ctx.channel().attr(AttributeKeys.SERVER_CONTEXT).get();
65-
ServerContext serverContext = serverContexts != null ? serverContexts.peekFirst() : null;
64+
ServerContext serverContext = ServerContexts.peekFirst(ctx.channel());
6665
if (serverContext != null) {
6766
NettyErrorHolder.set(serverContext.context(), throwable);
6867
}

instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/AttributeKeys.java

+3-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import io.netty.util.AttributeKey;
99
import io.opentelemetry.context.Context;
10-
import java.util.Deque;
1110

1211
/**
1312
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
@@ -17,9 +16,9 @@ public final class AttributeKeys {
1716

1817
// this is the context that has the server span
1918
//
20-
// note: this attribute key is also used by ratpack instrumentation
21-
public static final AttributeKey<Deque<ServerContext>> SERVER_CONTEXT =
22-
AttributeKey.valueOf(AttributeKeys.class, "server-context");
19+
// note: this attribute key is also used by finagle instrumentation
20+
public static final AttributeKey<ServerContexts> SERVER_CONTEXTS =
21+
AttributeKey.valueOf(AttributeKeys.class, "server-contexts");
2322

2423
public static final AttributeKey<Context> CLIENT_CONTEXT =
2524
AttributeKey.valueOf(AttributeKeys.class, "client-context");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.netty.v4_1.internal;
7+
8+
import io.netty.channel.Channel;
9+
import io.netty.util.Attribute;
10+
import java.util.ArrayDeque;
11+
import java.util.Deque;
12+
13+
/**
14+
* A helper class for keeping track of incoming requests and spans associated with them.
15+
*
16+
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
17+
* at any time.
18+
*/
19+
public final class ServerContexts {
20+
private static final int PIPELINING_LIMIT = 1000;
21+
// With http pipelining multiple requests can be sent on the same connection. Responses should be
22+
// sent in the same order the requests came in. We use this deque to store the request context
23+
// and pop elements as responses are sent.
24+
private final Deque<ServerContext> serverContexts = new ArrayDeque<>();
25+
private volatile boolean broken = false;
26+
27+
private ServerContexts() {}
28+
29+
public static ServerContexts get(Channel channel) {
30+
return channel.attr(AttributeKeys.SERVER_CONTEXTS).get();
31+
}
32+
33+
public static ServerContexts getOrCreate(Channel channel) {
34+
Attribute<ServerContexts> attribute = channel.attr(AttributeKeys.SERVER_CONTEXTS);
35+
ServerContexts result = attribute.get();
36+
if (result == null) {
37+
result = new ServerContexts();
38+
attribute.set(result);
39+
}
40+
return result;
41+
}
42+
43+
public static ServerContext peekFirst(Channel channel) {
44+
ServerContexts serverContexts = get(channel);
45+
return serverContexts != null ? serverContexts.peekFirst() : null;
46+
}
47+
48+
public ServerContext peekFirst() {
49+
return serverContexts.peekFirst();
50+
}
51+
52+
public ServerContext peekLast() {
53+
return serverContexts.peekFirst();
54+
}
55+
56+
public ServerContext pollFirst() {
57+
return serverContexts.pollFirst();
58+
}
59+
60+
public ServerContext pollLast() {
61+
return serverContexts.pollLast();
62+
}
63+
64+
public void addLast(ServerContext context) {
65+
if (broken) {
66+
return;
67+
}
68+
// If the pipelining limit is exceeded we'll stop tracing and mark the channel as broken.
69+
// Exceeding the limit indicates that there is good chance that server context are not removed
70+
// from the deque and there could be a memory leak. This could happen when http server decides
71+
// not to send response to some requests, for example see
72+
// https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/11942
73+
if (serverContexts.size() > PIPELINING_LIMIT) {
74+
broken = true;
75+
serverContexts.clear();
76+
}
77+
serverContexts.addLast(context);
78+
}
79+
}

instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/server/HttpServerRequestTracingHandler.java

+7-20
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,12 @@
1010
import io.netty.channel.ChannelInboundHandlerAdapter;
1111
import io.netty.handler.codec.http.HttpRequest;
1212
import io.netty.handler.codec.http.HttpResponse;
13-
import io.netty.util.Attribute;
14-
import io.netty.util.AttributeKey;
1513
import io.opentelemetry.context.Context;
1614
import io.opentelemetry.context.Scope;
1715
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
1816
import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel;
19-
import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys;
2017
import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContext;
21-
import java.util.ArrayDeque;
22-
import java.util.Deque;
18+
import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContexts;
2319

2420
/**
2521
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
@@ -37,7 +33,7 @@ public HttpServerRequestTracingHandler(
3733
@Override
3834
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
3935
Channel channel = ctx.channel();
40-
Deque<ServerContext> serverContexts = getOrCreate(channel, AttributeKeys.SERVER_CONTEXT);
36+
ServerContexts serverContexts = ServerContexts.getOrCreate(channel);
4137

4238
if (!(msg instanceof HttpRequest)) {
4339
ServerContext serverContext = serverContexts.peekLast();
@@ -66,17 +62,18 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
6662
// the span is ended normally in HttpServerResponseTracingHandler
6763
} catch (Throwable throwable) {
6864
// make sure to remove the server context on end() call
69-
ServerContext serverContext = serverContexts.removeLast();
70-
instrumenter.end(serverContext.context(), serverContext.request(), null, throwable);
65+
ServerContext serverContext = serverContexts.pollLast();
66+
if (serverContext != null) {
67+
instrumenter.end(serverContext.context(), serverContext.request(), null, throwable);
68+
}
7169
throw throwable;
7270
}
7371
}
7472

7573
@Override
7674
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
7775
// connection was closed, close all remaining requests
78-
Attribute<Deque<ServerContext>> contextAttr = ctx.channel().attr(AttributeKeys.SERVER_CONTEXT);
79-
Deque<ServerContext> serverContexts = contextAttr.get();
76+
ServerContexts serverContexts = ServerContexts.get(ctx.channel());
8077

8178
if (serverContexts == null) {
8279
super.channelInactive(ctx);
@@ -89,14 +86,4 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
8986
}
9087
super.channelInactive(ctx);
9188
}
92-
93-
private static <T> Deque<T> getOrCreate(Channel channel, AttributeKey<Deque<T>> key) {
94-
Attribute<Deque<T>> attribute = channel.attr(key);
95-
Deque<T> deque = attribute.get();
96-
if (deque == null) {
97-
deque = new ArrayDeque<>();
98-
attribute.set(deque);
99-
}
100-
return deque;
101-
}
10289
}

instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/server/HttpServerResponseTracingHandler.java

+5-11
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,16 @@
1313
import io.netty.handler.codec.http.HttpResponse;
1414
import io.netty.handler.codec.http.HttpResponseStatus;
1515
import io.netty.handler.codec.http.LastHttpContent;
16-
import io.netty.util.Attribute;
1716
import io.netty.util.AttributeKey;
1817
import io.opentelemetry.context.Context;
1918
import io.opentelemetry.context.Scope;
2019
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
2120
import io.opentelemetry.instrumentation.netty.common.internal.NettyErrorHolder;
2221
import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel;
23-
import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys;
2422
import io.opentelemetry.instrumentation.netty.v4_1.internal.ProtocolEventHandler;
2523
import io.opentelemetry.instrumentation.netty.v4_1.internal.ProtocolSpecificEvent;
2624
import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContext;
27-
import java.util.Deque;
25+
import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContexts;
2826
import javax.annotation.Nullable;
2927

3028
/**
@@ -51,12 +49,8 @@ public HttpServerResponseTracingHandler(
5149

5250
@Override
5351
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) throws Exception {
54-
Attribute<Deque<ServerContext>> serverContextAttr =
55-
ctx.channel().attr(AttributeKeys.SERVER_CONTEXT);
56-
57-
Deque<ServerContext> serverContexts = serverContextAttr.get();
52+
ServerContexts serverContexts = ServerContexts.get(ctx.channel());
5853
ServerContext serverContext = serverContexts != null ? serverContexts.peekFirst() : null;
59-
6054
if (serverContext == null) {
6155
super.write(ctx, msg, prm);
6256
return;
@@ -86,7 +80,7 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) thr
8680
} else {
8781
// Headers and body all sent together, we have the response information in the msg.
8882
beforeCommitHandler.handle(serverContext.context(), (HttpResponse) msg);
89-
serverContexts.removeFirst();
83+
serverContexts.pollFirst();
9084
writePromise.addListener(
9185
future ->
9286
end(
@@ -102,7 +96,7 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) thr
10296
// Body sent after headers. We stored the response information in the context when
10397
// encountering HttpResponse (which was not FullHttpResponse since it's not
10498
// LastHttpContent).
105-
serverContexts.removeFirst();
99+
serverContexts.pollFirst();
106100
HttpResponse response = ctx.channel().attr(HTTP_SERVER_RESPONSE).getAndSet(null);
107101
writePromise.addListener(
108102
future ->
@@ -130,7 +124,7 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) thr
130124
try (Scope ignored = serverContext.context().makeCurrent()) {
131125
super.write(ctx, msg, writePromise);
132126
} catch (Throwable throwable) {
133-
serverContexts.removeFirst();
127+
serverContexts.pollFirst();
134128
end(serverContext.context(), serverContext.request(), null, throwable);
135129
throw throwable;
136130
}

instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/TracingHandler.java

+3-7
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@
99
import static io.opentelemetry.javaagent.instrumentation.ratpack.RatpackSingletons.updateServerSpanName;
1010
import static io.opentelemetry.javaagent.instrumentation.ratpack.RatpackSingletons.updateSpanNames;
1111

12-
import io.netty.util.Attribute;
1312
import io.opentelemetry.context.Scope;
14-
import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys;
1513
import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContext;
14+
import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContexts;
1615
import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge;
17-
import java.util.Deque;
1816
import ratpack.handling.Context;
1917
import ratpack.handling.Handler;
2018

@@ -26,10 +24,8 @@ public final class TracingHandler implements Handler {
2624

2725
@Override
2826
public void handle(Context ctx) {
29-
Attribute<Deque<ServerContext>> serverContextAttribute =
30-
ctx.getDirectChannelAccess().getChannel().attr(AttributeKeys.SERVER_CONTEXT);
31-
Deque<ServerContext> serverContexts = serverContextAttribute.get();
32-
ServerContext serverContext = serverContexts != null ? serverContexts.peekFirst() : null;
27+
ServerContext serverContext =
28+
ServerContexts.peekFirst(ctx.getDirectChannelAccess().getChannel());
3329

3430
// Must use context from channel, as executor instrumentation is not accurate - Ratpack
3531
// internally queues events and then drains them in batches, causing executor instrumentation to

instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/reactornetty/ContextHandlerInstrumentation.java

+2-4
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@
1010

1111
import io.netty.channel.Channel;
1212
import io.opentelemetry.context.Scope;
13-
import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys;
1413
import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContext;
14+
import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContexts;
1515
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
1616
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
17-
import java.util.Deque;
1817
import net.bytebuddy.asm.Advice;
1918
import net.bytebuddy.description.type.TypeDescription;
2019
import net.bytebuddy.matcher.ElementMatcher;
@@ -39,8 +38,7 @@ public static class CreateOperationsAdvice {
3938
@Advice.OnMethodEnter(suppress = Throwable.class)
4039
public static Scope onEnter(@Advice.Argument(0) Channel channel) {
4140
// set context to the first unprocessed request
42-
Deque<ServerContext> serverContextx = channel.attr(AttributeKeys.SERVER_CONTEXT).get();
43-
ServerContext serverContext = serverContextx != null ? serverContextx.peekFirst() : null;
41+
ServerContext serverContext = ServerContexts.peekFirst(channel);
4442
if (serverContext != null) {
4543
return serverContext.context().makeCurrent();
4644
}

instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/reactornetty/HttpTrafficHandlerInstrumentation.java

+2-5
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@
1010

1111
import io.netty.channel.ChannelHandlerContext;
1212
import io.opentelemetry.context.Scope;
13-
import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys;
1413
import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContext;
14+
import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContexts;
1515
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
1616
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
17-
import java.util.Deque;
1817
import net.bytebuddy.asm.Advice;
1918
import net.bytebuddy.description.type.TypeDescription;
2019
import net.bytebuddy.matcher.ElementMatcher;
@@ -40,9 +39,7 @@ public static class RunAdvice {
4039
public static Scope onEnter(
4140
@Advice.FieldValue("ctx") ChannelHandlerContext channelHandlerContext) {
4241
// set context to the first unprocessed request
43-
Deque<ServerContext> serverContexts =
44-
channelHandlerContext.channel().attr(AttributeKeys.SERVER_CONTEXT).get();
45-
ServerContext serverContext = serverContexts != null ? serverContexts.peekFirst() : null;
42+
ServerContext serverContext = ServerContexts.peekFirst(channelHandlerContext.channel());
4643
if (serverContext != null) {
4744
return serverContext.context().makeCurrent();
4845
}

0 commit comments

Comments
 (0)