• Что такое fail-fast design?

  • Что уже есть в Nuke?

  • Можно ли расширить fail-fast инструменты?

  • Заключение

Вступление

Раз уж вы зашли сюда, то надеюсь, что вы уже работали с Nuke или хотя бы слышали о нём. В двух словах - это система автоматизации сборок в виде консольного .Net Core приложения, так что пишем всё на С# и радуемся жизни.

Кто не знаком с Nuke вы всегда можете ознакомиться или на официальном сайте или посмотреть вот эту презентацию.

Далее в статье мы поговорим о существующем в Nuke fail-fast подходе и о том, как его можно развивать.

Что такое fail-fast design?

На официальном сайте указано, что Nuke следует fail-fast философии. Раз уж это является основной темой статьи, то логично было бы разобраться что это такое.
Википедия на этот счёт говорит следующее:

Длинная и скучная цитата из Википедии

In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system's state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.

В моем примитивном понимании это то, что мы, как программисты, делаем каждый день. А именно вызываем исключение максимально близко к месту появления проблемы, то есть как только поняли что что-то идёт не по плану.

В контексте систем автоматизации сборок получается посмотреть на fail-fast design немного под другим углом. Всё дело в том, что любая система автоматизации сборок это по сути набор шагов, каждый шаг изолированно выполняет свою часть работы и имеется некая взаимосвязь между ними. Допустим у нас есть Nuke проект для деплоя некого абстрактного веб-приложения, тогда его план будет выглядеть примерно так:

Пример очередности выполнения шагов
Пример очередности выполнения шагов

Что будет если что-то пойдёт не так на шаге Deploy? Логично что деплой упадёт и наш код не будет доставлен на нужное окружение. Но кроме этого стоит учесть что уже были выполнены шаги Build, Migration, StopIisPool. А значит кроме этого у нас ещё и выключен пул, а значит приложение не работает, хотя могло бы.

Читатель может справедливо заметить, что это проблемы пайплайна и это нужно было бы учесть и например, сделать включение пула обязательным действием вне зависимости от всего остального, но fail-fast design подталкивает нас к мысли "Зачем запускать то, что всё равно упадёт?".

Неуместная шутка

Лучше не работать с мыслью "Зачем запускать то, что всё равно упадёт?", потому что тогда можно вообще перестать писать код)

Что уже есть в Nuke?

Из коробки в Nuke доступна проверка параметров перед запуском. На всякий случай напомню что Параметр в Nuke это переменная значение которой может передаваться через аргументы командной строки или как переменная окружения.

Логика следующая - если для выполнения некого шага (в Nuke они именуются Target) обязательно нужен параметр переданный извне, то без него будет ошибка. И проверить его наличие мы можем на самом старте приложения при этом не запуская ни один из шагов.

Реализовывается такая проверка следующим образом:

private Target Deploy => _ => _
    .Requires(() => IisPoolName!= null)
    .Requires(() => IisPoolName) // упрощенная проверка на null
    .DependsOn(StopIisPool)
    .Executes(() => { });
Расшифровка кода выше

Target в Nuke это делегат из-за этого и получается такая сомнительная конструкция в виде смайлика => _ =>

.DependsOn(StopIisPool) - указывает что данный таргет зависит от таргета StopIisPool, а значит перед его выполнением должен сначала выполнится StopIisPool.

.Executes() - это сама суть таргета, тот код который непосредственно в нём выполняется.

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

Возможно у кого-то возникнет вопрос, почему параметр передаётся через лямбду? Всё дело в том, что у Nuke своеобразный жизненный цикл, а точнее создание экземпляра класса и инициализация параметров происходят не одновременно, а значит если передать просто параметр, то в нём будет не проинициализированное значение (значение по умолчанию). Передача же лямбды позволяет получить значение параметра в момент выполнения метода Requires().

Если мы теперь запустим Nuke с добавленным методом Requires(), то получим:

Результат выполнения Nuke если не был передан обязательный параметр
Результат выполнения Nuke если не был передан обязательный параметр

Как видно ни один из таргетов не был выполнен. Единственное что мне не нравится - это не особо понятная ошибка, привыкнуть можно, но если кто-то из коллег посмотрит почему упал деплой, то может не сразу понять в чём проблема.

