Подтвердить что ты не робот

Что такое хороший стиль кодирования С# для ловли SQLException и повторной попытки

У меня есть метод, который вызывает функцию SQLServer для выполнения свободного текстового поиска по таблице. Эта функция будет иногда при первом результате вызова в SQLException: "Время прерывания Word для полнотекстовой строки запроса". Поэтому обычно я хочу повторить этот запрос, потому что он будет успешным при последующих запросах. Что такое хороший стиль для структурирования логики повтора. На данный момент у меня есть следующее:

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;
4b9b3361

Ответ 1

Спасибо за отзывы. Я отвечаю на это сам, поэтому я могу включить элементы из приведенных ответов. Пожалуйста, дайте мне знать, если я что-то пропустил. Мой метод:

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

И я переработал оригинальный метод повторного использования. Еще много уровней гнездования. Он также полагается на конструктор по умолчанию для контекста данных, который может быть слишком строгим. @Martin, я считал, что в том числе ваш метод PreserveStackTrace, но в этом случае я не думаю, что он действительно добавляет достаточное значение - полезно знать для будущей ссылки спасибо:

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++;
        }
    }
}

Ответ 2

Я бы изменил обработку исключений, чтобы повторять только некоторые ошибки:

  • 1204, 1205 взаимоблокировок
  • -2 timeout
  • -1 сломанное соединение

Это основные ошибки "повторного использования"

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

Изменить, я очищаю забыл о ожиданиях, чтобы вы не забивали ящик SQL:

  • Добавить ожидание в течение 500 мс в тупике
  • Добавить задержку 5 секунд при тайм-ауте

Изменить 2:

Я разработчик DBA, не делайте много С#. Мой ответ заключался в исправлении обработки исключений для вызовов...

Ответ 3

Мое перечисление повторных попыток для sql выглядит следующим образом:

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

Ответ 4

Это не хороший стиль, но иногда вам приходится это делать, потому что вы просто не можете изменить существующий код и иметь дело с ним.

Я использую следующий общий метод для этого сценария. Обратите внимание на метод PreserveStackTrace(), который иногда может быть очень полезен в сценарии повторного броска.

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);
}

Вы бы назвали это так:

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

Ответ 5

Нет хорошего стиля для того, чтобы делать что-то подобное. Вам лучше разобраться, почему запрос не срабатывает в первый раз, но преуспевает во второй раз.

Кажется возможным, что Sql Server должен сначала составить план выполнения, а затем выполнить запрос. Таким образом, первый вызов завершается с ошибкой, потому что комбинированное время превышает ваше свойство тайм-аута и выполняется во второй раз, поскольку план выполнения уже скомпилирован и сохранен.

Я не знаю, как работает UserDataContext, но может быть, если у вас есть опция Prepare запроса до его фактического выполнения.

Реальный ответ: Если бы мне пришлось это сделать, я бы повторил только один раз и не снова, как это:

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;

Хотя я могу доверять вам не злоупотреблять процессом повторных попыток, вы соблазняете своего преемника увеличить количество повторных попыток как быстрое исправление.

Ответ 6

Вы можете просто использовать свойства SqlConnectionStringBuilder для повторения соединения sql.

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

Примечание: - Требуется .Net 4.5 или новее.

Ответ 7

Вытащите соответствующий код в свой собственный метод, затем используйте рекурсию.

Псевдо-код:

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();
}