ADO.NET Connection Pooling Explained
by James Still02/09/2004
Because the .NET managed providers manage the connection pool for us, using shared database connections is as easy as a summertime splash in the kiddie pool. But if those connections unexpectedly become invalid, you could find yourself floundering in the deep end. Any number of things can cause connections in the pool to become invalid; maybe the database administrator killed the wrong session. Perhaps the database server was rebooted. Or it could be that the firewall between the web server and the database server closed what it thought were idle connections. Whatever the reason, your application won't know if a pooled connection is invalid until it's too late and an exception has been thrown. Microsoft documents this behavior in an MSDN white paper on connection pooling:
"If a connection exists to a server that has disappeared, it is possible for this connection to be drawn from the pool even if the connection pooler has not detected the severed connection and marked it as invalid. When this occurs, an exception is generated."
|
Related Reading
ADO.NET Cookbook |
Most of us have already experienced this painful problem firsthand (and if
you haven't, it's only a matter of time before you do). We don't yet know whether
enhancements to the System.Data.SqlClient namespace in the upcoming
Whidbey release will address it. It's possible that Microsoft will introduce
some sort of failover solution behind the scenes in the SqlConnection
class that will first try to use other connections in the pool before throwing
an exception. However, even if they do address the problem, you don't want to
rely on a vendor-specific solution. After all, your data
access component might be using the SqlConnection class today,
but next year it might use OracleConnection or some other managed
provider's IDBConnection implementation. Therefore, your best bet
is to handle the problem in your own data access component.
One possible solution is to ping the server with something like "select date=getdate()"
(T-SQL) or "select
sysdate from dual" (PL/SQL) before each
real query. The idea behind this is to receive a date to verify that the pooled connection is
valid and will work correctly for the real query. I've seen this "pessimistic
ping" solution put into production, but it's a bad idea for two reasons. First,
it's an unnecessary and inefficient round trip to the server. Each query ends up being two round
trips, which could impact the performance of the application. Second, in a
busy connection pool, there's no guarantee you'll get the same connection on each
query. The ping query's connection might be unavailable when the real query is
executed, and who knows if the real query's connection is valid or invalid; you're right back to where you started. In this article, I recommend an optimistic
solution to the problem of invalid connections in the pool. But first, to
reproduce the problem, let's examine the behavior of the connection pool with a
simple test harness.
PoolTest
Create a console application called PoolTest, reference the System.Data and
System.Data.SqlClient namespaces, and rename the default Class1.cs to
PoolTest.cs. Then overwrite the default class with this one:
class PoolTest
{
const string connString = "server=localhost;" +
"uid=scott;" +
"pwd=tiger;" +
"database=Northwind;" +
"Min Pool Size=3;" +
"Max Pool Size=3";
[STAThread]
static void Main(string[] args)
{
while(true)
{
WriteDate();
Console.WriteLine("Press [Enter] to continue...\n");
Console.ReadLine();
}
}
protected static void WriteDate()
{
SqlDataReader dr = null;
string cmdText = "SELECT date=getdate()";
SqlConnection cn = new SqlConnection(connString);
SqlCommand cmd = new SqlCommand(cmdText, cn);
try
{
cn.Open();
dr = cmd.ExecuteReader();
if (dr != null) {
while(dr.Read()) {
Console.WriteLine(dr["date"].ToString());
}
}
}
finally
{
if (dr != null) dr.Close();
cn.Close();
}
}
}
Obviously, you'll want to replace the uid and pwd
connection-string parameters with credentials local to your database instance.
Run the application and notice that the current date writes out to the console.
That's not very interesting. But what is interesting is what happened behind
the scenes. When cn.Open() was called, the managed provider instantiated
an internal class called SqlConnectionPoolManager and invoked its
GetPooledConnection method, passing into it the connection string.
The pool manager examined all current pools to see if there was one that used
a connection string that exactly matched the one it was given. In our case, there
were none. Since there wasn't one, it constructed a new ConnectionPool
object passing in the connection string as a unique identifier for that pool.
It then seeded the ConnectionPool object with three connections.
Why three? Because that's how many we defined in the Min Pool Size
parameter. Finally, when ExecuteReader was called, the SqlConnection
instance used one of these connections to execute the query and fetch the current
system date from SQL Server.
As long as we don't change the signature of our connection string, every time
we press the Enter key, the managed provider will reuse one of the
existing connections in the pool. This boosts performance greatly, because the
managed provider doesn't have to go through the expensive process of instantiating
a new Connection from scratch every time there is a database query.
We can see the database processes that the managed provider created when it seeded the connection pool. Before shutting down the console application, go into Query Analyzer and run the following query:
SELECT spid, uid=rtrim(loginame), Program_name=rtrim(Program_name),
dbname=db_name(dbid), status=rtrim(status) FROM master.dbo.sysprocesses
WHERE loginame = 'scott';
The grid displays something like this:
spid uid Program_name dbname status
52 scott .Net SqlClient Data Provider Northwind sleeping
53 scott .Net SqlClient Data Provider Northwind sleeping
54 scott .Net SqlClient Data Provider Northwind sleeping
The connections to these three processes are maintained by the managed provider
and match the Min Pool Size value that we set in our connection
string. Now let's simulate a network problem in which the connections in the
pool become invalid without our managed provider's knowledge. To do that, we'll
manually kill all three processes while the test harness is running. In Query
Analyzer, use the T-SQL kill command and, showing no mercy, kill
all three of them:
kill 52;
kill 53;
kill 54;
If you're using Oracle on the back end, then run the following
query instead: SELECT SID, SERIAL#, STATUS FROM v$session WHERE OSUSER =
'ASPNET'. To kill a session, run ALTER SYSTEM KILL SESSION 'x,y'; (where x = SID and y = SERIAL#).
Press the Enter key on the test harness again, and an ugly
SqlException is thrown. Our SqlConnection instance didn't know its
underlying connection had been severed when Open() was called, and it was only after the
ExecuteReader method tried to query the database that the problem
was discovered and the error thrown. Obviously, it
would be desirable for us to handle the exception gracefully rather than being
unprepared for it and letting it
awkwardly bubble up to the caller.
Handling Invalid Connections
So how should we handle the exception? We could trap it at the user interface level and ask the user to try the action again. Even better, we could handle the exception in such a way that the user is never made aware of the problem. This pseudocode describes behavior that would be very desirable:
if exception thrown {
close the connection
if (numberOfTries < MAX_TRIES) {
numberOfTries += 1
try again
} else {
throw exception
}
}
Let's implement this solution in the test harness. Add these two private properties to the
PoolTest class:
static int numTries = 0;
const int MAX_TRIES = 3;
Then, in the Main method, wrap the contents of the while loop in a try-catch block so that it looks like this:
try
{
WriteDate();
Console.WriteLine("Press [Enter] to continue...\n");
Console.ReadLine();
}
catch (SqlException se)
{
if (numTries < MAX_TRIES) {
Console.WriteLine("SqlException thrown. Trying again...\n");
numTries += 1;
}
else {
// MAX_TRIES reached
string errorNum = se.Number.ToString();
Console.WriteLine(" SqlException Number " + errorNum +
" Message: " + se.Message);
Console.ReadLine();
}
}
Now if you run the test harness again and kill the three processes in the pool, you'll discover that the application recovers quite nicely by trying again until it finds a good connection or the pool manager creates and adds another valid one. It's important to close both the data reader and connection as soon as they are no longer needed. If you don't and they go out of scope, the application will leak a connection. I wrapped them in a finally block to make sure that even if an exception is thrown they will get closed properly. In fact, if an exception is thrown then you must explicitly close the connection to mark it as invalid in the pool. Only if a connection is marked as invalid will the pool manager remove it from the pool on its next scan.
A Simple Data Access Component
Console applications are one thing, but what about implementing this solution
in a real-world, data access component? In this section, I'll suggest one
way of doing just that by creating a simple data access component (DAC) that
builds on what we've learned so far about connection pool behavior. To begin,
add a new class to the PoolTest project called SqlHelper.cs
and make sure it references the System.Data and
System.Data.SqlClient namespaces. Then add these two private variables and
a static constructor to the class:
private static int NUM_TRIES;
private const int MAX_TRIES = 3;
static SqlHelper()
{
NUM_TRIES = 0;
}
Last, add this familiar code as a static public method that will fetch a
SqlDataReader from the database:
public static SqlDataReader ExecuteReader(string conStr,
CommandType eType,
string commandText)
{
SqlConnection cn = null;
SqlDataReader dr = null;
SqlCommand cmd = null;
try
{
cn = new SqlConnection(connString);
cmd = new SqlCommand(commandText, cn);
cmd.CommandType = eType;
cn.Open();
dr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
}
catch (SqlException se)
{
if (dr != null) dr.Close();
cn.Close();
if (NUM_TRIES < MAX_TRIES)
{
dr = ExecuteReader(connString, eType, commandText);
NUM_TRIES += 1;
}
else
throw se;
}
return dr;
}
Notice that in the catch block a recursive call is made. If the connection
was severed, then closing it will mark it as invalid in the pool. That way, the
recursive call will get a different connection on the next try. (If you're using
Oracle, this will not work. See the "gotcha" below.) Now, with the SqlHelper class built, we can modify the test harness to call the
ExecuteReader method. Replace the old while loop with this one:
while(true)
{
SqlDataReader dr = SqlHelper.ExecuteReader(conStr,
CommandType.Text,
"select date=getdate()");
if (dr != null)
{
while (dr.Read())
Console.WriteLine(dr["date"].ToString());
dr.Close();
}
Console.WriteLine("Press [Enter] to continue...\n");
Console.ReadLine();
}
Now run the test harness and pull back a few dates from the database. This
time, if you
kill all three sessions while it is running, the ExecuteReader method
will recover and try again. In this way, the DAC can successfully handle the exception
and get a valid connection from the pool.
Only if there is a persistent problem of some sort, and the number of tries reaches MAX_TRIES, is an error
allowed to bubble up to the caller.
Oracle 9i Gotcha
The details of ODP.NET,
Oracle's implementation of the .NET managed provider, are hidden from us, so
I can't explain why a certain bug happens. But in particular circumstances,
such as when a session is killed (raising OracleException ORA-00028)
or the connection is otherwise severed, the OracleConnection class
cannot reconnect to the database. The bottom line is that if you port the simple
DAC above over to use against an Oracle 9i database instance, you'll be disappointed.
Instead of recovering from the exception as expected, you'll next get OracleException
ORA-01012 ("Not logged on") on each recursive call to ExecuteReader
until MAX_TRIES is finally reached, and the error bubbles up to
the caller.
I do have a workaround for Oracle that I've tested thoroughly. My design
involves a straightforward
Observer Pattern in which
an observer class is notified if the subject class encounters an OracleException,
and then re-seeds the connection pool for the subject before the recursive call
is made. If you use Oracle on the
back end, feel free to contact me at still_james@hotmail.com and I'll email my implementation to you.
A Final Thought
In the interest of making the test harness simple, I used the integer constant
MAX_TRIES to prevent an infinite regress. But in a real-world application,
we have to assume that all connections up to the Max Pool Size parameter
value might be invalid. (If this parameter is not explicitly set in the connection
string, then for SqlConnection the default is 100.) A better solution than
hard-coding a constant is to use the Max Pool Size parameter instead of
an arbitrary MAX_TRIES constant. My final thought, expressed in code (of
course), is a handy method that will return the value of any connection string parameter
passed into it. I'll leave it to you to improve the DAC with it:
private string GetParameterValue(string conStr, string searchParam)
{
// searchParam, e.g., "Max Pool Size" or "Incr Pool Size"
int posBegin = 0;
int posEnd = 0;
char[] delimiter = {';'};
string paramVal = string.Empty;
posBegin = conStr.IndexOf(searchParam);
if (posBegin > -1)
{
// add one for '=' char
posBegin += searchParam.Length + 1;
if (conStr.LastIndexOfAny(delimiter) > posBegin)
// somewhere in the middle
posEnd = conStr.IndexOf(';', posBegin);
else
// at end of string
posEnd = conStr.Length;
paramVal = conStr.Substring(posBegin, (posEnd - posBegin));
}
return paramVal;
}
Related References
" Best Practices for Using ADO.NET"
" The .NET Connection Pool Lifeguard"
" Connection Pooling for the .NET Framework Data Provider for SQL Server"
James Still James Still is an experienced software developer in Portland, Oregon. He collaborated on "Visual C# .NET" (Wrox) and has immersed himself in .NET since the Beta 1 version was released.
Return to ONDotnet.com
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 15 of 15.
-
Oracle - how to validate connections
2004-02-24 15:16:40 akeh [Reply | View]
-
Oracle - how to validate connections
2006-01-04 12:19:44 random_ [Reply | View]
Has anyone actually gotten this to work? I have tried both ODP 9.0.4.01 and 9.0.7 with the same result:
System.ArgumentException: Keyword not supported: 'validate connection'
When adding"Validate Connection=true;"to the connection string. Also tried other variants with similar results.
-
Oracle - how to validate connections
2006-03-16 10:07:08 solutionsahead [Reply | View]
Hi Akeh!
I have a .Net - Oracle connection problem. Can you help?
I have developed a E-business web application in Visual Studio.NET1.0 with ASP.NET, C# and Oracle9i as the database. The application is hosted on a windows Terminal Server with 2 GB RAM and 80 GB HDD resource.
There is a lot of inactive sessions. The connections don't seem to be released from the pool whether I call dispose(), close(), or both, and this keeps building-up as users connect to the portal. We have used .NET 1.1 frame work with service pack 1 on the server. Can someone provide me with a solution recommendation asap?
Thanks in anticipation?
-
Slightly bad advice
2004-02-25 08:00:49 asills [Reply | View]
While the technique provided here is definitely valid, one huge thing you missed is I would probably bet that a lot of novice web developers will take this code as is and use it in their web applications, without realizing the multi-threaded problems that might arise when using this code.
Since ASP.NET is a multi-threaded environment, multiple threads could be failing and accessing the static variable at the same time, causing the retries variable to change from multiple threads, which would cause its value to be changed by one thread while another thread is dependent on the value being a certain thing.
So just FYI, be wary if you use this code, that it's not thread-safe. -
Slightly bad advice
2004-04-15 22:58:50 AndrewG [Reply | View]
A bit more information
You're right in saying ASP.NET is multi-threaded, but each request comes in on it's own thread and is seviced by a unique application instance. This means you don't have to concern yourself with MT issues - simply because all variables are local to your application instance. ASP.NET application objects are pooled and removed/returned to the pool upon each request/reply.
Shared/static variables will require protection from concurrent access. This is due to shared/static variables being maintained at the AppDomain level and not the Application level.
In the example of connection pooling a simple non-shared/static variable in the application object (initialized with the pool count) would be sufficient
-
Kill your connection?
2004-03-14 17:52:37 joshross [Reply | View]
How do you kill your connection when the user does not have access to the t-sql kill command? -
Kill your connection?
2005-02-01 22:39:55 sanjit@myuberall.com [Reply | View]
How can i kill the connection, in Oracle.
i am using Oracle 9i with ASP.NET & C#, so connection pooling peoblem occur,
how can i resolve it. -
Kill your connection?
2005-04-25 10:30:41 camesvoliaj [Reply | View]
Hi Sanjit,
I am facing the same problem that you have mentioned in your email. Did you find a possible solution to the problem when Oracle is runnign at the back end..or did u get hold of the implementation that the author is talking of in the article. I am a novice in this field and will appreciate any help that I get.
Thanks, -
Kill your connection?
2006-03-16 10:03:31 solutionsahead [Reply | View]
Hi Sanjit,
Just got to see the problem faced by you? Did you solve the same? If so plz mail me back asap...
I have developed an E-business web application in Visual Studio.NET1.0 with ASP.NET, C# and Oracle9i as the database. The application is hosted on a windows Terminal Server with 2 GB RAM and 80 GB HDD resource.
There is a lot of inactive sessions. The connections don't seem to be released from the pool whether I call dispose(), close(), or both, and this keeps building-up as users connect to the portal. We have used .NET 1.1 frame work with service pack 1 on the server. Can someone provide me with a solution recommendation asap?
Thanks in anticipation?
-
recursion
2004-11-04 03:26:10 kniva [Reply | View]
not sure ,but don't you think that in the recursion it should be:
NUM_TRIES += 1;
dr = ExecuteReader(connString, eType,commandText);
instead
dr = ExecuteReader(connString, eType,commandText);
NUM_TRIES += 1;
-
Recursive
2005-02-23 09:47:48 CodeHaxor [Reply | View]
I think the recursive idea is a bad idea,
it can simply blow the stack, if the pool size
was lets say 1000, you would make a 1000 retries
recursively, dont' you think that perhaps
a while loop would solve this issue in a much
cleaner and faster way?
Also in regards to keeping a connection
static in asp.net that would also be a very
bad idea, when multiple datareaders or writers
are trying to access the same connection pool;
it will periodically throw exceptions of
datareader or writer already in use.
I made the fatal mistake of creation my ConnectionManager within my table access classes
static and if two clients using my asp.net application where lets say doing a search
the datareader would get used by the first one
and an exception will be thrown for the second.
Microsoft recommends creating a new SqlConnection
for every io; this realistically wont' be a problem considering it will all be pooled at the end. -
Recursive
2005-07-27 13:18:18 Bill [Reply | View]
Yes the recursive idea is bad. Especially as written in the sample code.
First the num_tries counter never gets incremented as the recursion progresses.
Secondly, suppose max_tries is 100. And it takes 50 tries to find a working connection. Once this happens, the ExecuteDataReader will recursively re-execute 50 times as it ripples up the stack.
Too bad the author didn't understand recursion or test his code
-
After running SqlHelper.ExecuteReader connection pooling stops
2006-01-25 23:36:42 ManojAgarwal [Reply | View]
Hi,
I ran this sample and noticed that if ,as mentioned, the pooled connections are closed then this code recovers from it. But what I noticed is that after recovering the no. of connections in the pool is just one (I am checking open connections by the method mentioned)- so somehow pooling is not taking place. I didn't modify the code.
Also, another thing I noticed is that for SQL Server (I have 2005 version) the pool size is not 100 but 2 by default.
-
.NET-Oracle connectivity issue?
2006-03-16 10:08:54 solutionsahead [Reply | View]
Hi,
I have developed a E-business web application in Visual Studio.NET1.0 with ASP.NET, C# and Oracle9i as the database. The application is hosted on a windows Terminal Server with 2 GB RAM and 80 GB HDD resource.
There is a lot of inactive sessions. The connections don't seem to be released from the pool whether I call dispose(), close(), or both, and this keeps building-up as users connect to the portal. We have used .NET 1.1 frame work with service pack 1 on the server. Can someone provide me with a solution recommendation asap?
Thanks in anticipation?






Note that validation causes a round trip to the database for each connection coming out of the pool, which is very expensive in terms of performance. Therefore, this attribute should be used only if absolutely necessary.