Метод Requires() принимает Func<bool>, а значит можно проверять не только параметры, а всё на что хватит фантазии.

В целом из fail-fast в Nuke это всё, поэтому перейдём к моим размышлениям на эту тему.

Можно ли расширить fail-fast инструменты?

В моей команде мы используем собственную библиотеку на основе Nuke для автоматизации сборок, собственную потому что есть много нюансов и тонкостей  которые необходимо часто переиспользовать. У нас довольно много проектов и различных сценариев в которых используется Nuke поэтому базовая библиотека именно то, что нужно.

В нашей библиотеке довольно много зависимостей поэтому мы используем dependency injection внутри Nuke на основе IServiceCollection. Поэтому мне хотелось бы проверять перед запуском не только параметры, но и классы-сервисы в которым делегируется определенная работа.

Такие сервисы, которые выполняют свою работу в рамках Nuke я разделил на 2 группы и получились следующие интерфейсы:

  • IExternalService - интерфейс для классов которые в процессе свой работы делают запросы на внешние сервисы, например использование некого API

код IExternalService
  public interface IExternalService : INukeService
  {
    public string BaseUrl { get; }
  }
  • ICustomCheckedService - интерфейс для классов, которые подразумевают возможность проверки их работоспособности каким-то уникальным для этого класса способом.

код ICustomCheckedService
  public interface ICustomCheckedService : INukeService
  {
    bool CheckService();
  }

Использование в Nuke с помощью метода расширения выглядит таким образом:

private Target Pack => _ => _
    .Requires(() => ApiKey)
    .RequiresService<SomeService>() // Проверка сервиса на старте
    .DependsOn(Compile)
    .Executes(() => { Get<SomeService>().SomeMethod() });

Метод .RequiresService<T>() устроен следующим образом:

public static ITargetDefinition RequiresService<T>(this ITargetDefinition target) where T : class, INukeService
{
  var nuke = DependencyInjection.Get<NukeBase>();

  if (!nuke.WithoutServiceCheck) //Резервная возможность отключать проверки сервисов
  {
    DependencyInjection
      .Get<DependencyExtractor>()
      .Extract<T>(AlreadyCheckedTypes) //Метод расширения позволяющий достать из DI контейнера экземпляр класса Т и его зависимости
      .ForEach(type => CheckService(target, type));
  }

  return target;
}

В коде выше по сути есть 2 этапа:

  • Находим все зависимости для класса T

  • Для каждого класса выполняем метод CheckService()

На всякий случай замечу, что AlreadyCheckedTypes передаётся для того, чтобы не проверять много раз один и тот же сервис.

Метод который проверяет сервисы тоже достаточно прост:

private static void CheckService(ITargetDefinition target, Type serviceType)
{
  if (!AlreadyCheckedTypes.Contains(serviceType)) //Если уже проверяли этот сервис, то ничего не делаем
  {
    var service = DependencyInjection.Get(serviceType); //Достаем экземпляр класса из DI контейнера

    if (service is IExternalService externalService) //Если это IExternalService, то пингуем внешний url
      target.Requires(() => PingMethod(externalService.BaseUrl));

    if (service is ICustomCheckedService customCheckedService) //Если у сервиса кастомная логика проверки работоспособности - заупскаем её
      target.Requires(() => InternalCheckService(customCheckedService));
     
    AlreadyCheckedTypes.Add(serviceType);
  }
}

Далее уже при создании каждого сервиса реализовываем логику проверки, если это конечно же необходимо. Например для сервиса, который работает с файловой системой проверка может выглядеть как-то так:

    public bool CheckService()
    {
      try
      {
        var directories = new DirectoryInfo(Environment.CurrentDirectory);
        directories.GetFiles();
        return true;
      }
      catch (Exception e)
      {
        Logger.Error(e);
        return false;
      }
    }

Заключение

Таким образом мы рассмотрели реализацию в Nuke fail-fast подхода и пример того, как можно расширить стандартный подход за счёт проверки сервисов. Так как это моя первая статья на хабре - не судите строго.

Буду рад комментариями и предложениям/критике по поводу изложенного выше материала.

Комментарии (0)