Что такое хороший стиль кодирования C # для перехвата SQLException и повторной попытки - PullRequest
18 голосов
/ 27 января 2011

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

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;

Ответы [ 8 ]

17 голосов
/ 27 января 2011

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

  • 1204, 1205 взаимоблокировок
  • -2 тайм-аут
  • -1 разрывается

Это основные «повторяющиеся» ошибки

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

Редактировать, я убрал забыл об ожиданиях, чтобы вы не забили окно SQL:

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

Редактировать 2:

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

12 голосов
/ 28 января 2011

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

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++;
        }
    }
}
8 голосов
/ 04 июля 2011

Мое перечисление retryables для sql выглядит так:

SqlConnectionBroken = -1,
SqlTimeout = -2,
SqlOutOfMemory = 701,
SqlOutOfLocks = 1204,
SqlDeadlockVictim = 1205,
SqlLockRequestTimeout = 1222,
SqlTimeoutWaitingForMemoryResource = 8645,
SqlLowMemoryCondition = 8651,
SqlWordbreakerTimeout = 30053
7 голосов
/ 27 января 2011

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

Я использую следующий универсальный метод для этого сценария.Обратите внимание на метод 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);
3 голосов
/ 27 января 2011

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

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

Я не знаю, как работает UsersDataContext, но это может быть такчто у вас есть возможность 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;

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

1 голос
/ 27 января 2011

Я думаю, что аннотирование метода с аспектом, указывающим количество повторов, приведет к более структурированному коду, хотя для этого требуется некоторое инфраструктурное кодирование.

0 голосов
/ 31 июля 2017

Вы можете просто использовать свойства 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 или более поздняя версия.

0 голосов
/ 28 января 2011

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

Псевдо-код:

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();
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...