Google Apps Scriptを正確なスケジュールで実行する

課題

  • GASで「毎日 HH:MM に実行」といった感じのタスクスケジューリングをしたい
  • トリガー「時間主導型/日タイマー」だと「午前8時〜9時」のような、ざっくりとした指定しかできない
  • 「時間主導型/特定の日時」は YYYY-MM-DD HH:MM で指定ができ、指定した時間に正確に実行してくれるが、日時のピンポイント指定のみで条件指定ができないため、1実行につき1件のトリガーを作成しなければならず、運用するのは非現実的

ソリューション

「特定の日時」トリガーを動的に生成するタスクを「日タイマー」トリガーで実行する

サンプルコード

  • 下の例では createTriggers() で、土日祝を除く毎日、12:00、13:00、14:00に main() を実行するためのトリガーを生成する
  • 生成された main() のトリガーは実行が完了した後も残り続けるため、 ScriptApp.deleteTrigger() で掃除する
    • 削除する際に、 createTriggers() のトリガーを一緒に削除しないよう、trigger.getHandlerFunction() === 'main’ のものに絞って削除を行う
  • createTriggers() を実行するためのトリガーを「時間主導型/日タイマー/午前10時〜11時」で作成する
function isHoliday(date) {
  if (date.getDay() === 0 || date.getDay() === 6) {
    return true
  }
  
  // 日本の祝日カレンダーに終日予定があれば祝日とする
  var calendar = CalendarApp.getCalendarById('ja.japanese#holiday@group.v.calendar.google.com')
  var events = calendar.getEventsForDay(date)
  
  return events.length > 0
}

function createTriggers() {
  console.log('createTriggers')
  var now = new Date()

  // 残っているトリガーを掃除する
  var triggers = ScriptApp.getProjectTriggers()
  if (Array.isArray(triggers)) {
    triggers.forEach(function(trigger) {
      // mainのトリガーのみを削除する
      if(trigger.getHandlerFunction() === 'main') {
        ScriptApp.deleteTrigger(trigger);
      }
    })
  }
  
  // 土日祝はスケジュールしない
  if (isHoliday(now)) {
    console.log('there are no schedules today')
    return
  }
  var hours = [12, 13, 14]
  hours.forEach(function(hour) {
    var date = new Date()
    date.setHours(hour)
    date.setMinutes(0)
    if (now.valueOf() < date.valueOf()) {
      // main() のトリガーを指定した日時で作成
      ScriptApp.newTrigger("main").timeBased().at(date).create()
    }
  })
}

function main() {
  console.log('hello.')
}

実行結果

createTriggers() が日タイマーで実行された結果、11:00、12:00、13:00 のトリガーが生成されている。
(なぜか並び順は滅茶苦茶だが...)

f:id:nishaya:20171110152856p:plain

各トリガーの実行結果。
秒はまちまちだが、時/分までは指定した時刻に実行されているのがわかる。
(GASで console.log() した内容はStackdriver Loggingで確認できる)

f:id:nishaya:20171110152907p:plain

まとめ

  • GASを正確に定期実行したい場合、デフォルトで用意されている時間主導型トリガーのタイマーではなく、 ScriptApp.newTrigger() を使用する
  • 生成したトリガーは実行された後も残るので、次のトリガー生成のタイミングにリセットが必要