Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

SqlClient fix SqlEnvChangePool thread safety bug #36735

Merged
merged 4 commits into from
Apr 11, 2019
Merged
Changes from 2 commits
Commits
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 @@ -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
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link

@jnm2 jnm2 Apr 9, 2019

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.

{
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
Copy link

Choose a reason for hiding this comment

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

craete

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 😁

Copy link

Choose a reason for hiding this comment

The 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();
Copy link
Contributor

Choose a reason for hiding this comment

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

This allocated SqlEnvChange will not be tracked by _items
This seems counter intuitive to the design.

When this item is released in the Release call below, it will not go back to the _items. If _items is not gong to be a container for all the SqlEnvChange objects, then why have it in the first place ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Let me know if I have missed anything

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

image

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.

Copy link
Contributor Author

@Wraith2 Wraith2 Apr 9, 2019

Choose a reason for hiding this comment

The 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 ?

Copy link
Contributor

Choose a reason for hiding this comment

The 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
}
}

Expand Down