NetCoreServer icon indicating copy to clipboard operation
NetCoreServer copied to clipboard

OutOfMemory or StackOverflow exceptions occurs when trying to send a lot of data

Open dmrayt opened this issue 4 years ago • 7 comments

With following code on a TCP client side:

int bufferSize = 1024 * 1024;
var buffer = Enumerable.Repeat<byte>((byte)'.', bufferSize).ToArray();
int count = 3 * 1024;
for (int i = 0; i < count; i++)
{
    client.SendAsync(buffer);
}

I get an OutOfMemory exception in SendAsync(buffer).

If I replace SendAsync(buffer) with Send(buffer), the OutOfMemory exception occurs on the server side because in ProcessReceive() the size of the _receiveBuffer is always increasing with following code:

    // If the receive buffer is full increase its size
    if (_receiveBuffer.Capacity == size)
        _receiveBuffer.Reserve(2 * size);

When I commment out the lines above or if the server is slow (a SslServer for example), I get a StackOverflow exception in TryReceive().

dmrayt avatar May 19 '20 14:05 dmrayt

It's a known "slow receiver attack" problem. If you send lots of data with a slow or blocked receiver, your send buffer will grow. As a solution you have to implement some back pressure algorithm on a top of client/session overriding OnSend() method or using BytesPending/BytesSending properties inside your send logic. Your choice is to skip some data or even disconnect if you have pending data for example >10mb inside your buffers:

        /// <summary>
        /// Number of bytes pending sent by the client
        /// </summary>
        public long BytesPending { get; private set; }
        /// <summary>
        /// Number of bytes sending by the client
        /// </summary>
        public long BytesSending { get; private set; }

        /// <summary>
        /// Handle buffer sent notification
        /// </summary>
        /// <param name="sent">Size of sent buffer</param>
        /// <param name="pending">Size of pending buffer</param>
        /// <remarks>
        /// Notification is called when another chunk of buffer was sent to the server.
        /// This handler could be used to send another buffer to the server for instance when the pending size is zero.
        /// </remarks>
        protected virtual void OnSent(long sent, long pending) {}

chronoxor avatar May 19 '20 15:05 chronoxor

Thanks for your answer but I choose to use your library instead of implementing everything myself to avoid such problems. At least you should not resize the buffer indefinitely to avoid OutOfMemory exception.

It seems that the StackOverflow exception only occurs with a SSL connection. I received the StackOverflow exception as I was checking the performance of the SslClient and SslServer, in my case the buffer didn't grow over 16k. The StackOverflow occurs because ProcessReceive() calls TryReceive() which indirectly calls ProcessReceive() which calls TryReceive() which calls ... and end with a StackOverflow exception.

dmrayt avatar May 19 '20 16:05 dmrayt

Could you please provide a call stack when you get StackOverflow? I think the issue related with some recursion calls in SslClient/SslServer send/receive loop. And what version of NetCoreServer you're using? Do you use the latest version 3.0.13?

chronoxor avatar May 19 '20 16:05 chronoxor

Here the version info:

  • NetCoreServer 3.0.13
  • donet core 3.1.3
  • Visual Studio 16.5.4
  • Windows 10 1909

To reproduce my problem I do the following in actual master of NetCoreServer (3.0.13):

  • in SslChatClient at the end of the endless for loop in Main() add following
    if (line == "send")
    {
        int bufferSize = 1024 * 1024;
        var buffer = Enumerable.Repeat<byte>((byte)'.', bufferSize).ToArray();
        int count = 3 * 1024;
        for (int i = 0; i < count; i++)
        {
            client.SendAsync(buffer);
        }
    }
  • in SslChatServer delete the line Send(message) in ChatSession.OnHandshaked() otherwise the client always diconnect (see my comment in issue #62)

  • in SslChatServer replace ChatSession.OnReceived() with this code

        protected override void OnReceived(byte[] buffer, long offset, long size)
        {
            Console.WriteLine($"OnReceived: byte[{buffer.Length:N0}], {offset:N0} - {size:N0}");
        }
  • Start debugging SslChatServer and SslChatClient

  • type send in client window and press enter

Sometimes the client disconnect while sending but most of the time I get following exception in SslChatClient

System.OverflowException : Array dimensions exceeded supported range.
   at NetCoreServer.Buffer.Reserve(Int64 capacity) in C:\Projects\_lib\NetCoreServer\source\NetCoreServer\Buffer.cs:line 116
   at NetCoreServer.Buffer.Append(Byte[] buffer, Int64 offset, Int64 size) in C:\Projects\_lib\NetCoreServer\source\NetCoreServer\Buffer.cs:line 162
   at NetCoreServer.SslClient.SendAsync(Byte[] buffer, Int64 offset, Int64 size) in C:\Projects\_lib\NetCoreServer\source\NetCoreServer\SslClient.cs:line 465
   at NetCoreServer.SslClient.SendAsync(Byte[] buffer) in C:\Projects\_lib\NetCoreServer\source\NetCoreServer\SslClient.cs:line 442
   at SslChatClient.Program.Main(String[] args) in C:\Projects\_lib\NetCoreServer\examples\SslChatClient\Program.cs:line 118

but the output of SslChatServer look like this server output

I have no idea who is writing this Stack overflow message, but when I check the Threads in Visual Studio I got this (client is at top, server at bottom) Threads

if I double click on the worker thread of the server i come in NetCoreServer.SslSession.TryReceive() at line 420 (... _sslStream.BeginRead ...), and if I check the call stack i got following (check the size of scrollbar and the message from visual studio at the end) Call Stack

I don't know if the Stack overflow in server is related to the OverflowException in client or not.

dmrayt avatar May 20 '20 08:05 dmrayt

I just see that if I press F5 to continue after getting the first exception, I get a System.StackOverflowException in SslChatServer. image

dmrayt avatar May 20 '20 09:05 dmrayt

Please try this fragment of code with 3.0.14 NetCoreServer:

                if (line == "send")
                {
                    int bufferSize = 1024 * 1024;
                    var buffer = Enumerable.Repeat<byte>((byte)'.', bufferSize).ToArray();
                    int count = 3 * 1024;
                    for (int i = 0; i < count; i++)
                    {
                        client.SendAsync(buffer);
                        while (client.BytesPending + client.BytesSending > 10485760)
                            Thread.Yield();
                    }
                }

chronoxor avatar May 20 '20 12:05 chronoxor

The stack overflow is gone. Thanks for the quick fix. I hope you can also do something for the OutOfMemory exception (at least for the received buffer)

dmrayt avatar May 20 '20 15:05 dmrayt