Treasure Data - Support Engineering Team blog

トレジャーデータのサポートエンジニアリングチームのブログです。

Workflowの複雑なスケジュール設定を紹介します

※ 2022/03/14(Mon) 更新

こんにちは、Treasure Data サポートの伊藤です。

今回は、複雑なスケジュール設定をWorkflowで実現する方法について説明します。 サンプルも記載しますが、参考にされる場合は動作確認の上お使いください。

Workflowのスケジュール設定概要

Workflowは基本的には定期的に実行することを目的で利用されていることが多く、1度だけの処理のために実装することは少ないでしょう。

処理内容によって、日次、週次、月次など様々な間隔で実行する要件があるかと思いますが、基本的には ドキュメントにある下記記法で十分かと思います。

記法 説明
hourly>: MM:SS MM:SSに毎事実行
daily>: HH:MM:SS HH:MM:SSに毎日実行
weekly>: DDD,HH:MM:SS 毎週曜日がDDDのHH:MM:SSに実行
monthly>: D,HH:MM:SS 毎月D日のHH:MM:SSに実行
minutes_interval>: M 毎M分ごとに実行

複雑なスケジュール設定をするには

複雑なスケジュール設定する場合は先述した方法では十分ではないため、下記にある cron>: を利用する必要があります。

timezone: Asia/Tokyo

schedule:
  cron>: 42 4 1 * *

cron は Treasure Data 固有の考え方・方法ではありません。基本的にUnixLinuxで定期実行のスケジュール管理するための crontab コマンドによって利用できる機能と同等なため、cronやcrontabでWeb検索いただくと書き方やサンプルなどを見つけることができるでしょう。

日本の方が多いと思うので、本記事はスケジュールに関してタイムゾーンAsia/Tokyo、すなわち +09:00 とさせていただきます。Workflow(digファイル)に timezone: Asia/Tokyo と記載することでそのようにして取り扱われます。

cronについて

まず基本的な書き方ですが、5つの数値を半角スペースで区切って記法します。その5つの数字は左から順に、分、時、日、月、曜日を表します。

記載できる数値は下記の通りで、覚えづらい部分としては一番右側の曜日部分でしょう。0から7の数字を記載することで曜日を表すのですが、0と7は日曜日となり、1は月曜日、2は火曜日、・・・ということになります。曜日の最初の三文字(SUN, MON, ....)を利用することもできるので、こちらのほうが可読性が高いかもしれませんが、後述のリスト・範囲といった記法には利用できないので、要件に合わせてどちらで記載するか決めてください。

項目 数値
0から59
0から23
1から31
1から12
曜日 0から7

また、数値以外では * を指定することができ、これは特定の時刻ではなく毎回を意味しします。例えば1つめが * なのであれば毎分実行するスケジュールということになります。

そのため、先述した cron>: 42 4 1 * * であれば、毎月1日の04:42に実行するスケジュールとなります。

また、記法として下記2通りの方法が利用できます。これらを組み合わせることで daily>: などでは実現できなかった複雑なスケジュール実行が可能になるでしょう。

記法 設定例 説明
リスト cron>: 0,10,20 * * * * 毎時 0分、10分、20分に実行
範囲 cron>: 0 0 1 1-5 * 1から5月の間、1日の00:00に実行

サンプル

では、ここからはcron>:を用いた具体的なユースケースとサンプルを作っていきます。

cron>: は先述した通り他の記法と比較して複雑なスケジュール設定が可能ですが、後述するような複雑なスケジュール設定は cron>:だけでは実現できません。

そのため、cron>: で最低限のスケジュール設定を実装しつつ、後続の処理で条件分岐するという対応が必要になります。

毎月最初の月曜日に実行

まず、毎月最初の月曜日の13:00に実行するユースケースを考えてみます。

cronとしては「毎月最初の」という決まった記法があるわけではないため、何かしらのロジックを考えて実装する必要があります。 曜日は7通りあり、かつ該当月で最初ということは1日から7日の間の何れかの日付となるのは確実なので、日が1〜7で月曜日という条件で実装できそうです。例えば下記のように記載することになります。

