Что такое хороший стиль кодирования С# для ловли 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;
Ответы
Ответ 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();
}