ASP.NETでバックグラウンドジョブを実行してみる


# はじめに

Web Page や Web API からのファイル取込やまとまったメール送信がそこそこの量になってくると、
こんなに長い時間レスポンスを待つのつらい:cry: みたいなケースが出てくるかと思います。
そもそもジョブが終わるまで待たなくていいようなものであれば、
さっさとレスポンスを返して、後はよしなにやっておいて欲しいわけです :smile: 
そんなときに役立ちそうなバックグラウンドジョブの実行方法を紹介します。

# QueueBackgroundWorkItemとHangfire

他にもありますが、この２つについて調べてみました。

## :a: [QueueBackgroundWorkItem](https://msdn.microsoft.com/ja-jp/library/dn636892(v=vs.110).aspx)

公式のAPIです。`ThreadPool`を使用する`Task.Run`で実行するよりはマシですが、
ジョブが完走することは保証されていないので注意が必要です。
`CancellationToken`を参照して中断処理を記述できますが、それも完走するとは限りません。
（制限時間を超えたらアウト）

```csharp
HostingEnvironment.QueueBackgroundWorkItem(cancellationToken =>
{
    // something 
});

```
以下は [.NET Web Development and Tools Blog](http://blogs.msdn.com/b/webdev/archive/2014/06/04/queuebackgroundworkitem-to-reliably-schedule-and-run-long-background-process-in-asp-net.aspx) からの抜粋です。

### 概要
- .NET Framework 4.5.2 から利用可能。
- このAPIで登録されたジョブは、ASP.NET Runtimeはそのジョブが実行終了するまでAppDomainのシャットダウンを延期しようとします。
  - 通常のThreadPoolで実行されるジョブはこのような動作はしません。
  - **実行終了を保証するものではありません。**
  - `CancellationToken`を参照して途中終了時の処理を記述することができます。

### 制限
  - QBWI APIは、managed AppDomainの外からは呼びだすことはできません。
  - AppDomainシャットダウンは90秒（※1）延期できるだけです。
    - （※1）HttpRuntimeSection.ShutdownTimeoutとprocessModel shutdownTimeLimitの最小値
    - **この延期時間を超えたら実行中であってもジョブは強制終了されます。**
  - バックグラウンドジョブに呼び出し元のExecutionContextは引き継がれません。
    - `HttpContext`の情報を使用したければ値をコピーして渡してください。
      - スレッドセーフではないので`HttpContext`のインスタンス自体を渡さないこと。
  - アプリケーションプールのシャットダウンが始まったら、スケジューリングされたジョブが実行される保証はありません。
  - アプリケーションがシャットダウンされるとき、`CancellationToken`にシグナルが伝えられます。
      - ジョブはこのTokenを参照し（終了処理などを実行する）努力をするべきです。
      - もしジョブがこのTokenを尊重せずに実行し続ければ、ASP.NET Runtimeは、ジョブの終了をまたずにAppDomainをアンロードします。
  - 私達はバックグラウンドジョブが完璧に動くということを保証しません。
      - 私達はバックグラウンドジョブが不正な処理をしていると判断したらkillするし、`w3wp.exe`プロセスがクラッシュしたらすべてのタスクが死にます。
      - **信頼性が必要であればAzureのスケジューリング機能を使うべきです。**

## :b: [Hangfire](http://hangfire.io/) 
高機能なOSSのライブラリです。
単純にQueueを入れてバックグラウンドジョブを実行するだけでなく、時間差や定期実行も可能です。
ただ、そんなことより重要なのは、**Queue管理がセットになっていることやジョブの実行プロセスが完全に分離できること**です。

### 概要

> ![](https://gyazo.com/a26883c3929db444a6083b23f2ad4d37/raw)
> from http://docs.hangfire.io/en/latest/index.html

- .NET Framework 4.5 以上が必要。
- **Client , Job Storage, Sever の３つで構成されている。**
    - Client
        - Queueを登録する
    - Job Storage
        - ここでQueueデータが管理される
        - RedisやSQL Serverが必要
    - Server
        - ここでバックグラウンドジョブが実行される
        - [Webアプリ/コンソールアプリ/Windowsサービスから選択可能。](http://docs.hangfire.io/en/latest/background-processing/index.html)
            - ASP.NETから完全に分離できるのでより信頼性の高いジョブ実行が可能になる
        - スケーラビリティが高い（負荷に応じてこのサーバーを増減させればいい）
- ダッシュボードから、ジョブの状態の参照や削除・再実行などができる

## 試してみた
ASP.NET Web API (Client) + SQL Server (Job Storage) + Console Application (Server) のパターンを試してみました。

- 準備
    1. `Install-Package Hangfire`
        2. Client
        3. Server
    1. DBファイルを作成
        2. app.configとweb.configにConnectionStringを追加
    2. 初期設定(Config)
    3. SampleJobを作って、Web APIが呼び出されたときにQueueを登録ようにしておく

#### Config (ver 1.5.3)
```csharp
// Client
namespace WebApplication1
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            GlobalConfiguration.Configuration.UseSqlServerStorage("DefaultConnection2");
            app.UseHangfireDashboard();
        }
    }
}

// Server
namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            GlobalConfiguration.Configuration.UseSqlServerStorage("DefaultConnection2");
            using (var server = new BackgroundJobServer())
            {
                Console.WriteLine("Hangfire Server started. Press any key to exit...");
                Console.ReadKey();
            }
        }
    }
}
```

####SampleJob

```csharp
namespace ClassLibrary1
{
    public class Class1
    {
        public void SampleJob1(int id)
        {
            Console.WriteLine("fire and forget!!!!! ");
            System.Threading.Thread.Sleep(1000 * id);
        }
    }
}

```

####APIが呼び出されたらEnqueue

```csharp
namespace WebApplication1.Controllers
{
    public class TestController : ApiController
    {
        public void Post(int id)
        {
             BackgroundJob.Enqueue(() => new ClassLibrary1.Class1().SampleJob1(id));
        }
    }
}

```

準備はここまで。
Web API Application(Client) を起動してみます。
するとにDB(Job Storage)にこんなテーブルができます。
![](https://gyazo.com/53589e4fe1a4a6badbabb840d66da315/raw)

Console App(Server) も起動します。
![](https://gyazo.com/f96bf8da2ae2c7348758f2dea22701b7/raw)

ダッシューボードを開いてServersをみると、起動したConsole Appが反映されてますね。
![](https://qiita-image-store.s3.amazonaws.com/0/12981/17f0b5a7-1376-26c0-323c-9785dced91e0.jpeg)

Web APIを実行すると、Console App側でSampleJob1が実行されました！
![console app results](https://gyazo.com/47fdc7406d66413020f47ab146b9580b/raw)

ジョブの実行が終わると、ダッシュボードのProcessingからSucceededにJobが移動します。
![succeeded jobs](https://gyazo.com/8c0ad5d9c31ed4566fca830db7a324ea/raw)

Jobを選ぶとJobのパラメーターや実行時間、処理したサーバープロセスが参照できます。
![details](https://qiita-image-store.s3.amazonaws.com/0/12981/847ac47e-6628-d120-5722-ad81249a1dc5.jpeg)


# 個人的な感想
- QueueBackgroundWorkItem
    - 失敗してもいいジョブ向き？（どんなジョブだ）
    - 失敗したときのリトライは？
        - 確実に実行したいならQueueを何らかのストレージで管理しないといけない
            - 普通にBatch作ったほうがいいのでは感
- Hangfire
    - 実行信頼性高い
    - ダッシュボードでジョブが可視化されるし、再実行も容易
    - スケールできるし、実質ほぼBatchなのでBatch系の処理をすべてこれでやるのも不可能ではなさそう
    - Queue管理も委譲するので実装は楽ではあるが依存度は高くなる

# おわりに
まだ調べて試してみたというところで、実例はないです。。。
間違いなどありましたら教えていただけると助かります :pray: 

# 参考資料

- http://blog.stephencleary.com/2014/06/fire-and-forget-on-asp-net.html
- http://www.hanselman.com/blog/HowToRunBackgroundTasksInASPNET.aspx