簡體   English   中英

在 .NET Core 2.1 中使用通用主機正常關閉

[英]Graceful shutdown with Generic Host in .NET Core 2.1

.NET Core 2.1 引入了新的通用主機,它允許托管非 HTTP 工作負載,並具有 Web 主機的所有優勢。 目前,沒有太多的信息和食譜,但我使用了以下文章作為起點:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-2.1

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1

https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/background-tasks-with-ihostedservice

我的 .NET Core 應用程序啟動,通過 RabbitMQ 消息代理偵聽新請求,並根據用戶請求(通常在控制台中按 Ctrl+C)關閉。 但是,關閉並不優雅 - 應用程序在將控制權返回給操作系統時仍有未完成的后台線程。 我通過控制台消息看到它 - 當我在控制台中按 Ctrl+C 時,我看到我的應用程序的幾行控制台輸出,然后是操作系統命令提示符,然后是我的應用程序的控制台輸出。

這是我的代碼:

程序.cs

public class Program
{
    public static async Task Main(string[] args)
    {
        var host = new HostBuilder()
            .ConfigureHostConfiguration(config =>
            {
                config.SetBasePath(AppContext.BaseDirectory);
                config.AddEnvironmentVariables(prefix: "ASPNETCORE_");
                config.AddJsonFile("hostsettings.json", optional: true);
            })
            .ConfigureAppConfiguration((context, config) =>
            {
                var env = context.HostingEnvironment;
                config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
                config.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
                if (env.IsProduction())
                    config.AddDockerSecrets();
                config.AddEnvironmentVariables();
            })
            .ConfigureServices((context, services) =>
            {
                services.AddLogging();
                services.AddHostedService<WorkerPoolHostedService>();
                // ... other services
            })
            .ConfigureLogging((context, logging) =>
            {
                if (context.HostingEnvironment.IsDevelopment())
                    logging.AddDebug();

                logging.AddSerilog(dispose: true);

                Log.Logger = new LoggerConfiguration()
                    .ReadFrom.Configuration(context.Configuration)
                    .CreateLogger();
            })
            .UseConsoleLifetime()
            .Build();

        await host.RunAsync();
    }
}

WorkerPoolHostedService.cs

internal class WorkerPoolHostedService : IHostedService
{
    private IList<VideoProcessingWorker> _workers;
    private CancellationTokenSource _stoppingCts = new CancellationTokenSource();

    protected WorkerPoolConfiguration WorkerPoolConfiguration { get; }
    protected RabbitMqConfiguration RabbitMqConfiguration { get; }
    protected IServiceProvider ServiceProvider { get; }
    protected ILogger<WorkerPoolHostedService> Logger { get; }

    public WorkerPoolHostedService(
        IConfiguration configuration,
        IServiceProvider serviceProvider,
        ILogger<WorkerPoolHostedService> logger)
    {
        this.WorkerPoolConfiguration = new WorkerPoolConfiguration(configuration);
        this.RabbitMqConfiguration = new RabbitMqConfiguration(configuration);
        this.ServiceProvider = serviceProvider;
        this.Logger = logger;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        var connectionFactory = new ConnectionFactory
        {
            AutomaticRecoveryEnabled = true,
            UserName = this.RabbitMqConfiguration.Username,
            Password = this.RabbitMqConfiguration.Password,
            HostName = this.RabbitMqConfiguration.Hostname,
            Port = this.RabbitMqConfiguration.Port,
            VirtualHost = this.RabbitMqConfiguration.VirtualHost
        };

        _workers = Enumerable.Range(0, this.WorkerPoolConfiguration.WorkerCount)
            .Select(i => new VideoProcessingWorker(
                connectionFactory: connectionFactory,
                serviceScopeFactory: this.ServiceProvider.GetRequiredService<IServiceScopeFactory>(),
                logger: this.ServiceProvider.GetRequiredService<ILogger<VideoProcessingWorker>>(),
                cancellationToken: _stoppingCts.Token))
            .ToList();

        this.Logger.LogInformation("Worker pool started with {0} workers.", this.WorkerPoolConfiguration.WorkerCount);
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        this.Logger.LogInformation("Stopping working pool...");

        try
        {
            _stoppingCts.Cancel();
            await Task.WhenAll(_workers.SelectMany(w => w.ActiveTasks).ToArray());
        }
        catch (AggregateException ae)
        {
            ae.Handle((Exception exc) =>
            {
                this.Logger.LogError(exc, "Error while cancelling workers");
                return true;
            });
        }
        finally
        {
            if (_workers != null)
            {
                foreach (var worker in _workers)
                    worker.Dispose();
                _workers = null;
            }
        }
    }
}

VideoProcessingWorker.cs

internal class VideoProcessingWorker : IDisposable
{
    private readonly Guid _id = Guid.NewGuid();
    private bool _disposed = false;

    protected IConnection Connection { get; }
    protected IModel Channel { get; }
    protected IServiceScopeFactory ServiceScopeFactory { get; }
    protected ILogger<VideoProcessingWorker> Logger { get; }
    protected CancellationToken CancellationToken { get; }

    public VideoProcessingWorker(
        IConnectionFactory connectionFactory,
        IServiceScopeFactory serviceScopeFactory,
        ILogger<VideoProcessingWorker> logger,
        CancellationToken cancellationToken)
    {
        this.Connection = connectionFactory.CreateConnection();
        this.Channel = this.Connection.CreateModel();
        this.Channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);
        this.ServiceScopeFactory = serviceScopeFactory;
        this.Logger = logger;
        this.CancellationToken = cancellationToken;

