Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

Limit size of memory buffer when reading request (#304) #912

Merged
merged 37 commits into from
Jun 14, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
debf430
Limit size of memory buffer when reading request (#304)
mikeharder Jun 1, 2016
6a3c653
Fix bug in set_MaxInputBufferLength.
mikeharder Jun 2, 2016
7fb1028
Verify connection is paused when input buffer is full
mikeharder Jun 2, 2016
8832119
Add test to verify connection resumed when buffer not full
mikeharder Jun 2, 2016
ccd6e5c
Functional tests for MaxInputBufferLength
mikeharder Jun 3, 2016
a18b3ec
Functional tests pass reliably on Windows, but rely on Socket.Send() …
mikeharder Jun 3, 2016
4ae8d49
Refactor test and make more reliable
mikeharder Jun 4, 2016
7bb1f0b
Improve range check for bytesWritten when client is paused.
mikeharder Jun 4, 2016
507f555
Use `null` instead of `-1` to make buffer size unlimited
mikeharder Jun 6, 2016
818bcb9
Move buffer length tracking from SocketInput to Connection
mikeharder Jun 7, 2016
3716dba
Make bufferLengthControl an optional parameter
mikeharder Jun 7, 2016
ae07e92
Update SocketInputTests
mikeharder Jun 7, 2016
15fc50e
UvStreamHandle.ReadStop() should be idempotent
mikeharder Jun 7, 2016
fd8edf6
Replace InlineData with MemberData to reduce duplicate code
mikeharder Jun 8, 2016
7cd1f9b
Resume() should catch UvExceptions thrown by ReadStart()
mikeharder Jun 8, 2016
10d6446
Add SSL to functional tests.
mikeharder Jun 9, 2016
7fa1587
Add test where maxInputBufferLength is (_dataLength - 1)
mikeharder Jun 9, 2016
9d41182
Extract BufferLengthControl from a private nested class to a public c…
mikeharder Jun 9, 2016
93cb201
Fix IConnectionControl.Resume() to correctly match behavior of OnRead…
mikeharder Jun 9, 2016
fa4f94c
Doc comment for KestrelServerOptions.MaxInputBufferLength
mikeharder Jun 9, 2016
fea3dfe
Add test for MaxInputBufferSize=1.
mikeharder Jun 9, 2016
5e69570
Add default value to doc comment for KestrelServerOptions.MaxInputBuf…
mikeharder Jun 9, 2016
d40c8b9
Add comment explaining why MaxInputBufferLength defaults to 1MB.
mikeharder Jun 9, 2016
549aab9
Reduce lock contention if count is 0
mikeharder Jun 10, 2016
f977e9b
Use Write("\r\n") instead of WriteLine()
mikeharder Jun 10, 2016
aaab270
Only compute consumed length if _bufferLengthControl is not null.
mikeharder Jun 10, 2016
7cdb1e8
Compute lengthConsumed before modifying _head or consumed
mikeharder Jun 10, 2016
0084e4a
Make functional test use more async
mikeharder Jun 10, 2016
272c626
References to other projects should use "*" versions
mikeharder Jun 10, 2016
27283ee
Increase size of data sent by test client. On Linux, the client isn'…
mikeharder Jun 10, 2016
64aabce
Poll instead of blocking test method
mikeharder Jun 12, 2016
b17b94e
Improve doc comment for MaxInputBufferLength.
mikeharder Jun 13, 2016
1469b42
Further improve doc comment for MaxInputBufferLength.
mikeharder Jun 13, 2016
3fa77f9
Change type of MaxInputBufferLength from int? to long?
mikeharder Jun 13, 2016
342f26b
Rename "MaxInputBufferLength" to "MaxRequestBufferSize"
mikeharder Jun 13, 2016
c9e7f9b
Make test more robust, by continuing to wait if client has not yet up…
mikeharder Jun 14, 2016
ed94ef8
Rename file to match class name.
mikeharder Jun 14, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ public FilteredStreamAdapter(
Stream filteredStream,
MemoryPool memory,
IKestrelTrace logger,
IThreadPool threadPool)
IThreadPool threadPool,
IBufferSizeControl bufferSizeControl)
{
SocketInput = new SocketInput(memory, threadPool);
SocketInput = new SocketInput(memory, threadPool, bufferSizeControl);
SocketOutput = new StreamSocketOutput(connectionId, filteredStream, memory, logger);

_connectionId = connectionId;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Diagnostics;

namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public class BufferSizeControl : IBufferSizeControl
{
private readonly long _maxSize;
private readonly IConnectionControl _connectionControl;
private readonly KestrelThread _connectionThread;

private readonly object _lock = new object();

private long _size;
private bool _connectionPaused;

public BufferSizeControl(long maxSize, IConnectionControl connectionControl, KestrelThread connectionThread)
{
_maxSize = maxSize;
_connectionControl = connectionControl;
_connectionThread = connectionThread;
}

private long Size
{
get
{
return _size;
}
set
{
// Caller should ensure that bytes are never consumed before the producer has called Add()
Debug.Assert(value >= 0);
_size = value;
}
}

public void Add(int count)
{
Debug.Assert(count >= 0);

if (count == 0)
{
// No-op and avoid taking lock to reduce contention
return;
}

lock (_lock)
{
Size += count;
if (!_connectionPaused && Size >= _maxSize)
{
_connectionPaused = true;
_connectionThread.Post(
(connectionControl) => ((IConnectionControl)connectionControl).Pause(),
_connectionControl);
}
}
}

public void Subtract(int count)
{
Debug.Assert(count >= 0);

if (count == 0)
{
// No-op and avoid taking lock to reduce contention
return;
}

lock (_lock)
{
Size -= count;
if (_connectionPaused && Size < _maxSize)
{
_connectionPaused = false;
_connectionThread.Post(
(connectionControl) => ((IConnectionControl)connectionControl).Resume(),
_connectionControl);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Filter;
Expand Down Expand Up @@ -41,6 +42,8 @@ public class Connection : ConnectionContext, IConnectionControl
private ConnectionState _connectionState;
private TaskCompletionSource<object> _socketClosedTcs;

private BufferSizeControl _bufferSizeControl;

public Connection(ListenerContext context, UvStreamHandle socket) : base(context)
{
_socket = socket;
Expand All @@ -49,7 +52,12 @@ public Connection(ListenerContext context, UvStreamHandle socket) : base(context

ConnectionId = GenerateConnectionId(Interlocked.Increment(ref _lastConnectionId));

_rawSocketInput = new SocketInput(Memory, ThreadPool);
if (ServerOptions.MaxRequestBufferSize.HasValue)
{
_bufferSizeControl = new BufferSizeControl(ServerOptions.MaxRequestBufferSize.Value, this, Thread);
}

_rawSocketInput = new SocketInput(Memory, ThreadPool, _bufferSizeControl);
_rawSocketOutput = new SocketOutput(Thread, _socket, Memory, this, ConnectionId, Log, ThreadPool, WriteReqPool);
}

Expand Down Expand Up @@ -217,7 +225,7 @@ private void ApplyConnectionFilter()

if (_filterContext.Connection != _libuvStream)
{
_filteredStreamAdapter = new FilteredStreamAdapter(ConnectionId, _filterContext.Connection, Memory, Log, ThreadPool);
_filteredStreamAdapter = new FilteredStreamAdapter(ConnectionId, _filterContext.Connection, Memory, Log, ThreadPool, _bufferSizeControl);

SocketInput = _filteredStreamAdapter.SocketInput;
SocketOutput = _filteredStreamAdapter.SocketOutput;
Expand Down Expand Up @@ -316,7 +324,17 @@ void IConnectionControl.Pause()
void IConnectionControl.Resume()
{
Log.ConnectionResume(ConnectionId);
_socket.ReadStart(_allocCallback, _readCallback, this);
try
{
_socket.ReadStart(_allocCallback, _readCallback, this);
Copy link
Member

Choose a reason for hiding this comment

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

You might be able to avoid an exception in some cases by checking that _rawSocketInput.RemoteIntakeFin == false before making the call.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would rather not add extra code just to avoid an exception. It would be like checking File.Exists() before you call File.Open(), which usually isn't worth it.

Copy link
Member

@halter73 halter73 Jun 9, 2016

Choose a reason for hiding this comment

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

I think I disagree with you about checking File.Exists, at least when there's a good chance that the file in fact doesn't exist. This is all happening on the libuv thread so there is no race going on like there might be with files. The only time ReadStart may throw as far as I'm aware is in the somewhat rare case that we called ReadStop after receiving the last bit of data but before the FIN/reset.

It might be worth asking the libuv mailing list to see if there is a way to listen for a connection closed event without relying on the read callback (or still applying back pressure somehow with the read callback remaining wired).

If there is no other way, we could arguably have ReadStart return the status. We would likely have to check it and throw it it fails during the first call in Connection.Start, but here we could just check it in an if instead of using try/catch for control flow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Even though libuv uses a single thread, I assume ReadStart() calls into some OS function which itself can fail at any time. So we need to either try/catch or check a status code. Status code might have better perf but .NET convention for I/O is exceptions, and this particular exception will not be thrown often enough where it should impact real-world perf.

I think the current code is fine, and adding more code is just increasing complexity with no real benefit.

}
catch (UvException)
{
// ReadStart() can throw a UvException in some cases (e.g. socket is no longer connected).
// This should be treated the same as OnRead() seeing a "normalDone" condition.
Copy link
Member

Choose a reason for hiding this comment

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

This is actually the same as OnRead() seeing a "errorDone" condition since you're passing the exception. I'm not sure how safe this is since we normally treat a connection reset as "normalDone". It might be better to just log the exception and not pass it to IncomingComplete().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, I will stop passing the exception, but I don't think logging the exception is correct either, since this is a perfectly normal thing to happen, and logging an exception will confuse customers.

Connection.OnRead() just calls Log.ConnectionReadFin() for normalDone. Should I do the same in Resume()?

Copy link
Member

Choose a reason for hiding this comment

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

Log.ConnectionReadFin() is definitely a good call.

We don't log for ECONNRESET. Presumably we will log/throw given an incomplete request somewhere else (At least most of the time. Connection: close makes this tricky, but that's already an issue with ECONNRESET received via the read callback.)

Log.ConnectionReadFin(ConnectionId);
_rawSocketInput.IncomingComplete(0, null);
}
}

void IConnectionControl.End(ProduceEndType endType)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public interface IBufferSizeControl
{
void Add(int count);
void Subtract(int count);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class SocketInput : ICriticalNotifyCompletion, IDisposable

private readonly MemoryPool _memory;
private readonly IThreadPool _threadPool;
private readonly IBufferSizeControl _bufferSizeControl;
private readonly ManualResetEventSlim _manualResetEvent = new ManualResetEventSlim(false, 0);

private Action _awaitableState;
Expand All @@ -32,10 +33,11 @@ public class SocketInput : ICriticalNotifyCompletion, IDisposable
private bool _consuming;
private bool _disposed;

public SocketInput(MemoryPool memory, IThreadPool threadPool)
public SocketInput(MemoryPool memory, IThreadPool threadPool, IBufferSizeControl bufferSizeControl = null)
{
_memory = memory;
_threadPool = threadPool;
_bufferSizeControl = bufferSizeControl;
_awaitableState = _awaitableIsNotCompleted;
}

Expand Down Expand Up @@ -63,6 +65,9 @@ public void IncomingData(byte[] buffer, int offset, int count)
{
lock (_sync)
{
// Must call Add() before bytes are available to consumer, to ensure that Length is >= 0
_bufferSizeControl?.Add(count);

if (count > 0)
{
if (_tail == null)
Expand Down Expand Up @@ -93,6 +98,9 @@ public void IncomingComplete(int count, Exception error)
{
lock (_sync)
{
// Must call Add() before bytes are available to consumer, to ensure that Length is >= 0
_bufferSizeControl?.Add(count);

if (_pinned != null)
{
_pinned.End += count;
Expand Down Expand Up @@ -189,10 +197,21 @@ public void ConsumingComplete(
{
if (!consumed.IsDefault)
{
// Compute lengthConsumed before modifying _head or consumed
var lengthConsumed = 0;
if (_bufferSizeControl != null)
{
lengthConsumed = new MemoryPoolIterator(_head).GetLength(consumed);
}

returnStart = _head;
returnEnd = consumed.Block;
_head = consumed.Block;
_head.Start = consumed.Index;

// Must call Subtract() after _head has been advanced, to avoid producer starting too early and growing
// buffer beyond max length.
Copy link
Member

Choose a reason for hiding this comment

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

This comment implies that the bytes should already be returned to the pool which they're not. Maybe "after _head as been advanced" would be clearer? Even that I don't think is too important as long as Subtract is called with the _sync lock.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would it be more correct to call Subtract() after ReturnBlocks()? My goal was to call Subtract() at the proper time, so the buffer count matches the actual consumed bytes as closely as possible. If Subtract() is called before bytes have been returned to the pool, it's possible for the producer to start again and grow the buffer larger than it should be allowed. Unlikely to matter much in practice, but I wanted to be as close as possible to correct.

Copy link
Contributor Author

@mikeharder mikeharder Jun 7, 2016

Choose a reason for hiding this comment

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

Specifically, would change code to:

// Compute lengthConsumed earlier in function, but don't call Subtract()
ReturnBlocks(returnStart, returnEnd);
_bufferLengthControl?.Subtract(lengthConsumed)

Copy link
Member

Choose a reason for hiding this comment

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

I don't think it matters if you call subtract before or after returning the blocks. I only commented because your comment makes it seem like it does. I think this is just me misconstruing what we mean by the bytes being "freed".

Copy link
Member

Choose a reason for hiding this comment

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

So I think it's fine leaving the code as it is. I was just trying to suggest a clearer comment. I think the comment you have now is fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree the current code seems to work fine, and in practice there will be a very short time window between the two places to call Subtract(). But which do you think is the most correct, given that the actual bytes used by the connection should not exceed the limit? I think it's waiting until after ReturnBlocks(), no?

_bufferSizeControl?.Subtract(lengthConsumed);
}

if (!examined.IsDefault &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,16 @@ public void ReadStart(
}
}

// UvStreamHandle.ReadStop() should be idempotent to match uv_read_stop()
public void ReadStop()
{
if (!_readVitality.IsAllocated)
if (_readVitality.IsAllocated)
{
throw new InvalidOperationException("TODO: ReadStart must be called before ReadStop may be called");
_readVitality.Free();
}
_allocCallback = null;
_readCallback = null;
_readState = null;
_readVitality.Free();
_uv.read_stop(this);
}

Expand Down
32 changes: 28 additions & 4 deletions src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,40 @@ namespace Microsoft.AspNetCore.Server.Kestrel
{
public class KestrelServerOptions
{
// Matches the default client_max_body_size in nginx. Also large enough that most requests
// should be under the limit.
private long? _maxRequestBufferSize = 1024 * 1024;

/// <summary>
/// Gets or sets whether the <c>Server</c> header should be included in each response.
/// </summary>
public bool AddServerHeader { get; set; } = true;

public IServiceProvider ApplicationServices { get; set; }

public IConnectionFilter ConnectionFilter { get; set; }

public bool NoDelay { get; set; } = true;

/// <summary>
/// Gets or sets whether the <c>Server</c> header should be included in each response.
/// Maximum size of the request buffer. Default is 1,048,576 bytes (1 MB).
/// If value is null, the size of the request buffer is unlimited.
/// </summary>
public bool AddServerHeader { get; set; } = true;
public long? MaxRequestBufferSize
{
get
{
return _maxRequestBufferSize;
}
set
{
if (value.HasValue && value.Value <= 0)
{
throw new ArgumentOutOfRangeException("value", "Value must be null or a positive integer.");
}
_maxRequestBufferSize = value;
}
}

public bool NoDelay { get; set; } = true;

/// <summary>
/// The amount of time after the server begins shutting down before connections will be forcefully closed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,35 @@ namespace Microsoft.AspNetCore.Hosting
{
public static class IWebHostPortExtensions
{
public static string GetHost(this IWebHost host)
{
return host.GetUris().First().Host;
}

public static int GetPort(this IWebHost host)
{
return host.GetPorts().First();
}

public static int GetPort(this IWebHost host, string scheme)
{
return host.GetUris()
.Where(u => u.Scheme.Equals(scheme, StringComparison.OrdinalIgnoreCase))
.Select(u => u.Port)
.First();
}

public static IEnumerable<int> GetPorts(this IWebHost host)
{
return host.GetUris()
.Select(u => u.Port);
}

public static IEnumerable<Uri> GetUris(this IWebHost host)
{
return host.ServerFeatures.Get<IServerAddressesFeature>().Addresses
.Select(a => a.Replace("://+", "://localhost"))
.Select(a => (new Uri(a)).Port);
.Select(a => new Uri(a));
}
}
}
Loading