GAS(Google Apps Script)の実行時間を伸ばす

# これはなに？
スクリプトの実行時間の壁を突破する方法をまとめました
※トリガーの合計実行時間については対処していません

# GASの制約について

GASは、Googleアカウントの契約状態によって`スクリプトの実行時間`と`トリガーの合計実行時間`に制約があります

https://developers.google.com/apps-script/guides/services/quotas?hl=ja#current_limitations

> 一般ユーザー向け
> トリガーの合計実行時間: 90分 / 日
> スクリプト ランタイム: 6分 / 実行

# 対応方法について

`実行したスクリプト自体が6分後以降に再度実行するように自分を登録する`

6分しか実行できない制限を変更することは、契約体系を変更するしか方法がない :cold_sweat:
再度同じスクリプトを実行した場合に続きから行う事ができれば、ずっと実行されているのと同じ状態になるはず:point_up:

# 導入の考え方について

`自分自身を登録する`のに2つの考え方があります

- 実行してから6分経過しそうな時、登録をする
- 実行したら、とりあえず登録をする。処理が完了したら登録を削除する

いろいろ試した結果からですが、後者の実装を記載したいと思います

理由になりますが、`6分経過しそうか`の判定を至る所でしていたのですが、ちょっと重たい処理があった時にタイミングによっては登録されない場合がありました
後者の場合は、かならず再実行されるを前提にしているので前者の問題を解消できました

# 実装方法について

## トリガーの作成メソッド

時間ベースのトリガーを作成します
引数には、実行したいメソッド名を指定します

```javascript
ScriptApp.newTrigger('myFunction')
    .timeBased()
    .after(6 * 60 * 1000) // 6分後
    .create();
```

https://developers.google.com/apps-script/reference/script/trigger-builder?hl=ja#timeBased()

https://developers.google.com/apps-script/reference/script/clock-trigger-builder?hl=ja

## トリガーの削除メソッド

指定されたトリガーを削除して、実行しないようにします

```javascript
// Deletes all triggers in the current project.
var triggers = ScriptApp.getProjectTriggers();

for (var i = 0; i < triggers.length; i++) {
  ScriptApp.deleteTrigger(triggers[i]);
}
```

https://developers.google.com/apps-script/reference/script/script-app?hl=ja#deleteTrigger(Trigger)

### サンプルコード

トリガーを登録して、登録したトリガーを削除するサンプルコードを以下に記述します

```javascript
function myFunction() {
  // 6分後に起動するトリガーを登録
  ScriptApp.newTrigger('myFunction')
    .timeBased()
    .after(6 * 60 * 1000) // 6分後
    .create();
  Logger.log('トリガーを登録しました');

  // 重たい処理
  Logger.log('重たい処理を実行中……');
  
  // トリガーの削除
  const triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    Logger.log('トリガーで実行するメソッド名: %s', triggers[i].getHandlerFunction());
    ScriptApp.deleteTrigger(triggers[i]);
  }
}
```

#### 実行結果のログ

![log.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266407/f3c176c6-8b3b-4ed1-11fd-e87158f08145.png)

すぐに削除されてしまいますが、項目`トリガー`から６分後に起動するトリガーが登録されたかを確認することができます

![trigger.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266407/f2f93b20-bd7c-8282-10bd-3e24c30c795c.png)


これで6分経過しても再実行されるスクリプトが完成しました :clap: 

# 保存する方法について

次に途中から処理を再実行するために途中まで進めた事を保存する必要があります

https://developers.google.com/apps-script/reference/properties?hl=ja

> このサービスを使用すると、スクリプトは 1 つのスクリプト、1 つのスクリプトの 1 人のユーザー、エディタ アドオンが使用されている 1 つのドキュメントをスコープとする Key-Value ペアとして文字列を保存できます。
> 各タイプのプロパティをどのような場合に使用するかについて詳しくは、プロパティ サービスのガイドをご覧ください。

## プロパティにデータ保存メソッド

GAS単体でも保存できるようにするため、スクリプトプロパティを利用します

```javascript
try {
  // Set a property in each of the three property stores.
  const scriptProperties = PropertiesService.getScriptProperties();

  scriptProperties.setProperty('SERVER_URL', 'http://www.example.com/');
} catch (err) {
  // TODO (developer) - Handle exception
  console.log('Failed with error %s', err.message);
}
```

### サンプルコード

実行するたびに、プロパティ名`STEP_COUNT`をインクリメントします

```javascript
function myFunction() {
  const scriptProperties = PropertiesService.getScriptProperties();

  // 重たい処理
  Logger.log('重たい処理を実行中……');

  try {
    const count = scriptProperties.getProperty('STEP_COUNT');
    scriptProperties.setProperty('STEP_COUNT', parseInt(count) + 1);
  } catch (err) {
    // TODO (developer) - Handle exception
    console.log('Failed with error %s', err.message);
  }
}
```

![property.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266407/7208f6a1-2c5d-2966-8f32-5c11b630e9e9.png)


# ２つを組み合わせたサンプルコード

重たい処理のループの開始位置をプロパティから取得して、途中から始まるようにしました

```javascript
function myFunction() {
  const scriptProperties = PropertiesService.getScriptProperties();

  // 6分後に起動するトリガーを登録
  ScriptApp.newTrigger('myFunction')
    .timeBased()
    .after(6 * 60 * 1000) // 6分後
    .create();
  Logger.log('トリガーを登録しました');

  // 重たい処理
  Logger.log('重たい処理を実行中……');
  for (var i = scriptProperties.getProperty('STEP_COUNT'); i < 100; i++) {
    Logger.log('カウント: %s', scriptProperties.getProperty('STEP_COUNT'));
    scriptProperties.setProperty('STEP_COUNT', parseInt(scriptProperties.getProperty('STEP_COUNT')) + 1);
  }

  if (scriptProperties.getProperty('STEP_COUNT') >= 100) {
    scriptProperties.setProperty('STEP_COUNT', 0);

    // トリガーの削除
    const triggers = ScriptApp.getProjectTriggers();
    for (var i = 0; i < triggers.length; i++) {
      Logger.log('トリガーで実行するメソッド名: %s', triggers[i].getHandlerFunction());
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}
```

# まとめ

プロパティを使って処理しているシートの行番号を保持したりすれば、GASでもバッチ処理的な事を実現できると思います
ただし、アカウント単位で１日のトリガーの合計実行時間が決まっているので、１つのGASが専有しないように気をつける必要があります

楽しいGASライフを:relaxed: