1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

たった200行で,GASのトリガ重複実行を検出・回避し,シート末尾に定期的にログ記録するサンプルコード (排他制御しつつ,Googleスプレッドシート内でデータが存在する最終行に情報記録)

1
Last updated at Posted at 2026-04-07

(※この記事の末尾には,続編へのリンクが掲載されています。)

トリガ機能を使う際,排他制御を考えるべきなのはなぜか

Googleスプレッドシート(GAS)では,トリガ機能を使ってバッチ処理を実装できます。
1分おきに処理を呼び出し」など定期的なタスク起動ができるのです。

このような定期処理を実現する場合,「同じ処理が同時に2つ呼ばれてしまわないように」という排他制御が必要です。


もし何かの手違いで,同じ処理が「同時に2つ」呼ばれてしまったら大変ですよね~。
同一シートの同一セルを,複数のプログラムが同時に別々の値で書き換えようとしたら…。シート内のデータがぐちゃぐちゃになっちゃいます。

そうならないようにするため,「いまこの瞬間に実行中の処理が必ず1個だけ」になるよう制限しなければいけません。これが排他制御です。


バッチ処理による定期実行でも,前回の処理がまだ終わってないうちに次回の処理が呼び出されてしまったら不都合が生じます。

Googleスプレッドシート上でトリガ機能を使う場合も同じです。
トリガの重複起動を回避する仕組みが要りますね。

下記のサンプルコードで試してみましょう。

ぴったり200行のサンプルコード

下記のコードはちょうど200行あるのですが,前半と後半の100行ずつで内容が分かれています。

  1. 前半の100行: トリガで起動された際に,ロック機能で排他制御する仕組み。

  2. 後半の100行: 排他制御しながら実行したい,具体的な処理の内容。ここでは,「シートの末尾にログを1行追加する」という内容の関数を作ってあります。

この前半と後半で,きっちりコードを分離することが重要なのです。
コードの構造がはっきりし,エラー発生時の分岐なども混同しなくて済みます。

  • 前半をテンプレート的に使いまわし,その部分には細かい処理内容は書かない。

  • 後半に,本当にやりたいこと(メインディッシュ)を記載する。

まず下記にコードを掲載し,そのあとでトリガ設定の方法などを述べます。

// 「たった200行で,Googleスプレッドシート上で
//  排他制御で同時実行を避けつつトリガ機能で関数を定期的に呼び出すサンプル」
// 
// 内容は下記の通りです。
//  (1) 「ブック内でトリガが重複実行されないように排他制御する処理」
//  (2) 「シート内の記録部分の最終行にログ追記する処理」(行数が上限を超えたら自動ローテート)
// GASエディタ上でトリガとして(1)を呼び出し,次いで(1)が(2)を呼び出します。
//
// (2)でログをわずか1行だけ記録する短時間の処理のため(1)で排他制御は不要では?
// と思われるかもしれません。
// それでも,複数人で1ブックを更新操作して更新イベントを拾う場合などに
// トリガが同時実行されてログ記載箇所がかち合うリスクはあります。そのため,
// 「トリガを利用する際にはロック処理のコードを併記する習慣を持つ」ために役立ちます。
// さらに,「排他制御のロジックとメイン処理のロジックをコード上は分離する」という習慣も持てます。
// 
// なお大きな目標としては,「Googleスプレッドシート(GAS)から
// Blueskyに定期的にbot投稿するコード」を作ろうとしており,
// その途中経過として本サンプルコードを記録しています。
// 2026.4.6. @rwanda_go_tan


