Hangfire でタスク実行時に追加の処理を行う

## はじめに

非同期にタスク（ジョブ）を分散実行できるOSS「Hangfire」ですが、
タスクの実行速度（着手までの時間、実行にかかった時間）をログに取りたいなと思って
色々と調べていたら `JobFilter` で実装できたので共有します。

### Hangfireとは？

[過去記事](https://qiita.com/yoh1496/items/98154b6596b2c8cccf04) も書いてますんで、読んで！

### リポジトリ

https://github.com/yoh1496/hangfire-dotnet3.1/tree/host_builder

上記リポジトリの内容をベースにお話します。

## 実装

実装は下記の公式ドキュメントにあるように `JobFilter` を使用して行っていきます。
https://docs.hangfire.io/en/latest/extensibility/using-job-filters.html

### 呼び出される関数

#### OnCreated(CreatedContext filterContext)

https://api.hangfire.io/html/M_Hangfire_Client_IClientFilter_OnCreated.htm

ジョブが生成された後に呼ばれます。フィルタが実行されるのはクライアント側。

#### OnCreating(CreatingContext filterContext)

https://api.hangfire.io/html/M_Hangfire_Client_IClientFilter_OnCreating.htm

ジョブが生成される前に呼ばれる関数。フィルタが実行されるのはクライアント側。

#### OnStateElection(ElectStateContext context)

https://api.hangfire.io/html/M_Hangfire_States_IElectStateFilter_OnStateElection.htm

ジョブがとあるstateに変わろうとしているタイミングで呼ばれる関数でしょうか。ここで違うstateに設定すればその遷移を止めることができる、とあります。使ったことないのでよくわかりません。

#### OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction)

https://api.hangfire.io/html/M_Hangfire_States_IApplyStateFilter_OnStateApplied.htm

ジョブの状態がとあるstateになった後に呼ばれる関数です。context.NewStateに遷移後のstateが格納されていますが、そのstateがどの型かによって格納されている情報が異なります。

#### OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction)

https://api.hangfire.io/html/M_Hangfire_States_IApplyStateFilter_OnStateUnapplied.htm

ジョブの状態がとあるstateからunappliedされたときに呼ばれる関数です。unappliedされる状況がいまいちわかっていないので、今後試していきたいと思います。

#### OnPerformed(PerformedContext filterContext)

https://api.hangfire.io/html/M_Hangfire_Server_IServerFilter_OnPerformed.htm

ジョブが実行された後に呼ばれる関数です。フィルタが実行されるのはサーバー側。

#### OnPerforming(PerformingContext filterContext)

https://api.hangfire.io/html/M_Hangfire_Server_IServerFilter_OnPerforming.htm

ジョブが実行される前に呼ばれる関数です。フィルタが実行されるのはサーバー側。

### 使うにあたって

上記に述べたとおり、サーバー側で呼ばれる関数もあればクライアント側で呼ばれる関数もあり、1つのジョブを追いかけるのはなかなか難しいです。よってログ集積ツールなど別途使用して追えるようにしておくのが現実的でしょうか。

また、ドキュメントに記載されている方法では、`CreatedContext`や`PerformedContext`に実装されている`Items` はジョブに追加情報を付加できる key-value に値を設定でき、異なるフィルタ間や関数間で共有するときに使うと良いとのことです。

### 使用例

```csharp
using System.Text.Json;
using Hangfire.Common;
using Hangfire.Logging;
using Hangfire.States;
using Hangfire.Storage;

namespace server.Filters {
    public class ReportPerformanceFilter : JobFilterAttribute, IApplyStateFilter {
        readonly ILog Logger = LogProvider.GetCurrentClassLogger();

        public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction) {
            var succeededState = context.NewState as SucceededState;
            if (succeededState != null) {
                Logger.Info(JsonSerializer.Serialize(
                    new { Latency = succeededState.Latency, Duration = succeededState.PerformanceDuration, },
                    new JsonSerializerOptions() {
                        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                    }));
            }
        }
        public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction) { }
    }
}
```

今回は タスクの Latency と Duration を出力する ReportPerformanceFilter を作成してみました。

これを下記のように `UseFilter` 関数を使って使用するよう設定します。

```csharp
services.AddHangfire(config => {
    config.UseColouredConsoleLogProvider();
    config.UseFilter(new Filters.ReportPerformanceFilter());
    config.UseMongoStorage("mongodb://localhost", "ApplicationDatabase");
});
```

## 実行例

左がキューにタスクを入れるプロセス、右がHangfire Server（ワーカー）の出力です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/755d7b43-37c3-540d-a2be-2fd180c68948.png)

4プロセスが同時に動いているので4ずつ出力されていきます。

## おわりに

このように、各タスクが開始したり終了するタイミングで特定の処理を挟む実装を行えることがわかりました。

今回使用したコードは下記タグで保存してあります。
https://github.com/yoh1496/hangfire-dotnet3.1/tree/filter