Dont allow exceptions to emerge on the threadpool #27439
Conversation
@dotnet-bot Test Windows x64 Debug Build please @danmosemsft I can figure out what failed for the given build from the logs. |
catch(Exception) | ||
{ | ||
// Catch all the exceptions occuring during CreateObject so that they | ||
// don't emerge as unhandled on the thread pool and don't crash applications |
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.
It'd be good to add why it's ok that we eat them... from your PR description, it sounds like there's some other backchannel through which they're passed up to the originating code?
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.
Sounds good.
What happens is that CreateObject handles the exception at
corefx/src/System.Data.SqlClient/src/System/Data/ProviderBase/DbConnectionPool.cs
Line 721 in 51bdece
if (!ADP.IsCatchableExceptionType(e)) |
And then it sets the ErrorEvent. The caller of the connection pool is signalled on the connection pool about the ErrorEvent being set and it knows that an exception was thrown. The caller of Connection.Open() ultimately gets the same exception that they were receiving on .Net Framework. It's just that the exception doesn't escape on to the threadpool
{ | ||
try | ||
{ | ||
connection.Open(); |
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.
Is this going to fail every time? If so, how about using Assert.Throws? That would make it something like:
string connectionString = ...;
for (int i = 0; i < 3; i++)
{
using (var connection = new SqlConnection(connectionString))
{
Assert.Throws<WhateverExceptionType>(() => connection.Open());
}
}
That'll also help validate that the right exception is thrown.
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.
Yes it will fail everytime. I will adapt the testcase.
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.
Note: The exception thrown is either InvalidOperationException (if the connection pool timeout is triggered) or SqlException (if the network error in connecting to invalid Data Source happens before the Connection Pool timeout) . So the specific exception type cannot be determined. I will use Assert.ThrowsAny<Exception>(() => connection.Open());
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.
So the specific exception type cannot be determined. I will use Assert.ThrowsAny(() => connection.Open());
In that case you can do:
Exception error = Record.Exception(() => connection.Open());
Assert.True(error is InvalidOperationException || error is SqlException, $"Unexpected exception: {error}");
} | ||
} | ||
} | ||
Assert.Equal(expected, count); |
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.
If you do end up keeping the current structure, this Assert.Equal seems unnecessary... the only way you'll get here, by construction, is if expected == count, as that's when you exit the loop.
// Don't specify any user options because there is no outer connection associated with the new connection | ||
newObj = CreateObject(owningObject: null, userOptions: null, oldConnection: null); | ||
} | ||
catch(Exception) |
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.
Since you're catching everything, you can just do catch without the Exception part.
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.
Agreed.
{ | ||
int count = 0; | ||
int expected = 3; | ||
string connectionString = $"Data Source={Guid.NewGuid().ToString()};uid=random;pwd=asd;Connect Timeout=2; Min Pool Size=3"; |
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.
Does this Connect Timeout mean this will take 2 seconds to fail? And we do it 3 times? Might want to make the test outerloop then.
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 test will try for a max of 2 seconds. However it could fail earlier. But it will go upto 2 seconds.
Outerloop makes sense. Thanks.
* Dont allow exceptions to emerge on the threadpool * Code review comments * Check for specific exceptions Commit migrated from dotnet/corefx@56ce6c1
PoolCreateRequest handles creation of new connections on the thread pool. However when an exception occurs, it is bubbled up to the thread pool. This is a regression from .Net Framework and causes applications using Min Pool Size to crash.
The solution is to catch all exceptions during connection open and to swallow them. This will not change the exception that app sees from Connection Pool, as the error is bubbled up from another code path where the errorWaitHandle is set during connection Open.
The test added, makes sure that we can open 3 bad connections using MinPoolSize. Before the fix, the test would crash after the first exception.
Fixes https://github.com/dotnet/corefx/issues/14615