#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()
регистрирует одноэлементную службу ConsoleLifeTime4. Я изменил свой код на
await CreateHostBuilder(args).RunConsoleAsync()
— никаких изменений в поведении.