// GASエディタ上で定期的なトリガを設定するメインの関数。
function my_triggeredMainFunction(){

  // ここには具体的な処理詳細は記載しない。
  // ロック取得して排他制御をかけるのみ。

  // ロック取得まで最大でどれだけ待つか
  const wait_ms_for_lock = 10 * 1000;

  // ドキュメントロック取得を試みる
  var docLock = LockService.getDocumentLock();
    my_debug_log( "ロック取得を試みます。最大待機時間は " + wait_ms_for_lock +"ms" );
  if (docLock.tryLock( wait_ms_for_lock )) {

    // メイン処理を呼び出してからロック開放する。
    try {
      my_debug_log( "ロック取得に成功しました。" );
      
      // ロック保持中に呼び出されるメイン処理
      my_mainRoutineWhileLockedSection();

    } catch(e) {
        // メイン処理中のエラー分岐があればここに記載
        my_err_log( "ロック保持中のメイン処理内でエラー発生。", e );

    } finally {
        // メイン処理の実行成否に関わりなく,最後にロック解放
        docLock.releaseLock();
          my_debug_log( "ロック解放しました。" );
    }
  }else{
    // 指定秒数だけ待ってもロック取得できなかった場合   
    my_err_log("ロック取得に失敗しました。");
  }

  return;

  // ロック処理について:
  // 
  // 排他制御でGoogle Apps Scriptを安全に実行【GAS】 (2018年)
  // https://officeforest.org/wp/%E6%8E%92%E4%BB%96%E5%88%B6%E5%BE%A1%E3%81%A7google-apps-script%E3%82%92%E5%AE%89%E5%85%A8%E3%81%AB%E5%AE%9F%E8%A1%8C/
  //
  //【GASの排他制御】LockServiceでデータの上書きを防止せよ (2024年)
  // https://uncle-gas.com/exclusive-control-by-lockservice/


  // トリガのスケジューリングについて:
  //
  // GASで定期実行を安定稼働させるには|実行抜け・タイムアウトを防ぐ設計の考え方 (2026年)
  // https://aizen-ai.co.jp/google-apps-script-trigger-automation-guide/

}


// ロック保持中に呼び出されるメイン処理
// (ロック保持中なので,ここに記載した処理が同時に重複実行される心配は不要)
function my_mainRoutineWhileLockedSection(){

  my_recordOneLogOnSheet("テスト動作用にトリガが起動されました。ロック保持中にログ記録しています。");

}



// ------------------------ 以下はログ用関数 -----------------------------



// 開発中のデバッグ用にコンソールログ表示
function my_debug_log( s ){
  console.log( "デバッグ情報: " + s );
}


// エラー発生時用にコンソールログ表示
function my_err_log( s, error ){
  console.error( "エラー情報: " + s, error );
}


// シート上にログを記録する。
// (シート内で行数が増えすぎたら,先頭にある古い内容は自動的に削除してゆく。)
function my_recordOneLogOnSheet(log_content){

  // ログ記録用のシート名
  const log_sheet_name = "全体ログ";

  // 何列目に日時を記録するか
  const col_num_logging_datetime = 2;

  // 何列目にログ本文を記録するか
  const col_num_logging_content = 4;

  // 何行目からログ記録を開始するか
  const row_num_logging_start = 2;

  // ログが何件まで増えたら,先頭カット処理を行なうか
  const max_log_count = 2500; //10;

  // ログが想定の最大件数を超過したら,先頭から何行をカット処理するか
  const how_many_logs_to_cut_off_from_head = 500; //3;

    // ※動作テストする場合は,10行を上限,カット数を3行などにします。

  // 以上,設定項目


  // シート名でシート取得
  const log_sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName( log_sheet_name );


  // シート内のログの最終行に追記するために,記録すべき行番号を求める。
  let lastCell = null;
  let lastRowNum = null;
  let row_num_new_record = -1;
  // ログが空か?
  const emptytest_str1 = log_sheet.getRange(row_num_logging_start, col_num_logging_datetime).getValue();
  // それとも,ログは1行だけか?
  const emptytest_str2 = log_sheet.getRange(row_num_logging_start + 1, col_num_logging_datetime).getValue();
  if( // ログが空の場合
    ( ! emptytest_str1 ) || ( emptytest_str1.length < 1 )
  ){
    // 最初の行に記載すべき
    row_num_new_record = row_num_logging_start;
  }
  else
  if( // ログが1行だけの場合
    ( ! emptytest_str2 ) || ( emptytest_str2.length < 1 )
  ){
    // 最初の行の次の行に記載すべき
    row_num_new_record = row_num_logging_start + 1;
  }
  else
  {
    // 列内で値がある最後のセルを求める。
    // 下方向(DOWN)のデータがある最終セルを取得
    lastCell = log_sheet.getRange(row_num_logging_start, col_num_logging_datetime).getNextDataCell(SpreadsheetApp.Direction.DOWN);
      // NOTE: 上記のコードは,ログ総数が0行の場合と1行の場合には正しく動作しない。
      // ログが2行以上ある場合は正しく動作する。
      // ログ総数が0行の場合と1行の場合には,シート内の「値の有無にかかわらず」最終行に飛んでしまう。
      // そのため,ログ総数が0行の場合と1行の場合は事前に分岐して別処理とする。

    // 最終セルの行番号を取得
    lastRowNum = lastCell.getRow();

    // 新規ログを記録すべき行番号
    row_num_new_record = lastRowNum + 1;
  }
    my_debug_log( "新規ログを記録すべき行番号は" + row_num_new_record );


  // 現在の日時を文字列としてフォーマット
  const current_datetime = Utilities.formatDate(new Date(), "JST", "yyyy-MM-dd (E) HH:mm:ss");
    // GASでのDate型データのフォーマット
    // https://jp.tdsynnex.com/blog/google/gas-data-format/


  // ログ内容をセルに記録
  log_sheet.getRange(row_num_new_record, col_num_logging_datetime).setValue( current_datetime );
  log_sheet.getRange(row_num_new_record, col_num_logging_content).setValue( log_content );


  // もし行数が増え過ぎたら,先頭の内容をカット。
  const current_log_count = row_num_new_record - row_num_logging_start + 1;
    my_debug_log( "現在のログ行数は current_log_count = " + current_log_count );
    my_debug_log( "ログ行数の上限値は max_log_count = " + max_log_count );
  // 上限を超過?
  if( max_log_count < current_log_count ){

    my_debug_log( "先頭のログをカット(削除)します。");

    // 先頭のログをカット(削除)する。行ごと消して上に詰める。
    log_sheet.deleteRows( row_num_logging_start, how_many_logs_to_cut_off_from_head + 1 );
      // https://auto-worker.com/blog/?p=4621

  }

  return;
}

