-
Notifications
You must be signed in to change notification settings - Fork 523
Limit size of memory buffer when reading request (#304) #912
Changes from all commits
debf430
6a3c653
7fb1028
8832119
ccd6e5c
a18b3ec
4ae8d49
7bb1f0b
507f555
818bcb9
3716dba
ae07e92
15fc50e
fd8edf6
7cd1f9b
10d6446
7fa1587
9d41182
93cb201
fa4f94c
fea3dfe
5e69570
d40c8b9
549aab9
f977e9b
aaab270
7cdb1e8
0084e4a
272c626
27283ee
64aabce
b17b94e
1469b42
3fa77f9
342f26b
c9e7f9b
ed94ef8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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); | ||
} | ||
|
||
|
@@ -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; | ||
|
@@ -316,7 +324,17 @@ void IConnectionControl.Pause() | |
void IConnectionControl.Resume() | ||
{ | ||
Log.ConnectionResume(ConnectionId); | ||
_socket.ReadStart(_allocCallback, _readCallback, this); | ||
try | ||
{ | ||
_socket.ReadStart(_allocCallback, _readCallback, this); | ||
} | ||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(). There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We don't log for ECONNRESET. Presumably we will log/throw given an incomplete request somewhere else (At least most of the time. |
||
Log.ConnectionReadFin(ConnectionId); | ||
_rawSocketInput.IncomingComplete(0, null); | ||
} | ||
} | ||
|
||
void IConnectionControl.End(ProduceEndType endType) | ||
|
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 |
---|---|---|
|
@@ -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; | ||
|
@@ -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; | ||
} | ||
|
||
|
@@ -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) | ||
|
@@ -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; | ||
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be more correct to call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Specifically, would change code to:
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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". There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 && | ||
|
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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 callFile.Open()
, which usually isn't worth it.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.