Skip to content
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

Fix buffer leak in WebSocket13FrameEncoder #12773

Merged

Conversation

pderop
Copy link
Contributor

@pderop pderop commented Sep 6, 2022

Motivation:

When encoding a CloseWebSocketFrame frame using the WebSocket13FrameEncoder.encode method, the binary data associated to the frame is not always disposed, causing a memory leak, and when using the leak detector, the following kind of logs can be observed:

15:51:00.952 [Cleaner-0] ERROR i.n.buffer.api.LoggingLeakCallback - LEAK: Object "buffer (22 bytes)" was not property closed before it was garbage collected. A life-cycle back-trace (if any) is attached as suppressed exceptions. See https://netty.io/wiki/reference-counted-objects.html for more information.
io.netty5.buffer.api.LoggingLeakCallback$LeakReport: Object life-cycle trace:
	Suppressed: io.netty5.buffer.api.internal.LifecycleTracer$Traceback: ALLOCATE (current acquires = 0) T-641565us.
		at io.netty5.buffer.api.internal.ResourceSupport.<init>(ResourceSupport.java:42)
		at io.netty5.buffer.api.internal.AdaptableBuffer.<init>(AdaptableBuffer.java:28)
		at io.netty5.buffer.api.bytebuffer.NioBuffer.<init>(NioBuffer.java:65)
		at io.netty5.buffer.api.bytebuffer.ByteBufferMemoryManager.createBuffer(ByteBufferMemoryManager.java:83)
		at io.netty5.buffer.api.bytebuffer.ByteBufferMemoryManager.recoverMemory(ByteBufferMemoryManager.java:78)
		at io.netty5.buffer.api.pool.PooledBufferAllocator.allocate(PooledBufferAllocator.java:321)
		at io.netty5.buffer.api.DefaultBufferAllocators$UncloseableBufferAllocator.allocate(DefaultBufferAllocators.java:123)
		at io.netty5.handler.codec.http.websocketx.CloseWebSocketFrame.newBinaryData(CloseWebSocketFrame.java:99)
		at io.netty5.handler.codec.http.websocketx.CloseWebSocketFrame.<init>(CloseWebSocketFrame.java:88)
		at io.netty5.handler.codec.http.websocketx.CloseWebSocketFrame.<init>(CloseWebSocketFrame.java:62)
		at io.netty5.handler.codec.http.websocketx.CloseWebSocketFrame.<init>(CloseWebSocketFrame.java:36)
		at io.netty5.handler.codec.http.websocketx.WebSocketServerProtocolHandlerTest.testExplicitCloseFrameSentWhenClientChannelClosed(WebSocketServerProtocolHandlerTest.java:354)
		at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
		at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
		at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
		at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
		at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
		at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
		at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
		at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
		at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
		at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
		at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
		at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
		at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
		at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
		at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
		at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
		at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
		at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
		at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
		at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	Suppressed: io.netty5.buffer.api.internal.LifecycleTracer$Traceback: TOUCH (ChannelHandlerContext(WebSocketClientProtocolHandler#0, [id: 0xembedded])) T-642854us.
		at io.netty5.buffer.api.internal.ResourceSupport.touch(ResourceSupport.java:224)
		at io.netty5.buffer.api.internal.AdaptableBuffer.touch(AdaptableBuffer.java:34)
		at io.netty5.buffer.api.internal.AdaptableBuffer.touch(AdaptableBuffer.java:22)
		at io.netty5.buffer.api.BufferHolder.touch(BufferHolder.java:156)
		at io.netty5.util.Resource.touch(Resource.java:130)
		at io.netty5.channel.DefaultChannelPipeline.touch(DefaultChannelPipeline.java:113)
		at io.netty5.channel.DefaultChannelHandlerContext.invokeWrite(DefaultChannelHandlerContext.java:846)
		at io.netty5.channel.DefaultChannelHandlerContext$AbstractWriteTask.write(DefaultChannelHandlerContext.java:1217)
		at io.netty5.channel.DefaultChannelHandlerContext$AbstractWriteTask.run(DefaultChannelHandlerContext.java:1187)
		at io.netty5.channel.embedded.EmbeddedEventLoop.runTasks(EmbeddedEventLoop.java:139)
		at io.netty5.channel.embedded.EmbeddedEventLoop.execute(EmbeddedEventLoop.java:125)
		at io.netty5.channel.DefaultChannelHandlerContext.safeExecute(DefaultChannelHandlerContext.java:1094)
		at io.netty5.channel.DefaultChannelHandlerContext.write(DefaultChannelHandlerContext.java:935)
		at io.netty5.channel.DefaultChannelHandlerContext.write(DefaultChannelHandlerContext.java:842)
		at io.netty5.channel.DefaultChannelPipeline.write(DefaultChannelPipeline.java:914)
		at io.netty5.channel.Channel.write(Channel.java:306)
		at io.netty5.channel.embedded.EmbeddedChannel.writeOutbound(EmbeddedChannel.java:343)
		at io.netty5.handler.codec.http.websocketx.WebSocketServerProtocolHandlerTest.testExplicitCloseFrameSentWhenClientChannelClosed(WebSocketServerProtocolHandlerTest.java:354)
		at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
		at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
		at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
		at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
		at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
		at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
		at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
		at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
		at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
		at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
		at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
		at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
		at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
		at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
		at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
		at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
		at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	Suppressed: io.netty5.buffer.api.internal.LifecycleTracer$Traceback: TOUCH (ChannelHandlerContext(ws-encoder, [id: 0xembedded])) T-643103us.
		at io.netty5.buffer.api.internal.ResourceSupport.touch(ResourceSupport.java:224)
		at io.netty5.buffer.api.internal.AdaptableBuffer.touch(AdaptableBuffer.java:34)
		at io.netty5.buffer.api.internal.AdaptableBuffer.touch(AdaptableBuffer.java:22)
		at io.netty5.buffer.api.BufferHolder.touch(BufferHolder.java:156)
		at io.netty5.util.Resource.touch(Resource.java:130)
		at io.netty5.channel.DefaultChannelPipeline.touch(DefaultChannelPipeline.java:113)
		at io.netty5.channel.DefaultChannelHandlerContext.invokeWrite(DefaultChannelHandlerContext.java:846)
		at io.netty5.channel.DefaultChannelHandlerContext.write(DefaultChannelHandlerContext.java:926)
		at io.netty5.channel.DefaultChannelHandlerContext.write(DefaultChannelHandlerContext.java:842)
		at io.netty5.handler.codec.http.websocketx.WebSocketProtocolHandler.write(WebSocketProtocolHandler.java:116)
		at io.netty5.handler.codec.http.websocketx.WebSocketClientProtocolHandler.write(WebSocketClientProtocolHandler.java:47)
		at io.netty5.channel.DefaultChannelHandlerContext.invokeWrite(DefaultChannelHandlerContext.java:854)
		at io.netty5.channel.DefaultChannelHandlerContext$AbstractWriteTask.write(DefaultChannelHandlerContext.java:1217)
		at io.netty5.channel.DefaultChannelHandlerContext$AbstractWriteTask.run(DefaultChannelHandlerContext.java:1187)
		at io.netty5.channel.embedded.EmbeddedEventLoop.runTasks(EmbeddedEventLoop.java:139)
		at io.netty5.channel.embedded.EmbeddedEventLoop.execute(EmbeddedEventLoop.java:125)
		at io.netty5.channel.DefaultChannelHandlerContext.safeExecute(DefaultChannelHandlerContext.java:1094)
		at io.netty5.channel.DefaultChannelHandlerContext.write(DefaultChannelHandlerContext.java:935)
		at io.netty5.channel.DefaultChannelHandlerContext.write(DefaultChannelHandlerContext.java:842)
		at io.netty5.channel.DefaultChannelPipeline.write(DefaultChannelPipeline.java:914)
		at io.netty5.channel.Channel.write(Channel.java:306)
		at io.netty5.channel.embedded.EmbeddedChannel.writeOutbound(EmbeddedChannel.java:343)
		at io.netty5.handler.codec.http.websocketx.WebSocketServerProtocolHandlerTest.testExplicitCloseFrameSentWhenClientChannelClosed(WebSocketServerProtocolHandlerTest.java:354)
		at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
		at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
		at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
		at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
		at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
		at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
		at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
		at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
		at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
		at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
		at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
		at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
		at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
		at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
		at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
		at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

The issue only happens when using netty5 latest snapshot version, not using netty 4.x version.

Modification:

Updated the WebSocket13FrameEncoder.encode in order to always close the frame data (when necessary).
Also, update the WebSocketServerProtocolHandlerTest.testExplicitCloseFrameSentWhenClientChannelClosed test

Result:

Using the proposed patch we don't see anymore leaks when encoding CloseWebSocketFrame frames using the WebSocket13FrameEncoder.

@chrisvest chrisvest merged commit 3225a97 into netty:main Sep 6, 2022
@chrisvest
Copy link
Contributor

Thanks!

@pderop pderop deleted the netty5-fix-leak-in-websocket13frameencoder branch September 7, 2022 08:24
@pderop
Copy link
Contributor Author

pderop commented Sep 7, 2022

@chrisvest , thanks for the merge !

@amizurov
Copy link
Sponsor Contributor

amizurov commented Sep 7, 2022

@pderop @chrisvest Hi guys. First of all @pderop thanks for this PR, but i think the fix should be done in another way. We need to override the WebSocket13FrameEncoder.encode(), not encodeAndClose() it give us an autoclose for the input argument and don't need to do this manually, behaviour will be similar with 4.1.

@pderop
Copy link
Contributor Author

pderop commented Sep 7, 2022

Hi @amizurov, @chrisvest

So, I understand your point, but there is one thing to take care about: indeed, if we override the WebSocket13FrameEncoder.encode() instead of encodeAndClose(), then the websocket frame to encode will be always closed, see MessageToMessageEncoder.encodeAndClose method:

    protected void encodeAndClose(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception {
        try (AutoCloseable ignore = autoClosing(msg)) {
            encode(ctx, msg, out);
        }
    }

However, from the current WebSocket13FrameEncoder.encodeAndClose method, there is one case where the WebSocketFrame data message is appended to the "out" list of produced data, see here, so closing the frame in this case is likely to a problem (if I'm correct ??)

In previous 4.x, when the frame data is appended to out list, then it was done like this, using data.retain()

So, then In this case, if we go for overriding encode instead of encodeAndClose, we will have to probably do something like:

out.add(data.split())

I'll try to prepare a new PR, let's see what it will give ...

@pderop
Copy link
Contributor Author

pderop commented Sep 7, 2022

@amizurov ,

I have created another PR here: #12780, which is following your suggestion.

@amizurov
Copy link
Sponsor Contributor

amizurov commented Sep 7, 2022

@pderop Oh, i missed that we have not something like retain() in new API. I'm didn't check if we have similar stuff in new API. From my point of view this is convenient and can help to prevent leaks. But maybe @chrisvest or @normanmaurer can suggest something about it.

@pderop
Copy link
Contributor Author

pderop commented Sep 7, 2022

@amizurov , please check the other PR where data.split is used from the new Buffer API.

@amizurov
Copy link
Sponsor Contributor

amizurov commented Sep 7, 2022

@amizurov , please check the other PR where data.split is used from the new Buffer API.

Sure

normanmaurer pushed a commit that referenced this pull request Sep 8, 2022
…ose (#12780)

Motivation:

The #12773 PR has fixed a memory leak from the WebSocket13FrameEncoder.encodeAndClose method, where the websocket frame to be encoded was not always closed properly.

Now, @amizurov has suggested to do a better fix by just letting the WebSocket13FrameEncoder override the _encode_ method instead of the _encodeAndClose_. It give an autoclose for the input argument and don't need to do this manually, behaviour will be similar with 4.1.

However, care must be taken when the frame is itself appended to the "out" list of produced data. In 4.x, this was done using "data.retain", with the new Buffer API, the data can be added to the "out" list using "data.split".

Modification:

The WebSocket13FrameEncoder is now overriding the _encode_ method instead of the _encodeAndClose_, and the frame message is not closed anymore at all (this is done by the _encodeAndClose_ method from the superclass).
And when the frame message data has to be appended into the "out" list of produced encoded data, then it is done using "data.split" 

Result:

The WebSocket13FrameEncoder class is simpler and don't have to close the frame data parameter.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants