2011-01-27 13 views
16

Tengo un método que llama a una función SQLServer para realizar una búsqueda de texto libre en una tabla. Esa función ocasionalmente en la primera llamada dará como resultado una SQLException: "Tiempo de espera agotado para la cadena de consulta de texto completo". Entonces, por lo general, quiero volver a intentar la solicitud porque tendrá éxito en las solicitudes posteriores. ¿Qué es un buen estilo para estructurar la lógica de reintento? Por el momento tengo el siguiente:Qué es un buen estilo de codificación C# para capturar SQLException y reintentar

var retryCount = 0; 
var results = new List<UserSummaryDto>(); 
using (var ctx = new UsersDataContext(ConfigurationManager.ConnectionStrings[CONNECTION_STRING_KEY].ConnectionString)) 
{ 
    for (; ;) 
    { 
     try 
     { 
      results = ctx.SearchPhoneList(value, maxRows) 
         .Select(user => user.ToDto()) 
         .ToList(); 
      break; 
     } 
     catch (SqlException) 
     { 
      retryCount++; 
      if (retryCount > MAX_RETRY) throw; 
     } 
    } 
} 

return results; 
+1

En realidad, es conceptualmente simple y debería funcionar. Aunque advertiría contra el uso excesivo de Var. A veces, un int es solo un int. – MAW74656

+0

'On Currículum de reanudación siguiente' en Visual Basic fue conceptualmente simple, eso no lo hizo una buena idea. – MusiGenesis

+0

@ MAW74656: He votado su comentario sobre la base de la observación 'var'. IMO, el compilador debe prohibir su uso donde oculta el tipo de objeto, como lo hace aquí. – MusiGenesis

Respuesta

12

Gracias por todos los comentarios. Estoy respondiendo esto yo mismo, así que puedo incorporar elementos de las respuestas dadas. Por favor, avíseme si me he perdido algo. Mi método se convierte en:

var results = new List<UserSummaryDto>(); 
Retry<UsersDataContext>(ctx => results = ctx.SearchPhoneList(value, maxRows) 
              .Select(user => user.ToDto()) 
              .ToList()); 
return results; 

Y he modificado el método original para su reutilización. Todavía hay muchos niveles de anidación. También se basa en que existe un constructor predeterminado para el contexto de datos que puede ser demasiado restrictivo. @ Martin, consideraba incluyendo su PreserveStackTrace método pero en este caso no creo que realmente añade valor suficiente - bueno saber para referencia futura gracias:

private const int MAX_RETRY = 2; 
private const double LONG_WAIT_SECONDS = 5; 
private const double SHORT_WAIT_SECONDS = 0.5; 
private static readonly TimeSpan longWait = TimeSpan.FromSeconds(LONG_WAIT_SECONDS); 
private static readonly TimeSpan shortWait = TimeSpan.FromSeconds(SHORT_WAIT_SECONDS); 
private enum RetryableSqlErrors 
{ 
    Timeout = -2, 
    NoLock = 1204, 
    Deadlock = 1205, 
    WordbreakerTimeout = 30053, 
} 

private void Retry<T>(Action<T> retryAction) where T : DataContext, new() 
{ 
    var retryCount = 0; 
    using (var ctx = new T()) 
    { 
     for (;;) 
     { 
      try 
      { 
       retryAction(ctx); 
       break; 
      } 
      catch (SqlException ex) 
       when (ex.Number == (int) RetryableSqlErrors.Timeout && 
         retryCount < MAX_RETRY) 
      { 
       Thread.Sleep(longWait); 
      } 
      catch (SqlException ex) 
       when (Enum.IsDefined(typeof(RetryableSqlErrors), ex.Number) && 
         retryCount < MAX_RETRY) 
      { 
       Thread.Sleep(shortWait); 
      } 
      retryCount++; 
     } 
    } 
} 
17

que cambiaría la gestión de excepciones a reintentar solamente en ciertos errores:

  • 1204, 1205 puntos muertos
  • -2 tiempo de espera
  • -1 conexión rota

Estos son los errores básicos "recuperables"

catch (SqlException ex) 
{ 
    if !(ex.Number == 1205 || ex.Number == 1204 || ...) 
    { 
     throw 
    } 
    retryCount++; 
    if (retryCount > MAX_RETRY) throw; 
} 

Editar, limpio olvidó acerca de las esperas para que no se martilla el cuadro SQL:

  • Añadir un 500 ms esperan en punto muerto
  • añadir un retardo de 5 segundos de tiempo de espera

Editar 2:

Soy un desarrollador de DBA, no hago mucho C#. Mi respuesta fue corregir el procesamiento de excepciones para las llamadas ...

+0

http://www.parashift.com/c++faq-lite/exceptions.html#faq-17.2 –

+0

@jani: su enlace puede mejorar la calidad de C# (soy desarrollador DBA: C# no es un núcleo habilidad para mí), pero no ayuda a detectar errores en la base de datos para resolver si es posible volver a intentarlo. – gbn

+1

Esto está bien para el propósito. Lo único que haría es agrupar los códigos de error en una matriz y usar .Contains (ex.Number); lo hace un poco más compacto. – KeithS

6

No es un buen estilo, pero a veces tienes que hacerlo, porque simplemente no puedes cambiar el código existente y tienes que lidiar con él.

Estoy utilizando el siguiente método genérico para este escenario. Tenga en cuenta el método PreserveStackTrace(), que a veces puede ser muy útil en un escenario de re-lanzamiento.

