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 UDP memory leak #5404

Merged
merged 14 commits into from
Dec 2, 2021
Merged

Conversation

Arkatufus
Copy link
Contributor

@Arkatufus Arkatufus commented Dec 1, 2021

Fix UDP memory leak problem because the rented byte buffer memory never got released when SocketAsyncEventArgs got lost.

Closes #5325

  • Moved ByteBuffer from Udp and UdpConnected into PreallocatedSocketEventAgrsPool. The socket event pool is resposible for renting and releasing buffers now, so we only need to care about renting and releasing socket events.
  • SocketAsyncEventArgs never leaves Udp, UdpListener, UdpConnect, and UdpConnection class. Required data are copied into the response messages instead of embedding SocketAsyncEventArgs inside the Received message. SocketAsyncEventArgs will either be released immediately if Socket.ReceiveAsync returned false or released when the OnComplete event fires.
  • Removed SocketAsyncEventArgs pooling functionality, caching SocketAsyncEventArgs is causing a lot more grief than its worth, we would have to catch all the edge cases where it can fail. SocketAsyncEventArgs is not a simple struct that can be cleaned and reused, it actually have internal states that can cause a lot of problems when reused.

if (buf.Count == _bufferSize && _segments.Contains(buf.Array))
_buffers.Push(buf);
if (buf.Count != _bufferSize || !_segments.Contains(buf.Array))
throw new Exception("Wrong ArraySegment<byte> was released to DirectBufferPool");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sanity check to make sure that any ArraySegment<byte> returned to the buffer actually belongs to the buffer, should only happen during development.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would it only happen during development?

}

internal class PreallocatedSocketEventAgrsPool : ISocketEventArgsPool
{
private readonly IBufferPool _bufferPool;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Buffer pool belongs to the socket event pool now, no one should touch this. ever.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment saying this and explaining why

private readonly EventHandler<SocketAsyncEventArgs> _onComplete;
private readonly ConcurrentStack<SocketAsyncEventArgs> _pool = new ConcurrentStack<SocketAsyncEventArgs>();
private readonly ConcurrentQueue<SocketAsyncEventArgs> _pool = new ConcurrentQueue<SocketAsyncEventArgs>();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to make sure that the content of the pool are rotated evenly, instead of only using the top ones. Minimizes the problem where SocketAsyncEventArgs could not be reused because it is locked by the previous socket.
To be honest, this whole class is a bad idea because SocketAsyncEventArgs are only supposed to be recycled if they're used by a single socket.

}
}

public SocketAsyncEventArgs Acquire(IActorRef actor)
{
if (!_pool.TryPop(out var e))
e = CreateSocketAsyncEventArgs();
var buffer = _bufferPool.Rent();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Byte buffer pool rent can only happen here and no where else.

}
catch (InvalidOperationException)
{
// it can be that for some reason socket is in use and haven't closed yet. Dispose anyway to avoid leaks.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will always be the case if this pool are being used by multiple sockets...


e.UserToken = actor;
e.SetBuffer(buffer.Array, buffer.Offset, buffer.Count);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine because we only rent once and if we fail to acquire an instance, we immediately dispose it.


protected SocketCompleted(SocketAsyncEventArgs eventArgs)
{
Data = ByteString.CopyFrom(eventArgs.Buffer, eventArgs.Offset, eventArgs.BytesTransferred);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of embedding the SocketAsyncEventArgs, we extract the data and store that instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we make a copy of the data, so there's no problems if the SocketAsyncEventArgs get processed and the buffer pool is released.

{
switch (e.LastOperation)
{
case SocketAsyncOperation.Receive:
case SocketAsyncOperation.ReceiveFrom:
return new Udp.SocketReceived(e);
case SocketAsyncOperation.Send:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are never used. Removed to remove red herrings and possible problem in the future.

default: return false;
}
}

private void DoRead(SocketReceived received, IActorRef handler)
{
var e = received.EventArgs;
var buffer = new ByteBuffer(e.Buffer, e.Offset, e.BytesTransferred);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should never happen anymore, all handled in the SocketAsyncEventArgs pool

var e = Udp.SocketEventArgsPool.Acquire(Self);
var buffer = Udp.BufferPool.Rent();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, we don't have to worry about this anymore.

@Aaronontheweb
Copy link
Member

@Arkatufus

  • Have a number of UDP test failures
  • API approval failures

_onComplete = onComplete;
for (var i = 0; i < initSize; i++)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed pool functionality

Copy link
Member

@Aaronontheweb Aaronontheweb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some questions, nitpicks, and minor suggestions but this looks like a great structural improvement for Akka.IO.Udp. Nice work.

poolInfo = udpConnection.SocketEventArgsPool.BufferPoolInfo;
poolInfo.Type.Should().Be(typeof(DirectBufferPool));
poolInfo.Free.Should().Be(poolInfo.TotalSize);
poolInfo.Used.Should().Be(0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

poolInfo = udp.SocketEventArgsPool.BufferPoolInfo;
poolInfo.Type.Should().Be(typeof(DirectBufferPool));
poolInfo.Free.Should().Be(poolInfo.TotalSize);
poolInfo.Used.Should().Be(0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@@ -131,6 +149,17 @@ public DirectBufferPool(int bufferSize, int buffersPerSegment, int initialSegmen
}
}

public BufferPoolInfo Diagnostics()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea

if (buf.Count == _bufferSize && _segments.Contains(buf.Array))
_buffers.Push(buf);
if (buf.Count != _bufferSize || !_segments.Contains(buf.Array))
throw new Exception("Wrong ArraySegment<byte> was released to DirectBufferPool");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would it only happen during development?

}

internal class PreallocatedSocketEventAgrsPool : ISocketEventArgsPool
{
private readonly IBufferPool _bufferPool;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment saying this and explaining why


protected SocketCompleted(SocketAsyncEventArgs eventArgs)
{
Data = ByteString.CopyFrom(eventArgs.Buffer, eventArgs.Offset, eventArgs.BytesTransferred);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we make a copy of the data, so there's no problems if the SocketAsyncEventArgs get processed and the buffer pool is released.

src/core/Akka/IO/UdpConnected.cs Show resolved Hide resolved
@Arkatufus
Copy link
Contributor Author

I mean its there so we can catch it while we're developing Akka, I think its better if I change that to an error log instead.

@Aaronontheweb
Copy link
Member

You can use 'Debug.Assert' for that

@Aaronontheweb Aaronontheweb enabled auto-merge (squash) December 1, 2021 23:01
@Arkatufus
Copy link
Contributor Author

Done with the changes

@Aaronontheweb Aaronontheweb enabled auto-merge (squash) December 2, 2021 14:46
@Aaronontheweb Aaronontheweb merged commit a920b07 into akkadotnet:dev Dec 2, 2021
@Aaronontheweb Aaronontheweb mentioned this pull request Dec 13, 2021
@Arkatufus Arkatufus deleted the IO_Fix_UDP_memory_leak branch December 13, 2021 21:10
@Arkatufus Arkatufus restored the IO_Fix_UDP_memory_leak branch April 22, 2022 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

DirectBufferPool: The buffer is not properly freed in the Release function
2 participants