Страницы

Поиск по вопросам

четверг, 28 ноября 2019 г.

Abort\Retry\Ignore в любой точке кода для возможности вывалить выбор на пользователя

#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 EventHandler Handler;

    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 Task TrySeveralTimesWithGrowingTimeout( 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 Queue activities = 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 Func HandlerFunc { 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.

Комментариев нет:

Отправить комментарий