-
Notifications
You must be signed in to change notification settings - Fork 4.9k
SqlClient fix SqlEnvChangePool thread safety bug #36735
Changes from 2 commits
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 |
---|---|---|
|
@@ -298,104 +298,56 @@ internal void Clear() | |
} | ||
} | ||
|
||
/// <summary> | ||
/// this class implements a static pool of SqlEnvChange objects to help reduce memory churn in low resource | ||
/// situations. For low frequency querying behaviour most env change objects will be sourced and returned | ||
/// to the pool. In higher frequency situations the pool will create and discard any additional objects that | ||
/// are needed without blocking the caller so that it doesn't become a bottleneck, overall memory consumption | ||
/// should still be lower than without the pool | ||
/// </summary> | ||
internal static class SqlEnvChangePool | ||
{ | ||
private static int _count; | ||
private static int _capacity; | ||
private static SqlEnvChange[] _items; | ||
#if DEBUG | ||
private static string[] _stacks; | ||
#endif | ||
|
||
static SqlEnvChangePool() | ||
// the conatiner struct allows avoidance of array variance checks on assignment | ||
private protected struct SqlEnvChangeContainer | ||
{ | ||
_capacity = 10; | ||
_items = new SqlEnvChange[_capacity]; | ||
_count = 1; | ||
#if DEBUG | ||
_stacks = new string[_capacity]; | ||
#endif | ||
public SqlEnvChange Item; | ||
} | ||
|
||
private static readonly SqlEnvChangeContainer[] _items = new SqlEnvChangeContainer[Environment.ProcessorCount * 2]; | ||
|
||
internal static SqlEnvChange Allocate() | ||
{ | ||
SqlEnvChange retval = null; | ||
lock (_items) | ||
SqlEnvChangeContainer[] items = _items; | ||
for (int index = 0; index < items.Length; index++) | ||
{ | ||
while (_count > 0 && retval is null) | ||
SqlEnvChange item = items[index].Item; | ||
if (item != null && Interlocked.CompareExchange(ref items[index].Item, null, item) == item) | ||
{ | ||
int count = _count; // copy the count we think we have | ||
int newCount = count - 1; // work out the new value we want | ||
// exchange _count for newCount only if _count is the same as our cached copy | ||
if (count == Interlocked.CompareExchange(ref _count, newCount, count)) | ||
{ | ||
// count is now the previous value, we're the only thread that has it, so count-1 is safe to access | ||
Interlocked.Exchange(ref retval, _items[count - 1]); | ||
} | ||
else | ||
{ | ||
// otherwise the count wasn't what we expected, spin the while and try again. | ||
} | ||
return item; | ||
} | ||
} | ||
if (retval is null) | ||
{ | ||
retval = new SqlEnvChange(); | ||
} | ||
return retval; | ||
// if we didn't find an item in the pool we've either never craete any or we're under pressure | ||
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.
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. You wouldn't believe I'm an English monoglot based on my comments would you 😁 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. Oh yes, I would, believe me :D |
||
// so just create a new one and let the caller get on with their work | ||
return new SqlEnvChange(); | ||
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 allocated When this item is released in the 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. Let me know if I have missed anything 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. SqlEnvChange is used in a small number of places. All allocation is done through this function. The pool is designed to be a buffer but not a block so if we have a free object hanging around then use it but if we're running fast and don't have a spare object I don't want to wait for someone to return one to the pool, just create a new one. Like allocation release is always done through the pool so if an item was new'ed rather than being sourced from the pool it will still be given back to the pool. If the pool is full the item will simply be discarded. This behaviour means that at low frequency somewhere below the pool threshold all items are likely to be sourced from the pool which makes the operations lower memory than they otherwise would be. At high frequency the pool will be recycled quickly but some items are still going to be new'ed and dropped to GC, overall the operations will have lower GC pressure. 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 am not sure if this assumption works very well. SqlEnvChange is used in a small number of places. But then the places it is used can happen concurrently and with high through put. For applications with small load, I don't care as much for this change, however the applications where the improvements is useful are web apps which will spin up a lot of connections at the same time from different threads, potentially from few connection pools. Those are the real world cases. In those cases, the number of env change token from the pool will be exhausted quickly enough. I don't have a quantification on how big the Env Change pool should be, but I do know that ProcessCount * 2 doesn't get close enough to the concurrent connection open requests and transaction promotions that happen in real world applications. 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. then either we need a larger pool or to drop it entirely, I'm fine with either. In low memory cases having a very large pool would be a constant cost and possibly go mostly unused. In the case of bringing up an entire pool how many concurrent connections are likely to be in the env change code at once do you think? my instinct says it's likely to be high so a high pool would be best. The list and array rental changes of the original RP are still beneficial to all cases. I suspect the pool is of dubious benefit without some perf numbers to back it up at this point. Would you agree @afsanehr ? 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 am leaning towards dropping this pool till we can have a more solid reasoning behind the size, and have a one size fits all model. I don't have any ideas to improve this right now. Let's try to keep this simple (though possible less performant in some cases) and remove the pool. |
||
} | ||
|
||
internal static void Release(SqlEnvChange item, [Runtime.CompilerServices.CallerMemberName] string caller = null) | ||
internal static void Release(SqlEnvChange item) | ||
{ | ||
if (item is null) | ||
{ | ||
Debug.Fail("attenpting to release null packet"); | ||
Debug.Fail("attempting to release null item"); | ||
return; | ||
} | ||
item.Clear(); | ||
SqlEnvChangeContainer[] items = _items; | ||
for (int index = 0; index < items.Length; index++) | ||
{ | ||
#if DEBUG | ||
if (_count > 0) | ||
if (Interlocked.CompareExchange(ref items[index].Item, item, null) is null) | ||
{ | ||
for (int index = 0; index < _count; index += 1) | ||
{ | ||
if (object.ReferenceEquals(item, _items[index])) | ||
{ | ||
Debug.Assert(false,$"releasing an item which already exists in the pool, count={_count}, index={index}, caller={caller}, released at={_stacks[index]}"); | ||
} | ||
} | ||
} | ||
#endif | ||
int tries = 0; | ||
|
||
while (!(item is null) && tries < 3 && _count < _capacity) | ||
{ | ||
int count = _count; | ||
int newCount = count + 1; | ||
if (count == Interlocked.CompareExchange(ref _count, newCount, count)) | ||
{ | ||
_items[count] = item; | ||
#if DEBUG | ||
_stacks[count] = caller; | ||
#endif | ||
item = null; | ||
} | ||
else | ||
{ | ||
tries += 1; | ||
} | ||
break; | ||
} | ||
} | ||
} | ||
|
||
internal static int Count => _count; | ||
|
||
internal static int Capacity => _capacity; | ||
|
||
internal static void Clear() | ||
{ | ||
Array.Clear(_items, 0, _capacity); | ||
_count = 0; | ||
// if we didn't add the item to the cache just let the go to the GC | ||
Wraith2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
|
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.
A comment about the behavior of this class would be nice.
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.
Added, and a couple of others inline to explain why items are new'ed and possibly dropped to the GC. Let me know if that explains things better and what else might be useful.
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.
The sentence with a comma should be split into two sentences. (http://www.sussex.ac.uk/informatics/punctuation/comma/joining)
Sentences should consistently have a capital letter and an end mark.