IHost.RunAsync() никогда не возвращает

#c# #.net-core-3.0

#c# #.net-core-3.0

Вопрос:

Я создаю приложение .NET Core 3.1, которое будет запускаться BackgroundService в контейнере Docker. Хотя я реализовал задачи запуска и завершения работы для BackgroundService, и служба определенно завершается при запуске через SIGTERM , я обнаружил, что await host.RunAsync() вызов никогда не завершается — это означает, что оставшийся код в моем Main() блоке не выполняется.

Я что-то упускаю или я не должен ожидать RunAsync() , что вызов вернет управление после завершения остановки фоновой службы?

(Обновлено с помощью простейшего воспроизведения, которое я могу придумать …)

     using System;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;

    namespace BackgroundServiceTest
    {
        class Program
        {
            static async Task Main(string[] args)
            {
                Console.WriteLine("Main: starting");
                try
                {
                    using var host = CreateHostBuilder(args).Build();

                    Console.WriteLine("Main: Waiting for RunAsync to complete");

                    await host.RunAsync();

                    Console.WriteLine("Main: RunAsync has completed");
                }
                finally
                {
                    Console.WriteLine("Main: stopping");
                }
            }

            public static IHostBuilder CreateHostBuilder(string[] args) =>
                Host.CreateDefaultBuilder(args)
                    .UseConsoleLifetime()
                    .ConfigureServices((hostContext, services) =>
                    {
                        services.AddHostedService<Worker>();

                        // give the service 120 seconds to shut down gracefully before whacking it forcefully
                        services.Configure<HostOptions>(options => options.ShutdownTimeout = TimeSpan.FromSeconds(120));
                    });

        }

        class Worker : BackgroundService
        {
            protected override async Task ExecuteAsync(CancellationToken stoppingToken)
            {
                Console.WriteLine("Worker: ExecuteAsync called...");
                try
                {
                    while (!stoppingToken.IsCancellationRequested)
                    {
                        await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
                        Console.WriteLine("Worker: ExecuteAsync is still running...");
                    }
                }
                catch (OperationCanceledException) // will get thrown if TaskDelay() gets cancelled by stoppingToken
                {
                    Console.WriteLine("Worker: OperationCanceledException caught...");
                }
                finally
                {
                    Console.WriteLine("Worker: ExecuteAsync is terminating...");
                }
            }

            public override Task StartAsync(CancellationToken cancellationToken)
            {
                Console.WriteLine("Worker: StartAsync called...");
                return base.StartAsync(cancellationToken);
            }

            public override async Task StopAsync(CancellationToken cancellationToken)
            {
                Console.WriteLine("Worker: StopAsync called...");
                await base.StopAsync(cancellationToken);
            }

            public override void Dispose()
            {
                Console.WriteLine("Worker: Dispose called...");
                base.Dispose();
            }
        }
    }
  

Dockerfile:

     #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

    FROM mcr.microsoft.com/dotnet/core/runtime:3.1-buster-slim AS base
    WORKDIR /app

    FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
    WORKDIR /src
    COPY ["BackgroundServiceTest.csproj", "./"]
    RUN dotnet restore "BackgroundServiceTest.csproj"
    COPY . .
    WORKDIR "/src/"
    RUN dotnet build "BackgroundServiceTest.csproj" -c Release -o /app/build

    FROM build AS publish
    RUN dotnet publish "BackgroundServiceTest.csproj" -c Release -o /app/publish

    FROM base AS final
    WORKDIR /app
    COPY --from=publish /app/publish .
    ENTRYPOINT ["dotnet", "BackgroundServiceTest.dll"]
  

docker-compose.yml:

     version: '3.4'

    services:
      backgroundservicetest:
        image: ${DOCKER_REGISTRY-}backgroundservicetest
        build:
          context: .
          dockerfile: Dockerfile
  

Запустите это через docker-compose up --build , а затем во втором окне запустите docker stop -t 90 backgroundservicetest_backgroundservicetest_1