コードの内容を概説します。

最初にある my_triggeredMainFunction という関数が,GASにトリガ登録しておくメイン関数です。
Googleスプレッドシート上で,この関数が定期的に呼び出されます。

排他制御のために,LockService というロック機構を使っています。
もしロックを取得できなかったら,「トリガが重複起動されている」と判断して処理を終了します。
(こういった事柄は,マルチスレッドプログラミングの基本事項としてよくご存じの方もきっと多いでしょう。)


次いで,my_mainRoutineWhileLockedSection という関数を書いてあります。
この関数は,排他制御が安全にできているという保証のもとで呼び出されます。

自分が行ないたいメインの処理内容は,この関数の中に記述しましょう。
そうすれば,「排他制御がかかって安全に実行されていることが保証された状況下」で,自分が望む通りの処理を確実に実行しきれます。


最後に my_recordOneLogOnSheet という関数を書いてあります。
これは,特定のシート内に上から下へ向かって1行ずつ,ログを記載していく…という地味な処理です。

もしそのシート内で行数が増えすぎたら,先頭にある古い内容は自動的に削除してゆきます。
容量が増え過ぎないように,ログをローテーションしているわけですね。
何行まで保管するかなどの値は変えられるので,関数内の設定項目をご自由に変更してください。

なお,この「シート内にログ記録」という関数は排他制御のために必須なロジックではありません。
そうではなく,「排他制御されながら呼び出される側の具体的な処理の一例」として,今回はこのようなものを作ってみた という事です。

テスト実行してみよう

このコードが動くためには,スプレッドシート内に「全体ログ」という名前のシートが必要です。
ですので,そのシートをまず作っておきましょう。


そして,GASエディタ上の画面上部の関数選択プルダウンから my_triggeredMainFunction を選び「実行」を押してみます。

すると,「全体ログ」のシート上にテスト用のログが1件記録されます。
前述のサンプルコードの通りであれば,B2セルに実行日時,D2セルに下記のテスト文言が入力されます。

「テスト動作用にトリガが起動されました。ロック保持中にログ記録しています。」

トリガを設定してみよう

GASエディタ上で,画面の左側に目覚まし時計のアイコンがあるのでクリックします。
トリガ一覧画面が開きます。