        #region [ Declare ]

        // ...

        #endregion

        #region [ Consume ]

        // ...

        #endregion
    }

    // ... worker logic ...

    public void Dispose()
    {
        if (!_disposed)
        {
            this.Channel.Close(200, "Goodbye");
            this.Channel.Dispose();
            this.Connection.Close();
            this.Connection.Dispose();
            this.Logger.LogDebug("Worker {0}: disposed.", _id);
        }
        _disposed = true;
    }
}

因此,當我按 Ctrl+CI 時,在控制台中看到以下輸出(當沒有請求處理時):

正在停止工作池...
命令提示符
工人ID :已處理。

如何優雅地關機?

你需要IApplicationLifetime 這為您提供了有關應用程序啟動和關閉的所有所需信息。 您甚至可以通過appLifetime.StopApplication();觸發關閉appLifetime.StopApplication();

查看https://github.com/aspnet/Docs/blob/66916c2ed3874ed9b000dfd1cab53ef68e84a0f7/aspnetcore/fundamentals/host/generic-host/samples/2.x/GenericHostSample/LifetimeEventsHostedService.cs

片段(如果鏈接無效):

public Task StartAsync(CancellationToken cancellationToken)
{
    appLifetime.ApplicationStarted.Register(OnStarted);
    appLifetime.ApplicationStopping.Register(OnStopping);
    appLifetime.ApplicationStopped.Register(OnStopped);

    return Task.CompletedTask;
}

我將分享一些我認為非常適用於非 WebHost 項目的模式。

namespace MyNamespace
{
    public class MyService : BackgroundService
    {
        private readonly IServiceProvider _serviceProvider;
        private readonly IApplicationLifetime _appLifetime;

        public MyService(
            IServiceProvider serviceProvider,
            IApplicationLifetime appLifetime)
        {
            _serviceProvider = serviceProvider;
            _appLifetime = appLifetime;
        }

        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _appLifetime.ApplicationStopped.Register(OnStopped);

            return RunAsync(stoppingToken);
        }

        private async Task RunAsync(CancellationToken token)
        {
            while (!token.IsCancellationRequested)
            {
                using (var scope = _serviceProvider.CreateScope())
                {
                    var runner = scope.ServiceProvider.GetRequiredService<IMyJobRunner>();
                    await runner.RunAsync();
                }
            }
        }

        public void OnStopped()
        {
            Log.Information("Window will close automatically in 20 seconds.");
            Task.Delay(20000).GetAwaiter().GetResult();
        }
    }
}

關於這個類的一些注意事項:

  1. 我使用 BackgroundService 抽象類來表示我的服務。 它在 Microsoft.Extensions.Hosting.Abstractions 包中可用。 我相信這計划在 .NET Core 3.0 中開箱即用。
  2. ExecuteAsync 方法需要返回一個代表正在運行的服務的 Task。 注意:如果您有一個同步服務,請在 Task.Run() 中包裝您的“Run”方法。
  3. 如果你想為你的服務做額外的設置或拆卸,你可以注入應用程序生命周期服務並掛鈎事件。 我添加了一個要在服務完全停止后觸發的事件。
  4. 因為您沒有像在 MVC 項目中那樣為每個 Web 請求創建新范圍的自動魔法,所以您必須為范圍服務創建自己的范圍。 將 IServiceProvider 注入服務來做到這一點。 應使用 AddScoped() 將作用域上的所有依賴項添加到 DI 容器中。

在 Main( string[] args ) 中設置主機,以便在調用 CTRL+C / SIGTERM 時正常關閉:

IHost host = new HostBuilder()
    .ConfigureServices( ( hostContext, services ) =>
    {
        services.AddHostedService<MyService>();
    })
    .UseConsoleLifetime()
    .Build();

host.Run();  // use RunAsync() if you have access to async Main()

我發現這組模式在 ASP.NET 應用程序之外工作得很好。

請注意,Microsoft 已針對 .NET Standard 進行構建,因此您無需使用 .NET Core 即可利用這些新的便利。 如果您在 Framework 中工作,只需添加相關的 NuGet 包。 該包是針對 .NET Standard 2.0 構建的,因此您需要使用 Framework 4.6.1 或更高版本。 您可以在此處找到所有基礎設施的代碼,並隨意瀏覽您正在使用的所有抽象的實現: https : //github.com/aspnet/Extensions

對於.NET Core 3.0,您可以注入並使用IHostApplicationLifetime

private readonly IHostApplicationLifetime _applicationLifetime;

public async Task StartAsync(CancellationToken cancellationToken)
{
    await DoWork();
    _applicationLifetime.StopApplication();
}

public Task StopAsync(CancellationToken cancellationToken)
{
    Console.WriteLine("StopAsync will be called after call to StopApplication");
    return Task.CompletedTask;
}

Startup.cs ,您可以使用當前進程的Kill()方法終止應用程序:

        public void Configure(IHostApplicationLifetime appLifetime)
        {
            appLifetime.ApplicationStarted.Register(() =>
            {
                Console.WriteLine("Press Ctrl+C to shut down.");
            });

            appLifetime.ApplicationStopped.Register(() =>
            {
                Console.WriteLine("Shutting down...");
                System.Diagnostics.Process.GetCurrentProcess().Kill();
            });
        }

程序.cs

在構建主機時不要忘記使用UseConsoleLifetime()

Host.CreateDefaultBuilder(args).UseConsoleLifetime(opts => opts.SuppressStatusMessages = true);

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM