【问题标题】:IHost.RunAsync() Never ReturnsIHost.RunAsync() 永不返回
【发布时间】:2020-10-20 20:57:26
【问题描述】:

我正在构建一个 .NET Core 3.1 应用程序,它将在 Docker 容器中运行 BackgroundService。虽然我已经为 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

控制台输出显示 Worker 已关闭并被释放,但应用程序(显然)在 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

【问题讨论】:

  • 你可以按CTRL+C 测试本地化吗? --- 如果它没有停止;很可能您的资源之一无法停止。如果您启动了前台线程,就会发生这种情况。
  • @Stefan BackgroundService 按预期完成关闭。但是Main() 中的剩余代码永远不会执行。
  • 您能分享您的Worker 代码吗?也许在某个地方它不处理取消。只是为了实验,请尝试将其注释掉。

标签: c# .net-core-3.0


【解决方案1】:

lengthy discussion on Github 之后,事实证明,一些小的重构解决了这个问题。简而言之,.RunAsync() 一直阻塞,直到主机完成并释放主机实例,这(显然)终止了应用程序。

通过将代码更改为调用.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();
    }
}

【讨论】:

  • 感谢您的跟进
【解决方案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();

【讨论】:

  • 我已经在主机生成器上调用.UseConsoleLifetime()。应用程序对 SIGTERM 的响应很好 - 它永远不会从 .RunAsync() 返回
  • @Mr.T 在哪里? ……哦,在下面。您是否尝试过在构建之前调用UseConsoleLifeTime()?调用顺序很重要。我不确定源代码在哪里,但是如果它尝试使用例如 DI 服务或主机 DI 容器并且稍后被替换,它将无法工作
  • @Mr.T 它所做的 - this source code 已移至不同的存储库,但它表明 UseConsoleLifeTime() 注册了一个单例 ConsoleLifeTime 服务
  • 我将代码更改为 await CreateHostBuilder(args).RunConsoleAsync() - 行为没有变化。
猜你喜欢
  • 2018-01-09
  • 2020-02-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-15
  • 2018-01-11
  • 2011-07-25
  • 1970-01-01
相关资源
最近更新 更多