右下の「トリガーを追加」を押し,トリガー内容の設定ダイアログが開くので,下記の通り設定します。

  • 「実行する関数を選択」→ my_triggeredMainFunction
  • 「実行するデプロイを選択」→ Head
  • 「イベントのソースを選択」→ 時間主導型
  • 「時間ベースのトリガーのタイプを選択」→ 分ベースのタイマー
  • 「時間の間隔を選択(分)」→ 15分おき
  • ダイアログ右上の「エラー通知設定」→ 毎日通知を受け取る

ダイアログ右下の「保存」を押下。

すると,トリガが保存されます。

image.png

上記では15分おきに定期呼び出しとしてあります。お好みで変更してください。

トリガの実行結果

トリガ設定ダイアログで設定した通りの時間間隔おきに,指定の関数が呼び出されます。

前述のサンプルコードの場合は,「全体ログ」という名前のシート上に上から下に向かって1行ずつログが追記されていきます。

image.png

上の画像はシートのスクショですが,どうしてこういう列の配置になっているのか?については,ワンポイントアドバイスがあります。


※ワンポイントアドバイス:
「たくさんの行にデータを列挙するシート」での列配置について。

「なんで上の画像では,見出しを記載する列とデータを記載する列を1列ずつずらしてあるの?」と,疑問に思われるかもしれません。

普通,先頭行に見出しとして項目名称を書いたら,その真下に該当データをずらずら~っと記入していきますよね。
そうすれば,オートフィルタなどの機能を使いやすいですもんね。
でも,ここではそうはしていません。

見出し記載の列とデータ記載の列を,あえて1列ずらす理由。それは・・・
列を選択してDELETEキーを押せば,簡単にデータを全件消せるから。」です。

DELETEキーを押したときに,消したいのはデータだけ です。見出しは消したくない。
そのために,見出しとデータの記載列を1列だけずらしておく…という,ちょこざいなテクニックなんですね~。



上記の「シート上へのログ記録」は,自分で実装したものです。
しかし自分で実装しなくても,毎回トリガ起動に成功したかどうかを見れる欄があります。

GASエディタの画面左端にある「実行数」というメニューを開いてみましょう。

image.png

これを見ると,右はしの「ステータス」の欄で大多数は「完了」となっているんですが,たまに「失敗しました」というエラー になってますね。

「失敗しました」をクリックして,エラー内容を見ると,たとえば下記のようなエラーメッセージが表示されていたりします。

Cloud のログ
2026/04/07 11:14:40 エラー The JavaScript runtime exited unexpectedly.

これは,たとえばメモリ不足などGoogleのサーバ上でトリガ起動に失敗した…という事なんだそうです。

無料でタイマーを使えるって言っても,完ぺきではない。現実的には,けっこうな頻度で起動に失敗するんですね。
まあ無料ですし・・・。文句を言わないほうがいいでしょう。クラウドってそういうもんです。

「15分おきにトリガ起動」と設定しても,2回連続で起動に失敗して,3回目でようやく起動に成功して,30~45分間ぐらいおとさたがない。なんて事もあります。

15分おきで定期的に起動する設定にしておいた場合,15分おきにきっちり実行されるわけではなく,「まあ1時間に1回は最低でも実行してもらえるだろう」ぐらいのつもりでいたほうがいいんでしょうね。

まとめと感想

今回,掲載したコードの挙動からわかることは下記の通りです。

Googleスプレッドシート上で無料でバッチ処理を実装する場合,毎回の処理が正常に実行されるためには,下記の条件をすべて満たす必要がある。

  1. まずは,トリガを指定時刻に設定しておくこと。
  2. 次に,1.で指定した時刻が実際に到来した時に,Googleのサーバ上でメモリ不足などのエラーが起きず,トリガ起動に成功すること。
  3. さらに,2.で起動に成功したトリガが呼び出した関数内で排他制御を行なっている場合,ロックの取得に成功すること。(トリガの重複起動を回避できること。)
  4. そして,3.で排他制御に成功したロジック内で,エラーを起こさずに処理をやり遂げること。エラーが起きたとしても適宜,そのエラーをcatchして,たとえ正常系の分岐をやり遂げられなかったとしても異常系の分岐を最後までやり遂げるように例外捕捉処理を作り込んでおくこと。
  5. おまけに,4.の処理を行なう際に,GASの仕様として1トリガあたり上限の実行時間は6分と決まっているので,1回のトリガ起動あたり6分以内に処理を終えること。
  6. さらにおまけに,5.の1トリガごとの時間制限に加えて,GAS上での1日の処理時間の合計は90分までという制約もあるので,1日あたり累計90分以内で全トリガの処理を終えること。

