A volte ci si perde in un bicchier d'acqua :-)

Un mese fa stavamo effettuando, con Markino, un test di performance su un web service implementato con  Visual Studio 2005 .NET CTP di dicembre il quale non fa altro che effettuare una query al database (SQL Server) e restituire dei valori (niente di più banale). Soltanto che in regime di stress (molto stress !!) ci siamo resi conto che dopo alcuni minuti di esecuzione del test si sollevavano alcune eccezzioni di quesyo tipo:

"The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached."

A questo punto inziammmo ad investigare e verificammo che il problema era dovuto alla mancata chiusura delle connessioni. Eppure il codice pareva del tutto lineare:

try
{
  SqlCommand cmd = new SqlCommand(sel_cmd, cn);
  // Fills parameters...

  cn.Open();
  SqlDataReader reader = cmd.ExecuteReader();

  while (reader.Read())
  {
   // do something
  }
  reader.Close();
}
catch (Exception ex)
{
  // Trace it...
}
finally
{
  if(cn.State == System.Data.ConnectionState.Open)
    cn.Close();
}

In realtà, il codice di cui sopra nasconde un'insidia importante:

finally
{
  if(cn.State == System.Data.ConnectionState.Open)
    cn.Close();
}

Infatti, in condizioni di stress capita spesso che lo stato della connessione non sia chiusa ma nemmeno aperta e quindi la connessione non verrà mai chiusa (se non quando scatta il garbage collector...ma potrebbe essere troppo tardi). Quindi, il gestore delle connessioni del db tenta di creare nuove connessioni anche quando ha raggiunto il limite (by default è 100, ma si può elevare definendo il parametro Max Pool Size = 1000 nella stringa di connessione) e solleva di conseguenza l'eccezzione di cui sopra. Per risolvere il problema bisogna chiudere la connessione in ogni caso, quindi il codice diviene:

finally
{
  cn.Close();
}

E' interessante notare che se avessimo usato la clausola using:

using(SqlConnection cn = new SqlConnection(cn_str))
{
  SqlCommand cmd = new SqlCommand(sel_cmd, cn);
  cmd.Parameters.Add(new SqlParameter("@initials", initials[rnd.Next(7)] + "%"));

  cn.Open();
  SqlDataReader reader = cmd.ExecuteReader();

  while (reader.Read())
  {
   // do something
  }
}

avremmo risolto il problema in quanto la Dispose (chiamata implicitamente con lo statement using) dell'oggetto SqlConnection chiama automaticamente il metodo Close. E' importante sapere che nel framework 1.x anche l'uso dello statement using ripropone il problema, in quanto internamente il metodo Dispose di SqlConnection verifica se lo stato della connessione è Open (esattamente come facevamo noi all'inizio) e quindi fallisce in condizioni di stress.

Dato che dal Framework 1.x al 2.0 ci sono dei cambiamenti sostanziali nell'implementazione della Dispose suggerisco di chiamare comunque la Close esplicitamente in modo da divernire immuni da futuri cambiamenti del framework.