public static void RetryBeforeThrow<T>(Action action, int retries, int timeout) where T : Exception 
{ 
    if (action == null) 
     throw new ArgumentNullException("action", string.Format("Argument '{0}' cannot be null.", "action")); 

    int tries = 1; 

    do 
    { 
     try 
     { 
      action(); 
      return; 
     } 
     catch (T ex) 
     { 
      if (retries <= 0) 
      { 
       PreserveStackTrace(ex); 
       throw; 
      } 

      Thread.Sleep(timeout); 
     } 
    } 
    while (tries++ < retries); 
} 

/// <summary> 
/// Sets a flag on an <see cref="T:System.Exception"/> so that all the stack trace information is preserved 
/// when the exception is re-thrown. 
/// </summary> 
/// <remarks>This is useful because "throw" removes information, such as the original stack frame.</remarks> 
/// <see href="http://weblogs.asp.net/fmarguerie/archive/2008/01/02/rethrowing-exceptions-and-preserving-the-full-call-stack-trace.aspx"/> 
public static void PreserveStackTrace(Exception ex) 
{ 
    MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace", BindingFlags.Instance | BindingFlags.NonPublic); 
    preserveStackTrace.Invoke(ex, null); 
} 

ustedes lo llaman así:

RetryBeforeThrow<SqlException>(() => MethodWhichFails(), 3, 100); 
+0

'throw' no debe eliminar el marco original de la pila. Solo si usaste 'throw ex' –

+1

@ František Žiačik Generaly debería hacerlo. Lea el artículo en el blog de Fabrice en mi respuesta y verá lo que quiero decir. –

+3

Lo intenté yo mismo y resulta que tienes razón. Uno nunca deja de aprender. La otra forma de conservar la información es envolver la excepción ('throw new Exception (" Message ", ex)') –

4

no hay un buen estilo para hacer algo como esto. Sería mejor averiguar por qué la solicitud falla la primera vez, pero sucede por segunda vez.

Parece posible que el servidor Sql tenga que compilar inicialmente un plan de ejecución y luego ejecutar la consulta. Por lo tanto, la primera llamada falla porque los tiempos combinados superan la propiedad de tiempo de espera excedido y la segunda vez porque el plan de ejecución ya está compilado y guardado.

No sé cómo funciona el UserDataContext, pero puede ser que tenga la opción de Prepare consultar antes de ejecutarlo.

respuesta real: Si tuviera que hacer esto, me gustaría volver a intentar sólo una vez y no más, como esto:

var results = new List<UserSummaryDto>(); 
using (var ctx = new 
    UsersDataContext(ConfigurationManager.ConnectionStrings[CONNECTION_STRING_KEY].ConnectionString)) 
{ 
     try 
     { 
      results = ctx.SearchPhoneList(value, maxRows) 
         .Select(user => user.ToDto()) 
         .ToList(); 
      break; 
     } 
     catch (SqlException) 
     { 
      try 
      { 
       results = ctx.SearchPhoneList(value, maxRows) 
         .Select(user => user.ToDto()) 
         .ToList(); 
       break; 
      } 
      catch (SqlException) 
      { 
       // set return value, or indicate failure to user however 
      } 
     } 
    } 
} 

return results; 

Mientras que yo podría confiar que a no abusar del proceso de reintento, se Sería tentador para su sucesor aumentar el conteo de reintentos como una solución rápida.

+1

Iba a decir esto, pero era demasiado gallina. Si los tiempos de espera son un gran problema, y ​​haciendo que el sistema llegue a donde puede mantenerse sin tener que hacer reintentos, consideraría suavizar las solicitudes en una cola y luego responder asincrónicamente. –

+3

No estoy de acuerdo: algunos errores en el servidor SQL son inherentemente recuperables. Si golpea el servidor durante la reconstrucción del índice, por ejemplo, esperar y reintentar es inofensivo y evita el soporte innecesario o el registro – gbn

+0

Averiguar por qué la consulta falla es una buena idea, pero no es particularmente defensiva. Como indiqué en mi pregunta original, la función realiza una búsqueda de texto libre y la excepción refleja eso. He utilizado el Administrador de configuración del servidor Sql para iniciar automáticamente el servicio de filtro de texto completo, pero todavía puedo obtener la excepción en la primera solicitud. Esto está en mi máquina de desarrollo usando una instalación local de Sql Server. –

-6

Extraiga el código correspondiente en su propio método, luego use recursividad.

Pseudo-código:

try 
{ 
    doDatabaseCall(); 
} 
catch (exception e) 
{ 
    //Check exception object to confirm its the error you've been experiencing as opposed to the server being offline. 
    doDatabaseCall(); 
} 
+0

Y si el servidor está desconectado durante 2 horas, ¿propones seguir intentándolo ...? – gbn

+0

@ gbn- No, por supuesto que no. Solo le estamos dando conceptos aquí, no escribiendo su código completo. Ver mi edición – MAW74656

+0

Quizás una combinación de su contador y mi abstracción sea lo mejor. – MAW74656

8

Mi enumeración de retryables para SQL tiene este aspecto :

SqlConnectionBroken = -1, 
SqlTimeout = -2, 
SqlOutOfMemory = 701, 
SqlOutOfLocks = 1204, 
SqlDeadlockVictim = 1205, 
SqlLockRequestTimeout = 1222, 
SqlTimeoutWaitingForMemoryResource = 8645, 
SqlLowMemoryCondition = 8651, 
SqlWordbreakerTimeout = 30053 
0

Puede simplemente usar las propiedades SqlConnectionStringBuilder para volver a intentar la conexión sql.

var conBuilder = new SqlConnectionStringBuilder("Server=.;Database=xxxx;Trusted_Connection=True;MultipleActiveResultSets=true"); conBuilder.ConnectTimeout = 90; conBuilder.ConnectRetryInterval = 15; conBuilder.ConnectRetryCount = 6;

Nota: - Se requiere .Net 4.5 o posterior.

Cuestiones relacionadas