色んな事を書く

シンプルさを極めたエンジニアになりたい

Azure Function の Timer Trigger の色々

Timer Trigger Function とは

Timer Trigger Function は大まかに以下の特徴を持ちます

  • CRON 式、もしくは TimeSpan を使ってスケジュールを指定する
  • Azure Function はスケジュールに従って起動する
  • 実行履歴は Blob に記録されている

Blob に記録する実行状態

Blob に以下のような json を記録して実行状態を管理しています。

{
  "Last": "2023-03-01T16:00:35.0115543+09:00",
  "Next": "2023-03-01T16:01:35+09:00",
  "LastUpdated": "2023-03-01T16:00:35.0115543+09:00"
}

それぞれ

  • Last: 前回メソッド実行日時
  • Next: 前回 ScheduleStatus 更新した時点での、次のスケジュール実行予定日時
  • LastUpdated: 前回 ScheduleStatus 更新した日時

という値になってます。この json が TimerTrigger Function に Bind される TimerInfo.SchedleStatusマッピングされます。

この値は Function の処理が終了した時点で計算がなされます。await _executor.TryExecuteAsync(input, token); の部分が Azure Function として実装した部分になるはず。

Timer Trigger Function はどうやって定期実行をやっているのか

まぁそういうもんだからと言えばそうなのですが、定期実行の処理を組むための方法はいくつかあります。

それぞれの Timer の良し悪しがどうかはありますが、結論から言うと Timer Trigger Function では System.Timers.Timer を使って定期実行を行っています。

TimerListener の実装を読めばわかるのですが、端的に言うと以下のことをやっていますね。

  • インスタンス起動時に、 Attribute に指定した Schedule と Blob に記録した実行状態もとに次回実行時間を決める
  • Timer インスタンスの Elapsed にイベントを登録
    • 開発者が実装した処理がイベントとして登録される
  • Elapsed に追加したイベントの終了時に、次回 Function 起動までのインターバルを登録

Elapsed に追加したイベントの終了時に、次回 Function 起動までのインターバルを登録

何でイベントの終了時に毎回インターバルを計算しているかというと、Function の実行時間を加味しているためと思われます。例えば以下の状況で考えてみます。

  • Schedule は 10 分に一回
  • Function の実行時間は平均して 5 分
  • Function の初回実行時間は 10:00:00+00:00

Schedule は 10 分に一回なので、10:10:00+00:0010:20:00+00:00 と起動していくことが期待されます。しかし Function の実行時間を 5 分としているため、初回 Function の終了時間は 10:05:00+00:00 となります。そうすると次回の Function 実行時間までの Interval は 00:05:00 となるので、毎回計算が必要というわけですね。

autoReset=true として Interval に Schedule の値をそのまま使えばいいのでは?とも思うかもですが、例えば Function の起動時間が Schedule の間隔を越してしまうとどうなるのでしょうか?

  • Schedule は 10 分に一回
  • Function の起動時間が 11 分

そうすると autoReset=true なので Function が完了せずに次の Function が実行されてしまいます (Elapsed のイベントが実行されるので)。そうすると Blob の実行状態もおかしなことになってしまうので、毎度インスタンスを作り直しているのだと思います。

TimerTrigger Attribute で指定出来る事

ここにまとまってます。

learn.microsoft.com

Schedule に CRON 式か TimeSpan を指定し Function の起動間隔を定義する

Schedule は Function の実行間隔を定義できるものです。その定義の仕方として CRON 式TimeSpan が使えるよという事です。

CRON 式は慣れないとぱっと書けないと思います。私は 秒 分 時 日 月 曜日 という順番で定義するとだけ覚えています。複雑な式を書く場合は Chat GPT や Copilot にお世話になれるので、最低限で良いかなと思っている今日この頃。

TimeSpan は正確に言うと TimeSpan 値を指定出来る、です。なので TimeSpan.FromMinutes(1) とかではなく、00:01:00 のようにせねばいけません。TimerTrigger Attributestring しか受け付けないようになっています。

1 年に一回の起動をさせたければ、365.00:00:00 のような感じになりますね。これはこれでわかりにくい。

ちなみにですが、設定名を % で囲んでおくと Schedule に設定できます。例えば、

[Function("TimerTriggerSample")]
public void Run([TimerTrigger("%SampleTimerSchedule%")] TimerInfo myTimer)
{
    _logger.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");

    if (myTimer.ScheduleStatus is not null)
    {
        _logger.LogInformation($"Next timer schedule at: {myTimer.ScheduleStatus.Next}");
    }
}

のように定義すると、%SampleTimerSchedule% の設定値をそのまま使えます。環境変数から簡単に起動間隔を修正できます。

RunOnStartup=true にすると、Function Runtime の起動時に Function を実行できる

なので開発環境では true にしておくのが便利かと思います。1h に一度起動するみたいな Function の動作確認がだるいですからね。

Function Runtime の起動時という事なので、Azure にデプロイすると例えば Function App 自体の再起動時やインスタンスのスケールアウト時に Function が実行できるということになります。本番環境で true にするのは推奨されていないみたいですね。

実装を読んでみると、インスタンスが起動した時に問答無用で Function の処理を実行しています。その時点をベースに次回実行時の日時が決まるみたいですね。インスタンスが再起動したりすると、その時点で Function の起動タイミングがずれていくのが分かります。

例えば、RunStartup=true、Schedule が 1 時間に一回だったとして、

  1. 2024-03-01T09:00:00+00 Function が起動
  2. Next=2024-03-01T10:00:00+00 として Function が終了
  3. 2024-03-01T09:30:00+00インスタンスが再起動
  4. 再起動完了後に Function が起動
  5. Next=2024-03-01T10:30:00+00 として Function が終了

とかになってしまうわけです。インスタンス再起動はプロダクト運用者が意図せずに行われるので予測がつきません。期待しているスケジュールともずれていく可能性があるというのも含めて、本番環境では false にするのが推奨されているのでしょう。

UseMonitor で Blob にスケジュールを記録するか切り替える

規定では UseMonitor=true となっていて Timer Trigger Atrbute では false にすることが出来ません。ドキュメントには実行間隔が 1 分以上の場合は true と書かれており、Schedule の値で起動間隔を 1 分未満にすると記録されなくなります。

以下が該当部分の Source Code ですね。

github.com

Timer Trigger Function は冒頭でも書きましたが、Function 終了後に Blob に 実行状態を永続化します。もちろんこの書き込み処理にも時間はかかりますし、ネットワークアクセスも行うので、頻繁に起動される Function の場合は永続化したくないという事なのでしょう。

もちろん、これが永続化されないからといって定期実行が行われないわけではありません。冒頭でも話しましたが、TimerTrigger Function は内部的には Timerインスタンスを使っています。なのでこのインスタンスの Interval (Timer Trigger Attribute の Schedule で設定した間隔) が経過する度に Function が起動されます。

デメリットとしてはインスタンス再起動時に前回実行時の状態を復元出来ない事でしょうか。1s 未満の起動を期待する Function で、それがデメリットになる場面があるかはわかりませんが (どちらにせよ再起動時にすぐ起動するわけだし)。

ただ、そもそも起動間隔を 1s 未満にするという事は、「Function の実行時間も 1s 未満である」という事であり、そういう処理を Timer Trigger で実装する場面がどれほどあるかはわからないですね。

何故スケールアウトされても単一のインスタンスで実行されるのか

TimeTriggerListener に Singleton Attribute が Mode=Listener で付与されているからです。