Вывод консоли показывает, что рабочий завершает работу и удаляется, но приложение (по-видимому) завершается до RunAsync() возврата.

     Successfully built 3aa605d4798f
    Successfully tagged backgroundservicetest:latest
    Recreating backgroundservicetest_backgroundservicetest_1 ... done
    Attaching to backgroundservicetest_backgroundservicetest_1
    backgroundservicetest_1  | Main: starting
    backgroundservicetest_1  | Main: Waiting for RunAsync to complete
    backgroundservicetest_1  | Worker: StartAsync called...
    backgroundservicetest_1  | Worker: ExecuteAsync called...
    backgroundservicetest_1  | info: Microsoft.Hosting.Lifetime[0]
    backgroundservicetest_1  |       Application started. Press Ctrl C to shut down.
    backgroundservicetest_1  | info: Microsoft.Hosting.Lifetime[0]
    backgroundservicetest_1  |       Hosting environment: Production
    backgroundservicetest_1  | info: Microsoft.Hosting.Lifetime[0]
    backgroundservicetest_1  |       Content root path: /app
    backgroundservicetest_1  | Worker: ExecuteAsync is still running...
    backgroundservicetest_1  | Worker: ExecuteAsync is still running...
    backgroundservicetest_1  | info: Microsoft.Hosting.Lifetime[0]
    backgroundservicetest_1  |       Application is shutting down...
    backgroundservicetest_1  | Worker: StopAsync called...
    backgroundservicetest_1  | Worker: OperationCanceledException caught...
    backgroundservicetest_1  | Worker: ExecuteAsync is terminating...
    backgroundservicetest_1  | Worker: Dispose called...
    backgroundservicetest_backgroundservicetest_1 exited with code 0
  

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

1. Можете ли вы протестировать локально, нажав CTRL C ? — если он не останавливается; скорее всего, один из ваших ресурсов не может остановиться. Это может произойти, если вы запустили поток переднего плана.

2. @Stefan Фоновая служба завершает свое завершение работы, как и ожидалось. Но оставшийся код Main() никогда не выполняется.

3. Не могли бы вы поделиться своим кодом для Worker ? Возможно, где-то он не обрабатывает отмену. просто ради эксперимента попробуйте прокомментировать это.

Ответ №1:

После длительного обсуждения на Github выясняется, что некоторый незначительный рефакторинг решает проблему. В двух словах, .RunAsync() блокируется до тех пор, пока хост не завершит и не удалит экземпляр хоста, который (по-видимому) завершает работу приложения.

Изменив код на call .StartAsync() , а затем await host.WaitForShutdownAsync() управление возвращается обратно Main() , как и ожидалось. Последний шаг — поместить хост в finally блок, как показано на рисунке:

 static async Task Main(string[] args)
{
    Console.WriteLine("Main: starting");
    IHost host = null;
    try
    {
        host = CreateHostBuilder(args).Build();

        Console.WriteLine("Main: Waiting for RunAsync to complete");
        await host.StartAsync();

        await host.WaitForShutdownAsync();

        Console.WriteLine("Main: RunAsync has completed");
    }
    finally
    {
        Console.WriteLine("Main: stopping");

        if (host is IAsyncDisposable d) await d.DisposeAsync();
    }
}
  

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

1. Спасибо за доработку

2. Не уверен, что что-то изменилось в .NET 7. Но это решение не работает.

3. Это работает на net7:

Ответ №2:

Вы должны использовать RunConsoleAsync вместо RunAsync. RunConsoleAsync Прослушивает только Ctrl C или SIGTERM :

RunConsoleAsync включает поддержку консоли, создает и запускает хост и ожидает завершения работы Ctrl C / SIGINT или SIGTERM.

Код должен измениться на :

 await CreateHostBuilder(args).RunConsoleAsync();
  

Это эквивалентно вызову UseConsoleLifeTime в сборщике хоста перед его сборкой:

 
var host=CreateHostBuilder(args).UseConsoleLifetime().Build();
...
await host.RunAsync();
  

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

1. Я уже вызываю .UseConsoleLifetime() конструктор хостов. Приложение отлично реагирует на SIGTERM — оно просто никогда не возвращается из .RunAsync()

2. @Mr .T где? … О, там, внизу. Вы пробовали вызывать UseConsoleLifeTime() прямо перед сборкой? Важна последовательность вызовов. Я не уверен, где находится исходный код, но если он попытается использовать, например, службы DI или контейнер host DI, и он будет заменен позже, это не сработает

3. @Mr .T что он и делает — этот исходный код был перемещен в другое хранилище, но он показывает, что UseConsoleLifeTime() регистрирует одноэлементную службу ConsoleLifeTime

4. Я изменил свой код на await CreateHostBuilder(args).RunConsoleAsync() — никаких изменений в поведении.