web-dev-qa-db-ja.com

.NET CoreのHostingEnvironment.QueueBackgroundWorkItemの代替ソリューション

私たちは.NET Core Web Apiと連携しており、さまざまな強度のリクエストをデータベースに記録する軽量なソリューションを探していますが、クライアントが保存プロセスを待つことを望んでいません。
残念ながら、dnxにはHostingEnvironment.QueueBackgroundWorkItem(..)が実装されておらず、Task.Run(..)は安全ではありません。
エレガントなソリューションはありますか?

38

QueueBackgroundWorkItemはなくなりましたが、前のバージョンで使用されているIApplicationLifetimeの代わりにIRegisteredObjectがあります。そして、そのようなシナリオには非常に有望だと思います。

アイデアは(それがかなり悪いものであるかどうか、まだよくわかりません。したがって、注意してください!)シングルトンを登録して、を生成し、新しいタスクを観察することです。そのシングルトン内で、まだ実行中のタスクを適切に待機するために、「停止イベント」をさらに登録できます。

この「概念」は、ロギング、メール送信などの短時間の実行に使用できます。それほど時間はかかりませんが、現在のリクエストでは不必要な遅延が発生します。

public class BackgroundPool
{
    protected ILogger<BackgroundPool> Logger { get; }

    public BackgroundPool(ILogger<BackgroundPool> logger, IApplicationLifetime lifetime)
    {
        if (logger == null)
            throw new ArgumentNullException(nameof(logger));
        if (lifetime == null)
            throw new ArgumentNullException(nameof(lifetime));

        lifetime.ApplicationStopped.Register(() =>
        {
            lock (currentTasksLock)
            {
                Task.WaitAll(currentTasks.ToArray());
            }

            logger.LogInformation(BackgroundEvents.Close, "Background pool closed.");
        });

        Logger = logger;
    }

    private readonly object currentTasksLock = new object();

    private readonly List<Task> currentTasks = new List<Task>();

    public void SendStuff(Stuff whatever)
    {
        var task = Task.Run(async () =>
        {
            Logger.LogInformation(BackgroundEvents.Send, "Sending stuff...");

            try
            {
                // do THE stuff

                Logger.LogInformation(BackgroundEvents.SendDone, "Send stuff returns.");
            }
            catch (Exception ex)
            {
                Logger.LogError(BackgroundEvents.SendFail, ex, "Send stuff failed.");
            }
        });

        lock (currentTasksLock)
        {
            currentTasks.Add(task);

            currentTasks.RemoveAll(t => t.IsCompleted);
        }
    }
}

そのようなBackgroundPoolはシングルトンとして登録される必要があり、DIを介して他のコンポーネントで使用できます。現在、メールの送信に使用していますが、正常に機能します(アプリのシャットダウン時にテスト済みのメールを送信することもできます)。

注:バックグラウンドタスク内の現在のHttpContextのようなものにアクセスすることはできません。 古いソリューション は、UnsafeQueueUserWorkItemを使用して、とにかくそれを禁止します。

どう思いますか?

更新:

ASP.NET Core 2.0には、バックグラウンドタスク用の新しいものがあり、ASP.NET Core 2.1ではより良くなります。 IHostedServiceおよびBackgroundServiceクラスを使用した.NET Core 2.x webappsまたはマイクロサービスでのバックグラウンドタスクの実装

11
Axel Heer

@axelheerが述べたように、 IHostedService は.NET Core 2.0以降で使用する方法です。

HostingEnvironment.QueueBackgroundWorkItemの代わりにASP.NET Coreのような軽量のものが必要だったため、 DalSoft.Hosting.BackgroundQueue を使用しました。これは.NET Core 2.0を使用します IHostedService

PM>インストールパッケージDalSoft.Hosting.BackgroundQueue

ASP.NET Core Startup.csで:

public void ConfigureServices(IServiceCollection services)
{
   services.AddBackgroundQueue(onException:exception =>
   {

   });
}

バックグラウンドタスクをキューに入れるには、BackgroundQueueをコントローラーのコンストラクターに追加して、Enqueueを呼び出します。

public EmailController(BackgroundQueue backgroundQueue)
{
   _backgroundQueue = backgroundQueue;
}

[HttpPost, Route("/")]
public IActionResult SendEmail([FromBody]emailRequest)
{
   _backgroundQueue.Enqueue(async cancellationToken =>
   {
      await _smtp.SendMailAsync(emailRequest.From, emailRequest.To, request.Body);
   });

   return Ok();
}
11
DalSoft

.NET Coreのバックグラウンドジョブには、Hangfire( http://hangfire.io/ )を使用できます。

例えば ​​:

var jobId = BackgroundJob.Enqueue(
    () => Console.WriteLine("Fire-and-forget!"));
8
ycrumeyrolle

Axelの答え の微調整バージョンを以下に示します。これにより、デリゲートを渡し、完了したタスクをより積極的にクリーンアップできます。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;

namespace Example
{
    public class BackgroundPool
    {
        private readonly ILogger<BackgroundPool> _logger;
        private readonly IApplicationLifetime _lifetime;
        private readonly object _currentTasksLock = new object();
        private readonly List<Task> _currentTasks = new List<Task>();

        public BackgroundPool(ILogger<BackgroundPool> logger, IApplicationLifetime lifetime)
        {
            if (logger == null)
                throw new ArgumentNullException(nameof(logger));
            if (lifetime == null)
                throw new ArgumentNullException(nameof(lifetime));

            _logger = logger;
            _lifetime = lifetime;

            _lifetime.ApplicationStopped.Register(() =>
            {
                lock (_currentTasksLock)
                {
                    Task.WaitAll(_currentTasks.ToArray());
                }

                _logger.LogInformation("Background pool closed.");
            });
        }

        public void QueueBackgroundWork(Action action)
        {
#pragma warning disable 1998
            async Task Wrapper() => action();
#pragma warning restore 1998

            QueueBackgroundWork(Wrapper);
        }

        public void QueueBackgroundWork(Func<Task> func)
        {
            var task = Task.Run(async () =>
            {
                _logger.LogTrace("Queuing background work.");

                try
                {
                    await func();

                    _logger.LogTrace("Background work returns.");
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex.HResult, ex, "Background work failed.");
                }
            }, _lifetime.ApplicationStopped);

            lock (_currentTasksLock)
            {
                _currentTasks.Add(task);
            }

            task.ContinueWith(CleanupOnComplete, _lifetime.ApplicationStopping);
        }

        private void CleanupOnComplete(Task oldTask)
        {
            lock (_currentTasksLock)
            {
                _currentTasks.Remove(oldTask);
            }
        }
    }
}
5

オリジナル HostingEnvironment.QueueBackgroundWorkItemはワンライナーであり、非常に便利に使用できました。 ASP Core 2.xでこれを行う「新しい」方法では、不可解なドキュメントのページを読んで、かなりの量のコードを書く必要があります。

これを回避するには、次の代替方法を使用できます

    public static ConcurrentBag<Boolean> bs = new ConcurrentBag<Boolean>();

    [HttpPost("/save")]
    public async Task<IActionResult> SaveAsync(dynamic postData)
    {

    var id = (String)postData.id;

    Task.Run(() =>
                {
                    bs.Add(Create(id));
                });

     return new OkResult();

    }


    private Boolean Create(String id)
    {
      /// do work
      return true;
    }

静的ConcurrentBag<Boolean> bsは、オブジェクトへの参照を保持します。これにより、コントローラーが戻った後にガベージコレクターがタスクを収集できなくなります。

0
user11658885