Как стать автором
Обновить

Комментарии 10

если мы как вы пишите "не блокируем" запускаем Task и возвращаем Task.CompletedTask . Как программа понимает что её не нужно завершаться ?

Какой то внутренний механизм слежки за пустотой TaskFactory пулом?

Сам IHost при запуске IHostedService делает awaitи будет ждать завершения метода StartAsync. То есть, если внутри сервиса заэвейтит задачу await DoSomeWorkEveryFiveSecondsAsync(cancellationToken);, то IHost будет бесконечно ждать старта сервиса и никогда не запустит остальные.

Ну, или запустит, если внутри StartAsync завершится через какое-то время, например, прогрев кэши или завершив другое нужное нам перед началом обработки запросов действие.

Речь как раз о том, что если нам не нужно блокировать дальнейший запуск хоста, то внутри StartAsync достаточно запустить задачу без ожидания и следить за переданным CancellationToken . В этом случае await StartAsync получит Task.CompletedTask и продолжит дальнейшую инициализацию.

Как-то это всё очень странно.


И вы, и официальная документация рекомендуют fire and forget по типу #1, т.е. с игнорированием исключений. Причём если исключение выброшено из синхронной части вызываемого метода, оно будет передано наверх, а если микросекундой позже — проигнорировано.


Мне кажется, это применимо (?) только если из StartAsync вызывается именно ExecuteAsync, как в документации, а не что-то другое.

Да, async/await работает именно так. Вижу здесь 2 возможных разреза вопроса. Но для начала хочу показать код запуска HosterService внутри класса Host:

foreach (IHostedService hostedService in _hostedServices) 
{   
  // Fire IHostedService.Start    
  await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);       

  if (hostedService is BackgroundService backgroundService)
    _ = TryExecuteBackgroundServiceAsync(backgroundService);
}


1. Выполняет ли HostedService разовую работу при инициализации приложения или же он запускает периодическое выполнение задачи как в примере из статьи.
1.1. Если выполняет разовую работу при инициализации, то важным для await будет вопрос о том можно ли начать обработку запросов ДО завершения работы HostedService.
1.1.1. Если нет, то мы должны ожидать асинхронных вызовов внутри StartAsyncчтобы сработал внешний await в Host. В этом случае await hostedService.StartAsync получит задачу, которую будет ожидать.
1.1.2. Если да, то чтобы хост смог запуститься понадобится не ожидать асинхронные вызовы внутри hostedService.StartAsync, и отдать из метода Task.CompletedTask чтобы выполнение старта хоста продолжилось.
1.2. Если HostedService запускает периодическое выполнение задачи, то мы не можем ждать внутри StartAsync, иначе хост просто никогда не начнет обрабатывать вызовы потому что будет ждать завершения StartAsync.

Но на самом деле сценарии 1.2 или 1.1.2 означают, что нам вообще не нужно использовать свою реализацию IHostedService, а достаточно будет отнаследоваться от BackgroundService.

2. Может ли приложение продолжить работать при ошибке в HostedService.
2.1. Если да (например, HostedService подчищал старые кэши, отправлял дополнительную необязательную телеметрию), то какой-то особенной обработки исключений нам не нужно.
2.2. Если нет, то для gracefully shutdown стоит использовать упомянутый в статье IHostApplicationLifetime.

Но на самом деле в .net 6 хост будет останавлён при возникновении исключения в ExecuteAsync. Именно за это отвечает метод TryExecuteBackgroundServiceAsync, который был в начале комментария. Вот статья об этом изменении: Breaking changes / Unhandled exceptions from a BackgroundService. Чтобы переопределить это поведение и не останавливать хост в случае исключений в BackgroundService нужно сконфигурировать хост для игнорирования таких исключений:

Host.CreateBuilder(args)
    .ConfigureServices(services =>
    {
        services.Configure<HostOptions>(hostOptions =>
        {
            hostOptions.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore;
        });
    });

А вы не перепутали с IHostLifetime? Это вот как раз он будет ждать, а IHostedService ждать не будет.

У CancellationToken в StopAsync есть 5 секунд для корректного завершения

Важное уточнение, что это время задается HostOptions.ShutdownTimeout и задается для всего хоста целиком я не для каждого сервиса.

Метод ExecuteAsync в некотором роде нарушает контракт.
При получении токена отмены он говорит "я завершился" а не OperationCanceledException.
Я бы предложил следующее видение кода:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
  await WaitForAppStartup(lifetime, stoppingToken);
  ....
}

static async Task WaitForAppStartup(IHostApplicationLifetime lifetime, CancellationToken stoppingToken)
{
  var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
  using var reg1 = lifetime.ApplicationStarted.Register(() => tcs.TrySetResult());
  using var reg2 = stoppingToken.Register(() => tcs.TrySetCanceled());
  await tcs.Task;
}

Если речь о том, что на самом деле таймаут общий и StopAsync последовательный, поэтому когда 1 HostedService потратил на остановку 4 секунды, то второму осталась всего одна, то да, тут я поправлю статью, это тонкий и важный момент.

Что касается второй части — не совсем вижу тут проблему. Если в моем коде приложение стартовало и сработал lifetime.ApplicationStarted.Register, то проблемы нет. Если приложение не стартовало и сработал stoppingToken.Register, то в моем коде всё равно вернется корректный результат WaitForAppStartup без ошибок, а в вашем внутри ExecuteAsync будет исключение System.Threading.Tasks.TaskCanceledException: A task was canceled. — почему этот сценарий лучше?

Сугубо личное мнение по второй части в порядке уменьшения важности:

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

  • Используется только один TaskCompletionSource (Экономия на спичках)

  • Код получается более лаконичным.

Посмотрел код

Запуск когда не держим таск:

if (hostedService is BackgroundService backgroundService)

_ = TryExecuteBackgroundServiceAsync(backgroundService);

Инит:

IHost host = Host.CreateDefaultBuilder(args).ConfigureServices

services.AddHostedService<MyService>();

Внутри

await host.RunAsync();

try{  
await host.StartAsync(token).ConfigureAwait(false);  
await host.WaitForShutdownAsync(token).ConfigureAwait(false);}

Не увидел как этот console application не закрывается после окончания main?

Нету никакого массива тасков, никакого массива services , даже никакой проверки что "запущен хотябы один не системный task в приложении" потипу:

while (Task.Factory.RunCount > 0) {

thread.sleep(1)

}

await host.RunAsync();

внутри реализован как

await host.StartAsync(token).ConfigureAwait(false);
await host.WaitForShutdownAsync(token).ConfigureAwait(false);

Т.е. внутри него и происходит ожидание сигнала, что хост завершил свою работу.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации