Google Apps Script で並列処理をしたい 2

# 背景と概要

2019 年 4 月 8 日の Google Apps Script Project の[アップデート](https://developers.google.com/apps-script/guides/cloud-platform-projects)により、これ以降に作成した GAS プロジェクトで Apps Script API を使用するためには Cloud Platform Project と GAS プロジェクトをリンクする必要が発生し、これにより設定が少し複雑になりました。このため、ここでは設定が簡単な Web Apps を使った並列処理を提案させていただきます。これに関する問い合わせが多かったこともあり、こちらでもご紹介させていただきます。

以前紹介させていただきました Google Apps Script API を使用した並列処理は[こちら](https://qiita.com/tanaike/items/f6da88f0f769f6096c37)でご覧頂けます。

# fetchAll について

初めに、 UrlFetch サービスに追加された[fetchAll メソッド](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app)について簡単に記載させていただきます。fetchAll メソッドの使い方を見ても動作状態などは書かれていませんでしたので、実際に調べてみようと次のような実験を行いました。

- [Web Apps](https://developers.google.com/apps-script/guides/web) を 5 つデプロイ
  - 動作は 5 秒のスリープ
- デプロイした 5 つの Web Apps へのコールをリクエストとして fetchAll メソッドで実行

この結果、`UrlFetchApp.fetchAll()`は総実行時間は 5 秒程度だったのに対して、従来の`UrlFetchApp.fetch()`では総実行時間は 25 秒程度でした。これは fetchAll メソッドが非同期で実行していることを意味します。この結果を得たことで並列処理に応用してみようと考えました。

詳細、並びに使用したスクリプトは[こちらの gist](https://gist.github.com/tanaikech/c0f383034045ab63c19604139ecb0728)でご覧いただけます。

# GAS を用いた並列処理

fetchAll メソッドが非同期で動作することが確認できたことで、この fetchAll メソッドと ここでは Web Apps を利用してネイティブ GAS による並列処理を行うために Google Apps Script 用のライブラリとして RunAll([GitHub](https://github.com/tanaikech/RunAll))をアップデートしました。スクリプト自体はシンプルにも関わらずライブラリとして作成した理由は、クライアント側のスコープの自由度を維持したいと考えたためです。この意味の詳細は[こちら](https://qiita.com/tanaike/items/6488f755f742c9031e73)をご覧ください。

# RunAll

ライブラリ自体は、Google Apps Script API を使った並列処理のためのメソッドと、Web Apps を使った並列処理のメソッドの 2 つを用意しています。ここでは、後者の Web Apps を使った並列処理について紹介させていただきます。

RunAll ([https://github.com/tanaikech/RunAll](https://github.com/tanaikech/RunAll))を使用するまでの手順は下記の通りです。

### 1. スタンドアロンスクリプトタイプあるいはバウンドスクリプトタイプのプロジェクトを用意する

こちらへアクセスして[https://script.google.com/](https://script.google.com/)新規スクリプト（スタンドアロンタイプ）を作成することも可能です。

### 2. ライブラリ(RunAll)をインストールする

インストール方法は[こちら](https://developers.google.com/apps-script/guides/libraries)です。

- ライブラリのプロジェクトキーは **`1FWYhQFhL7UIAZJn-FR3TlcHvXwHPJc2HwI4vtmNUAQv2OybGe-S97Lal`** です。

### 3. Web Apps をデプロイする

1. スクリプトエディタ上で「公開」 -> 「ウェブアプリケーションとして導入」を選択してダイアログを開く。
1. 「次のユーザーとしてアプリケーションを実行:」を「自分」、「アプリケーションにアクセスできるユーザー:」を「自分だけ」に設定する。
   - この設定では、オーナー本人からのアクセスのみです。他者からのアクセスはできません。
   - 「プロジェクト バージョン:」へ適当な文字列を入れてバージョンに説明を付けることも可能です。入れなくても問題ありません。
1. 「承認が必要です」というダイアログが表示されると、許可を確認し、使用するスコープを承認してください。
   - ここで使用するスコープは次の通りです。
     - `https://www.googleapis.com/auth/drive.readonly` : Web Apps へのアクセスに使用
     - `https://www.googleapis.com/auth/script.external_request` : 各エンドポイントへのアクセスに使用
1. 承認が完了すると、承認のためのダイアログが閉じて「現在のウェブ アプリケーションの URL」が表示されたダイアログが出ます。ここで、「ウェブ アプリケーションで最新のコードをテストします。」の「**最新のコード**」の URL をコピーしておきます。
   - URL はこのようなものです。`https://script.google.com/macros/s/###/dev`
1. OK ボタンを押して設定を終了します。

この設定では Web Apps をデプロイしたプロジェクト内のスクリプトから、デプロイした Web Apps へアクセスする流れです。

### 4. サンプルスクリプトをコピーペーストする

下記のスクリプトを今開いている Web Apps をデプロイしたスクリプトエディタへ入れて下さい。このとき、スクリプト内の`url`へ先ほどコピーした`https://script.google.com/macros/s/###/dev`をセットしてください。

```javascript
function myFunction(e) {
  Utilities.sleep(1000);
  return e;
}

function doPost(e) {
  return RunAll.RunFunctionsByDoPost(this, e);
}

function main() {
  var url = "https://script.google.com/macros/s/###/dev"; // Here, please set the URL of Web Apps.
  var token = ScriptApp.getOAuthToken();
  var resource = [
    {
      functionName: "myFunction",
      arguments: "sample parameter 1",
      webAppsURL: url,
      accessToken: token
    },
    {
      functionName: "myFunction",
      arguments: "sample parameter 2",
      webAppsURL: url,
      accessToken: token
    },
    {
      functionName: "myFunction",
      arguments: "sample parameter 3",
      webAppsURL: url,
      accessToken: token
    }
  ];
  var res = RunAll.DoWebApps(resource);
  res.forEach(function(r) {
    Logger.log(r.getContentText());
  });
}
```

実行した結果は下記の通りです。

```json
{"FunctionName":"myFunction","Arguments":"sample parameter 1","Result":"sample parameter 1"}

{"FunctionName":"myFunction","Arguments":"sample parameter 2","Result":"sample parameter 2"}

{"FunctionName":"myFunction","Arguments":"sample parameter 3","Result":"sample parameter 3"}
```

- `main()`を実行することで非同期で`myFunction()`が 3 回実行されます。
- 実際に実行してみると、同期実行の場合は少なくとも 3 秒以上経過しますが、この方法では 1.6 秒程度でした。

# 制限

- Web Apps を使用していますので、この仕様により、与える引数と返される値は文字型である必要があります。これが制限の一つです。

- 一度に実行できる最大ワーカー数は 30 です。ただし、30 丁度で実行させると、時々 1 あるいは 2 つエラーが返されますので、28 を最大値にする方が安心かもしれません。[参考](https://stackoverflow.com/a/50033976/7108653)
  - [Quotas for Google Services](https://developers.google.com/apps-script/guides/services/quotas#current_limitations)でも Simultaneous executions は 30 になっています。

# 参考

- [Taking advantage of Web Apps with Google Apps Script](https://github.com/tanaikech/taking-advantage-of-Web-Apps-with-google-apps-script)
- [RunAll](https://github.com/tanaikech/RunAll)
- [Running Functions by Specifying Function Names with Web Apps for Google Apps Script](https://gist.github.com/tanaikech/b2e0325a28efe7c609e4688ec22ee22c)