本文へ移動
戻る
実装パターン2026-09-15 更新読了 6分

時間主導型トリガーの張り方と、二重起動を止める書き方

トリガーは画面からも張れますが、配布するツールではコードから張ることが多くなります。 そのときに出てくる「増える」「ずれる」「重なる」の3つを、順番に片づけていきます。

定期実行のツールを配ると、受け取った人はまずトリガーを張る必要があります。エディタの画面から張ってもらうこともできますが、手順が増えるほど途中でつまずく人も増えます。できれば設定ボタンひとつで済ませたいところです。

ただ、コードから張ると、画面から張るときには出てこない問題が3つあります。

コードから張ると、実行するたびに増える

ScriptApp.newTrigger() は、同じ関数のトリガーがすでにあっても、呼ぶたびに新しく作ります。重複のチェックはしてくれません。

// これを2回実行すると、5分おきの実行が2本になる
ScriptApp.newTrigger('runDigest').timeBased().everyMinutes(5).create();

設定ボタンを2回押す人がいると、集計メールが2通届くようになります。3回押せば3通です。

作る前に、同じ関数のトリガーを消しておくと安心です。

function ensureTrigger() {
  for (const trigger of ScriptApp.getProjectTriggers()) {
    if (trigger.getHandlerFunction() === 'runDigest') {
      ScriptApp.deleteTrigger(trigger);
    }
  }
  ScriptApp.newTrigger('runDigest').timeBased().everyMinutes(5).create();
}

'runDigest' の2か所を、定期実行したい関数の名前に置き換えて使います。

getProjectTriggers() は、このプロジェクトで自分が作ったトリガーをまとめて返します。関数名で絞らずに全部消すと、ほかの機能のトリガーまで消えてしまうので気をつけてください。

everyMinutes() に渡せる値は 1・5・10・15・30 のどれかです。7分や20分のような間隔は指定できません。

実行時刻はぴったりにはならない

everyHours(1) で張っても、毎時0分ちょうどに動くとは限りません。何分に動くかは Google 側で決まります。nearMinute() で分を指定しても、前後15分の幅があります。atHour(9) も同じで、9時台のどこかで動きます。

なので「毎朝9時00分の時点の値を記録する」のような処理は、思ったとおりになりません。時刻そのものを前提にせず、実行されたときに「前回から今までの分」を取る形にしておくと崩れません。

処理の中では new Date() を基準にして、そこから遡るのが扱いやすいです。

const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);

先頭の 7 が日数です。1日分にしたいなら 1 に変えます。

前の実行が終わる前に、次が始まる

5分おきのトリガーで、処理に6分かかったとします。前の実行がまだ動いているうちに次の実行が始まり、2つが同じシートへ同時に書き込むことになります。

行を追記する処理だと、同じデータが2行入ります。「最終行を調べて、その下に書く」書き方をしていると、2つが同じ最終行を見てしまい、片方の書き込みが上書きされます。

データが少ないうちは処理がすぐ終わるので、まず起きません。件数が増えて処理時間が延びてきた頃に、原因のわかりにくい重複として出てきます。

LockService で入口を1つにする

ロックを取れた実行だけが先に進み、取れなかった実行は何もせずに終わる、という形にします。

function runDigest() {
  const lock = LockService.getScriptLock();

  // 5秒待っても取れなければ、今回は何もしない。次のトリガーで拾える。
  if (!lock.tryLock(5000)) return;

  try {
    sendDigest(); // 集計してメールを送る、本来の処理
  } finally {
    lock.releaseLock();
  }
}

sendDigest() のところを、実際にやりたい処理に置き換えて使ってください。

ロックには3種類あります。

取り方 同時に1つだけになる範囲
getScriptLock() スクリプト全体。誰が実行しても同じロック
getUserLock() 実行しているユーザーごと。別のユーザーとは同時に動ける
getDocumentLock() 紐づいているドキュメントごと。ドキュメントに紐づかないスクリプトでは null

トリガーは、作った人のアカウントで実行されます。作った人が1人なら、getUserLock() でもトリガー同士の重なりは止められます。ただ、ほかの人が画面のボタンから同じ処理を呼んだ場合は、ユーザーが違うので止まりません。どこから呼ばれても1本にしておきたいなら、getScriptLock() を選んでおくのが確実です。

tryLock と waitLock の使い分け

tryLock(ミリ秒) は、ロックが取れたかどうかを true / false で返します。waitLock(ミリ秒) は、時間内に取れなければ例外を投げます。

定期実行なら tryLock がおすすめです。ロックが取れなかったときは、前の実行がまだ動いているということなので、その回は何もせずに終わらせて大丈夫です。残った分は次のトリガーが拾ってくれます。

waitLock で例外にすると、そのたびに実行ログへエラーが残ります。5分おきのトリガーだと、処理が重なるたびに失敗の記録が増えていきます。異常ではないものがエラーとして並ぶと、本当のエラーを見落としやすくなります。

画面のボタンから呼ぶ処理は逆で、押した人が結果を待っています。こちらは waitLock で待ってもらい、それでも取れなければ「ほかの処理が動いています」と画面に出すのが親切です。

解放は finally に置く

releaseLock() は finally の中に書きます。途中で例外が出ても、必ず解放されます。

スクリプトが終われば、ロックは自動で解放されます。それでも公式のリファレンスでは、使い終わったらすぐ解放するよう勧めています。ロックが要るのが処理の前半だけなら、そこで解放しておくと、待っているほかの実行が早く進めます。

動きがおかしいときに見るところ

トリガーが思ったとおりに動かないときは、エディタの左側から次の順に見ていくと早いです。

  1. 「トリガー」の画面で、同じ関数のトリガーが何本あるか
  2. 同じ画面に出ているエラー率
  3. 「実行数」の画面で、その関数が1回にどれくらいかかっているか

2本以上あるなら、ensureTrigger() を通さずに作ったか、途中で作り方を変えたときの古いものが残っています。1本しかないのに重複が出るなら、処理時間がトリガーの間隔を超えています。

2026-09-15 追記: getUserLock() がトリガーの二重起動に効かない、と書いていた部分は誤りでした。作った人が1人なら効きます。getScriptLock() を選ぶ理由を、別のユーザーからの実行も止められる点に直しています。