timezone: Asia/Tokyo

schedule:
  cron>: 0 13 1-7 * 1

ここで、細かいのですが注意点があります。

crontabコマンドの場合とWorkflowの場合で、同じ記載でも挙動(スケジュール)が異なります。crontabコマンドの場合上記は期待したスケジュール設定とはなりません。

man 5 crontab コマンドでcronについて確認するとわかるのですが、日と曜日がどちらも設定されている場合はANDではなくORで判定されるという仕様になっています。 そのため、1日〜7日の13時と、全ての月曜日の13時に実行されてしまいます。

Note: The day of a command's execution can be specified by two fields -- day of month, and day of week. If both fields are restricted (ie, are not *), the command will be run when either field matches the current time. For example, ``30 4 1,15 * 5'' would cause a command to be run at 4:30 am on the 1st and 15th of each month, plus every Friday.

一方、Workflowはcron4jというライブラリを利用しスケジュール設定される作りになっています。Documentに下記記載がある通り、crontabコマンドとは異なりORではなくANDで判定されます。

Once the scheduler is started, a task will be launched when the five parts in its scheduling pattern will be true at the same time.

少し脱線してしまいましたが、例のように記載いただければ、0分 かつ 13時 かつ 1〜7日 かつ 月曜日 という条件を満たす場合のみ実行されるため、要件を満たすことができます。

平日のみ(土日祝日は処理しない)

たまにお問い合わせを頂戴するのですが、平日のみ(土日祝日は処理させない)処理したいというユースケースを考えます。 2021年のケースで考えるので祝日は下記に記載されているものを想定します。

https://www8.cao.go.jp/chosei/shukujitsu/gaiyou.html

土日に実行させないという点だけであれば cron>: 0 13 * * 1-5 で良いのですが、考えなければいけないのは祝日判定部分です。 残念ながらTreasure Dataでは祝日かどうか判定する仕組みもなければ祝日リストを保持しているわけではないので、何かしらの方法で祝日リストを格納しておき、月〜金曜日に実行した直後に祝日かどうかチェックする必要があります。

具体的には、テーブルに事前に格納しておくか、下記サンプルのようにクエリ内に祝日リストを入れておくことで実現できるでしょう。

Workflowのビルトイン変数 ${session_date} とクエリ内に内包した holiday カラムを IN 句で比較することで、実行予定日が祝日であれば true が、そうでなければ false が返ります。

WITH holiday_list AS (
SELECT holiday
  FROM (VALUES
  ('2021-01-01','元日'),
  ('2021-01-11','成人の日'),
  ('2021-02-11','建国記念の日'),
  ('2021-02-23','天皇誕生日'),
  ('2021-03-20','春分の日'),
  ('2021-04-29','昭和の日'),
  ('2021-05-03','憲法記念日'),
  ('2021-05-04','みどりの日'),
  ('2021-05-05','こどもの日'),
  ('2021-07-22','海の日'),
  ('2021-07-23','スポーツの日'),
  ('2021-08-08','山の日'),
  ('2021-08-09','休日'),
  ('2021-09-20','敬老の日'),
  ('2021-09-23','秋分の日'),
  ('2021-11-03','文化の日'),
  ('2021-11-23','勤労感謝の日')
  ) AS t(holiday, holiday_desc)
)
SELECT '${session_date}' IN (SELECT holiday
                               FROM holiday_list                            
                            ) AS result_holiday
;

このクエリの結果を後続のタスクで利用すれば、祝日かどうかの条件分岐が可能になります。 Workflow(digファイル)のサンプルとしては下記のようになります。

timezone: Asia/Tokyo

schedule:
  cron>: 0 13 * * 1-5

_export:
  td:
    database: kazzy_test

+check_holiday:
  td>: check_holiday.sql
  store_last_results: true

