Что такое 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()
, то получим:
Как видно ни один из таргетов не был выполнен. Единственное что мне не нравится - это не особо понятная ошибка, привыкнуть можно, но если кто-то из коллег посмотрит почему упал деплой, то может не сразу понять в чём проблема.
Метод 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 подхода и пример того, как можно расширить стандартный подход за счёт проверки сервисов. Так как это моя первая статья на хабре - не судите строго.
Буду рад комментариями и предложениям/критике по поводу изложенного выше материала.