LoginSignup
1
1

実行時間制限を抜け出そう! GoogleAppsScript(GAS)の並列処理化のススメ その2

Posted at

既知の問題(再掲)

前回の投稿

にて、以下2つの問題がありました。
今回は、この2つを解決しようと思います。

問題1

UrlFetchApp.fetchAllメソッドに引き渡すリクエスト数が多いと、以下のようなエラーが起こります。
2023-12-21_09h33_30.png

問題2

検証の結果、同時実行数が多いと、レスポンスの一部に以下のようなHTMLが返ってきます。
2023-12-21_15h26_57.png

問題1の解決策

この問題の解決策としては、リクエストを100個ずつに分割して、fetchAll ()メソッドに渡す方法が良いでしょう。
と、いうことで、chunkRequest()関数を作ることにします。
この関数は、リクエスト配列を指定された数で分割し、分割後のリクエスト配列を要素とした配列を返します。

/**
 * リクエスト配列を指定された数で分割し、分割されたリクエスト配列を要素とした二次元配列を返す。
 * @param {object[]} requests - 分割対象のリクエスト配列
 * @param {number} chunkSize - 分割するサイズ
 * @return {object[][]} - 分割されたリクエスト配列を要素とした二次元配列
*/

function chunkRequest(requests,chunkSize) {
    let chunkedRequests = [];

    // whileループで元の配列から要素を取り出す。
    while(requests.length > 0) {
        chunkedRequests.push(requests.splice(0, chunkSize));
    }

    return chunkedRequests;
}

問題2の解決策

この問題の解決策としては、再帰関数を利用すると良いでしょう。
と、いうことで、fetchParaRequests()関数を作ることにします。
この関数は、UrlFetchApp.fetchAll()メソッドを実行し、レスポンス内に「同時に実行中のスクリプト」が含まれていたら、再実行用のリクエスト配列に、エラーが出たリクエストを追加します。

/**
 * リクエスト配列を、fetchAll()メソッドを実行し、レスポンス内に、「同時実行数が多い」という
 * エラーが出たリクエストは、再実行用リクエスト配列に追加し、再実行用リクエスト配列を引渡し
 * 再帰実行する。
 * その他のレスポンスは、レスポンスの配列に追加され、関数終了時に返す。
 * 
 * @param {object[]} requests - 並列実行させるリクエストの配列
 * @param {number} count - 再帰実行カウント用。呼び出し時は、0を引き渡す。
 * @return {object[]} - レスポンスを要素とした配列
*/

function fetchParaRequests(requests,count) {
    let reRequests = [];
    let reses = [];
    Logger.log('再帰実行カウント: ' + count);
    count++;

    const res = UrlFetchApp.fetchAll(requests);

    for (let i = 0; i < res.length; i++) {     
        if (res[i].getContentText().includes('同時に実行中のスクリプト')) {
            //レスポンス内に「同時に実行中のスクリプト」という文字が含まれていた場合(=同時実行数エラー)
            Logger.log('再実行 要素No.' + i);
            reRequests.push(requests[i]);//該当リクエストを再実行用配列に追加
        } else {
            reses.push(res[i]);//結果をレスポンス配列に追加
        }
    }

    if (reRequests.length != 0) {
        //再実行用配列の要素数が0でない(=再実行対象のリクエストがある。)
        Utilities.sleep(10000);//10秒休んで
        //再帰呼び出しする。再帰呼び出しの結果を reses に追加
        reses = reses.concat(fetchParaRequests(reRequests,count)); 
    }
    return reses;
}

既知の問題に対応したメインルーチン

前回の投稿のコードを、書き換えます。
今回、既知の問題対応のため、chunkRequest()関数と、再帰関数のfetchParaRequests()関数を新たに作ったので、それを処理に組み込みましょう。
さらに、実行数の制限がなくなったので、30個の並列実行を、300個の並列実行にしてみます。
リクエストで送信する「age」が、最大334という、ありえない数になってしまいますが...(汗

function fetchPostPara300Req() {
    //ウェブアプリを300個並列実行するサンプル
    const fetchUrl='[ウェブアプリのURL]';
    //DriveApp.getRootFolder().getName(); //API実行時に401エラーが出た場合はこれを実行すると良さそう 
    //APIを全体公開出来ない場合はトークンが必要。トークンをfetch直前に取得
    //const token = ScriptApp.getOAuthToken(); //APIを全体公開出来ない場合この行が必要。

    //リクエスト配列の生成
    let requests = [];

    for (let i = 0; i < 300; i++) {
        //ウェブアプリに引き渡すデータ部分を生成。
        let data = {
            'name': 'Bob Smith',
            'age': 35 + i, //すべて同じリクエストだと寂しいので少し変える。
            'hobby' : '他人のソースコードのダメ出し。// : ? . % a',
            'pets': ['fido', 'fluffy']
        };

        //リクエストのJavaScriptオブジェクトを作成
        let request = {
            'url': fetchUrl,
            'method' : 'post',
            'headers': {
                'Content-Type': 'application/json'
                //,'Authorization': `Bearer ${token}` //APIを全体公開出来ない場合この認証が必要
            },
            'payload' : JSON.stringify(data) //ウェブアプリに引き渡すデータ部分をJSON変換し、payloadに配置。
        };

        //リクエスト配列にリクエストのJavaScriptオブジェクト追加
        requests.push(request);
    }

    //100個ずつのリクエストに分割
    const chunkedRequests = chunkRequest(requests,100);
    //100個ずつリクエストを送信
    for (let j = 0; j < chunkedRequests.length; j++) {
        const res = fetchParaRequests(chunkedRequests[j],0);
        //レスポンスの確認
        for (let k = 0; k < res.length; k++) {
            Logger.log(res[k].getContentText());
        }
    }
}
1
1
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
1