+judge_holiday:
  if>: ${td.last_results.result_holiday}
  _do:
    echo>: Today is holiday
  _else_do:
    echo>: Today is business day

最後に整理すると下記のような流れで実装するサンプルとなります。

  • 月〜金曜日に実行するよう schedule: で設定
  • td>: オペレータで祝日のときはtrue、そうでないときはfalseを返すクエリを実行
  • if>: オペレータでクエリ結果がtrueのときとそうでないときの処理を分岐

隔週の月曜日実行

今度は隔週で実行するユースケースになります。 曜日は月曜日、時刻は13:00で考えてみます。

週番号(その年で第n週なのか)を2で割った余りで判定させてもそれらしく動作するかと思いますが、年の切り替わりなどを考慮すると、ある基準日時(月曜日)から何日経過したかを算出し、14で割った余りが0の場合に処理させるなどで十分ではないかと思います。

時刻と曜日は cron>: で指定しておき、Moment.jsにて実行予定日時(session_time変数)と基準日(下記例では 2021-01-04)との差分を算出し、14で割った余りが0かどうかを if>: オペレータで判定するのが良いでしょう。

下記サンプルになりますが、実行日を1週ずらしたい場合は基準日をずらすか14で割った余りが7かどうかで判定すれば良いかと思います。

timezone: Asia/Tokyo

schedule:
  cron>: 0 13 * * 1

+check_biweekly:
  if>: ${moment(session_time).diff(moment('2021-01-04 00:00:00'), 'days')%14==0}
  _do:
    echo>: Today is day for processing!

月末のみ実行

こちらは月末のみ実行するユースケースです。

月初であれば当然1日にのみ実行するようにしていただければ良いのですが、月末の場合は月ごとに異なるということと、うるう年を考慮しなければいけません。

いくつか方法はあるのではないかと思いますが、今回は毎月28〜31日に実行させ、翌日が1日かどうかで条件分岐するという方法を紹介します。

今回のユースケースでもMoment.jsを使います。実行予定日時はビルトイン変数の session_time を利用して、Moment.jsのadd関数にて1日追加、format関数で日部分のみを抽出し、Java Scriptsの比較演算子で比較しています。

timezone: Asia/Tokyo

schedule:
  cron>: 0 13 28-31 * *

+check_lastday:
  if>: ${moment(session_time).add(1, 'day').format('DD')=='01'}
  _do:
    echo>: Today is last day of this month.

第2月曜日と毎週水曜日のような複数スケジュール

最後に、第2月曜日と毎週水曜日の13時に実行させたいというユースケースについて考えてみます。

1つのWorkflowで頑張って実装しても良いのですが、せっかくなので複数Workflow(digファイル)を用いる方法を使ってみましょう。

まず、下記2つのWorkflowを作成します。前者は毎月最初の月曜日に実行のケースを参考にし、後者は cron>: 0 13 * * 3 で良いでしょう。

  • 第2月曜日の13時に実行するWorkflow
  • 毎週水曜日の13時に実行するWorkflow

そして上記2つのWorkflowから call>: オペレータを利用して、実施したい処理が記載されているWorkflowを呼び出します。 そうすることで、1つのWorkflowで複雑な条件分岐を書かずとも簡単に実装することができます。

timezone: Asia/Tokyo

schedule:
  cron>: 0 13 * * 3

+call_main:
  call>: main.dig
timezone: Asia/Tokyo

schedule:
  cron>: 0 13 * * 1

+check_day:
  if>: ${(moment(session_time).format('DD')>=8) && (moment(session_time).format('DD')<=14)}
  _do:
    +call_main:
      call_task>: main.dig

最後に

いくつかサンプルを紹介させていただきましたが、いかがでしたでしょうか?

内容を見ていただいたら感じていただけたのかと思うのですが、特別な方法があるわけではなく、要件を満たすようなロジックを考える部分が肝になっています。 そのため、まずロジックを考えていただき、それから実装方法を検討すると良いでしょう。

本記事がそのロジック及び実装方法の参考となると嬉しいです。