#c_sharp #инспекция_кода
При операциях с сетью и IO часто вылазят ограничения, которые легко решить даже пользователю (пусть и опытному, да), но сложно узнать о них заранее. Занятый файл, нестабильное соединение - мне часто нехватает кнопки повторить, особенно когда весь процесс берет и откатывается. Накидал пока на коленке реализацию более-менее универсального решения этой проблемы: public enum ExceptionHandle { Abort, Retry, Ignore } public class ExceptionEventArgs { public Exception Exception { get; } public ExceptionHandle? Handled { get; set; } public ExceptionEventArgs(Exception ex) { this.Exception = ex; } } public static class ExceptionHandler { public static event EventHandlerHandler; public static void TryExecute(Action action) { TryExecute(() => { action(); return true; }, false); } public static T TryExecute (Func action, T whenIgnored) { ExceptionHandle? handled = ExceptionHandle.Retry; while (handled == ExceptionHandle.Retry) { try { return action(); } catch (Exception ex) { handled = OnHandler(new ExceptionEventArgs(ex)); if (handled.HasValue) { switch (handled.Value) { case ExceptionHandle.Abort: throw; break; case ExceptionHandle.Retry: break; case ExceptionHandle.Ignore: break; default: throw new ArgumentOutOfRangeException(); } } else { throw; } } } return whenIgnored; } private static ExceptionHandle? OnHandler(ExceptionEventArgs e) { if (Handler == null || !Handler.GetInvocationList().Any()) { ExceptionDispatchInfo.Capture(e.Exception).Throw(); } else { Handler.Invoke(null, e); } return e.Handled; } } Таким образом, любой подписчик ExceptionHandler.Handler может либо резолвить проблемы в автоматическом режиме, либо вываливать решение на пользователя. Любой опасный код теперь можно обернуть: var tested = ExceptionHandler.TryExecute(() => { using (var destination = new MemoryStream()) { using (Stream stream = entry.Open()) stream.CopyTo(destination); return destination.Length == entry.Length; } }, false); В целом, текущая реализация мне кажется уже терпимой и она работает. Но, подозреваю, что такие решения уже где то есть, просто я не смог их найти. Может кто-то посоветует где взять или хотя бы посмотреть готовые решения? Ну и, если в моем коде есть косяки - тоже не отказался бы от помощи. UPD: да, я понимаю, что даже так остаются проблемные ситуации - экшн может быть одноразовым (закрывать соединение, гробить sql сессию, да что угодно делать). Это уже остается на совести того, кто использует код. Хотя, интересные варианты по этой проблеме я бы тоже глянул, это же фиг ограничишь. UPD2: пока не смог придумать, можно ли оборачивать один такой блок в другой, а то сейчас в итоге на аборте внутреннего блока внешний снова уходит на обработку.
Ответы
Ответ 1
Мне не кажется, что существует общее решение проблемы. Поскольку то, что вы кодируете — это по сути бизнес-логика, которая «разнообразна как сама жизнь», вы не сможете заранее покрыть все возможные случаи. Навскидку что с кодом не так: Выполнение происходит синхронно, блокирующим образом. Это не всегда так, очень часто «строительные блоки» бизнес-логики формируются из асинхронных процедур. Этой возможности у вас нет. Модель подписки на событие у стороннего объекта мне кажется слишком сложной и нарушающей линейную логику. У вас один, статический экземпляр ExceptionHandler'а, а значит, вам придётся подписывать несколько обработчиков одновременно. При этом нужен механизм, который решает, ответственен ли данный обработчик за данную ошибку или нет. Этой логики у вас нет, и она получится достаточно сложной. Также у универсального объекта должны возникать проблемы с многопоточным доступом, как только вы попытаетесь сделать его сложнее. Очень часто простого повторения действия недостаточно, т. к. условия не поменялись, и значит, новое действие завершится с той же ошибкой. Нужно какое-нибудь дополнительное действие: сделать паузу, подобрать другие исходные данные, провести диалог с пользователем и. т. д. В вашем дизайне эти все дополнительные действия придётся паковать в обработчик события, что не очень читаемо. Мне кажется, вы не должны пытаться построить универсальную логику, это и не выйдет, т. к. случаев очень много. Намного лучше, проще и эффективнее писать маленькие вспомогательные функции на каждый случай жизни, и собирать их в классы-утилиты. Например, такую простую логику static async TaskTrySeveralTimesWithGrowingTimeout ( int count, Func > taskCreator, TimeSpan timeoutDiff, Action failureLogger) { TimeSpan timeout = TimeSpan.Zero; for (int i = 0; i < count; i++) // count раз: { timeout += timeoutDiff; // нарастим таймаут try { // пытаемся выполнить Task, если не будет return await taskCreator(); // исключения, возвращаем результат } catch (Exception ex) when (i < count - 1) // ловим исключение всегда, кроме { // последней итерации failureLogger($"Operation failed #{i}, retrying after {timeout}. " + $"Exception was {ex.Message}"); // залогировали } await Task.Delay(timeout); // выдерживаем паузу до следующего запуска } // сюда мы не попадём, т. к. если последняя итерация провалилась, // то было выброшено исключение throw new Exception("cannot happen"); } достаточно сложно закодировать в терминах ExceptionHandler'а. Тестовой функцией пользоваться так: async Task GetFileContent(string path) { using (var sr = new StreamReader(path)) return await sr.ReadToEndAsync(); } // ... try { var text = await TrySeveralTimesWithGrowingTimeout( count: 3, taskCreator: () => GetFileContent(path), timeoutDiff: TimeSpan.FromSeconds(10), failureLogger: Console.Error.WriteLine); // тут ещё километр логики Console.WriteLine(text); } catch (IOException ex) { Console.Error.WriteLine($"Failed: {ex.Message}"); } Но это не универсальная функция, это просто пример того, что можно легко закодировать вручную. Ответ 2
"Я не доктор, но посмотреть могу" (С) Готовое решение найдено на просторах Сети и выглядит весьма неплохо: public interface ISequentialActivity { bool Run(); } public enum UserAction { Abort, Retry, Ignore } public class FailureEventArgs { public UserAction Action = UserAction.Abort; } public class SequentialActivityMachine { private Queueactivities = new Queue (); public event Action OnFailed; protected void PerformOnFailed(FailureEventArgs e) { var failed = this.OnFailed; if (failed != null) failed(e); } public void Add(ISequentialActivity activity) { this.activities.Enqueue(activity); } public void Run() { while (this.activities.Count > 0) { var next = activities.Peek(); if (!next.Run()) { var failureEventArgs = new FailureEventArgs(); PerformOnFailed(failureEventArgs); if (failureEventArgs.Action == UserAction.Abort) return; if (failureEventArgs.Action == UserAction.Retry) continue; } activities.Dequeue(); } } } Ответ 3
Есть способ с использованием PostSharp. Решение разрабатывалось на основе примеров: пример №1, пример №2. Преимущества: Исходный код подвергается минимальным изменениям, добавляется лишь один атрибут к методам. Никаких ограничений на методы. Атрибут может быть применен как к статическим, так и к инстансным методам, к конструкторам и свойствам, с параметрами или без. Недостатки: Хотя PostSharp имеет бесплатную версию, нормальная версия стоит денег. Быть может есть бесплатные аналоги, имеющие схожую функциональность... Автообработка исключений работает только для метода целиком, и не работает для части метода. Пусть и небольшое, но увеличение времени компиляции и времени выполнения кода. Дополнительная зависимость в проекте. Не предусмотрена возможность запихнуть штатными средствами делегат в атрибут, поэтому текущий обработчик исключений лежит в статическом свойстве CurrentExceptionHandler.HandlerFunc. Это костыль. Код: public enum ExceptionHandlerResult { Abort, Retry, Ignore } public static class CurrentExceptionHandler { public static FuncHandlerFunc { get; set; } = DefaultHandlerFunc; public static ExceptionHandlerResult DefaultHandlerFunc(Exception e) { var response = MessageBox.Show(e.ToString(), "Error", MessageBoxButtons.AbortRetryIgnore, MessageBoxIcon.Error); switch (response) { case DialogResult.Abort: return ExceptionHandlerResult.Abort; case DialogResult.Retry: return ExceptionHandlerResult.Retry; case DialogResult.Ignore: return ExceptionHandlerResult.Ignore; default: throw new ArgumentOutOfRangeException(); } } } [PSerializable] public class HandleExceptionAttribute : MethodInterceptionAspect { /// /// If the value is negative, ignore the retries count. /// public int MaxRetries { get; set; } public override void OnInvoke(MethodInterceptionArgs args) { int retriesCount = 0; while (true) { try { base.OnInvoke(args); return; } catch (Exception e) { Console.WriteLine("Exception during attempt {0} of calling method {1}.{2}: {3}", retriesCount, args.Method.DeclaringType, args.Method.Name, e.Message); var handlerFunc = CurrentExceptionHandler.HandlerFunc; if (handlerFunc == null) throw; var response = handlerFunc(e); switch (response) { case ExceptionHandlerResult.Abort: throw; case ExceptionHandlerResult.Retry: retriesCount++; if (MaxRetries >= 0 && retriesCount > MaxRetries) throw; continue; case ExceptionHandlerResult.Ignore: return; } } } } } Пример использования: [HandleException(MaxRetries = 10)] private static void TestMethod(int n) { throw new ApplicationException(); } TestMethod(1); Чтобы использовать свой обработчик исключений, необходимо установить его в качестве значения статического свойства CurrentExceptionHandler.HandlerFunc.
Комментариев нет:
Отправить комментарий