上記の全条件をクリアできるように,不測の事態にも備え,ゆとりを持たせたバッチ処理を設計・実装してゆくことにいたしましょう。


なお,私が今回どうしてこのような「GASトリガ排他制御のサンプルコード」を書いたかというと,
Twitter(現X)上で2026年にbot利用が規制され,無料ではbotを作れなくなってしまったのがきっかけです。

かわりに,「GASでBlueskyにbot投稿するような仕組み」を自前で作りたいと思ったのです。
そのためには,1時間おきにBlueskyのbot APIを呼び出すような堅牢なバッチ処理が必要になります。

Qiitaの記事:
「たった100行で,Blueskyにbot投稿するGASサンプルコード (GoogleスプレッドシートからブルースカイAPIを無料で使う方法を理解する)」
https://qiita.com/rwanda_go_tan/items/c2a28e21b9db3ec05004

  • Blueskyの投稿APIをGASから使うための最も簡潔なコード。

Posfieまとめ:
「X(旧Twitter)上で,もう無料でbot作成できない。2026年2~3月にAPI完全有料化。価格は100ツイート1ドル。10ドル分クレジットを消費し次第botつぶやきを強制停止。有力な移行先・代替手段はBlueskyか」
https://posfie.com/@ouen_suru_tan/p/iT3HTHp

  • 2026年3月の情報

Posfieまとめ:
「作業ログ:【パート2】「X(旧Twitter)上で,もう無料でbot作成できない」の件の続き。Blueskyへのbot移行に向けGASで開発作業を進めるも,次々にアカウントが停められてゆく…」
https://posfie.com/@ouen_suru_tan/p/wxlcpz6

  • 2026年4月の情報

SNS等のWebサービスのAPI呼び出しと,今回の記事にあるバッチ処理。
これらを組み合わせれば,botの仕組みを自前で無料で作れますね!
365日,24時間ずっと安定してbotが動き続けてほしい。


実際にbotの仕組みを自前でしっかり作ってみた結果,どんなコードになるのか?については,それはまた今度。
そのうち別の記事にて作業経過や,プロトタイプ的な作品をご紹介させてください。


あと末筆ながら,いまさら気づいたんですけど,シート内の「最終行」にログ記録ってのはあまり良くなかったような気が・・・
シートを開くたびに最終行まで毎回,手動でスクロールするのがめんどいし,最終行じゃなくて「一番上の行」に追記する形でログ記録するようにしといたほうが見やすくなったかも。。。

参考資料

コード中のコメント内にも記載されていますが,GASでのトリガ排他制御については下記リンクが参考になります。

排他制御でGoogle Apps Scriptを安全に実行【GAS】 (2018年)
https://officeforest.org/wp/%E6%8E%92%E4%BB%96%E5%88%B6%E5%BE%A1%E3%81%A7google-apps-script%E3%82%92%E5%AE%89%E5%85%A8%E3%81%AB%E5%AE%9F%E8%A1%8C/

  • tryLockとwaitLockを比較

【GASの排他制御】LockServiceでデータの上書きを防止せよ (2024年)
https://uncle-gas.com/exclusive-control-by-lockservice/

  • 同時起動のシチュエーションを人為的に引き起こして,ロック動作を検証するサンプル

GASで定期実行を安定稼働させるには|実行抜け・タイムアウトを防ぐ設計の考え方 (2026年)
https://aizen-ai.co.jp/google-apps-script-trigger-automation-guide/

  • トリガのスケジューリングについて考え方が整理されている

追記: 汎用的なフレームワークを作りました。

(2026年5月14日に追記です。)

GASのトリガを管理するための,汎用的なフレームワークを作成・公開しました。

下記のQiita記事をご覧ください。

GASのトリガー残り時間を制御・配分・管理する汎用フレームワーク「GTRM (=GAS トリガー・リソース・マネージャー)」。わずか2ステップで利用開始する方法
https://qiita.com/rwanda_go_tan/items/26071fdcbe7f7a915765

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?