GASの「起動時間の最大値を超えました」の壁を超えてみる!!!

 

新玉ねぎっておいしいですよね。甘くって。
ただ、生で食べる時は、まあまあ薄くきらないと苦くって悲しくなりました。

今回は、みんな大好きGASの
「最大起動時間の壁を超える!」
の巻です。

 

GASの起動時間の最大値は?

リファレンスより、

「Consumer(多分無課金の人)のScript runtime」は 6minのようですね。

 

つまり、

一回の処理が6分を超えると、
「起動時間の最大値を超えました」
と表示され処理が途中で終わってしまいます。
(そもそも、6分以上の処理させんなよって感じなんですが、、、)

ググったら、回避方法を書いてくれてる方がおり、とても助かりました。
おかげで、わたしのバカ長い処理も何回かに分けて、
最後まで実行完了することができるようになりました。

参考:http://direction-note.com/gas-beyond-execution-time/

やり方

処理が5分を超える場合は、
トリガーを設定して、1分後に処理を続きからするようにする。

 

①for文のカウントなどを保持し、正しく続きから処理を再開するために、
GASのスクリプトプロパティでデータを保持させます。

スクリプトのプロパティとは?

ファイル > プロジェクトのプロパティ > スクリプトのプロパティ
にあり、キーバリューでデータを保持できます。

スクリプトのプロパティ

↑ キー:nowRow、バリュー:10のデータを保持させている。
この画面の「行を追加」ボタンよりデータを追加/削除することが可能。

 

GASにて追加することも可能

let properties = PropertiesService.getScriptProperties();

//スクリプトプロパティの登録
properties.setProperty(key, value);

//スクリプトプロパティのバリューを取得
properties.getProperty(key); //return value

 

 

②最大処理時間を超える前の5分で処理を中断し、
1分後にトリガーを設定する。

let properties = PropertiesService.getScriptProperties();
let targetArray = ['foo', 'bar', 'baz'....];  //この配列が膨大量の時に有効

/* メイン */
function mainSort() {  
  ...
  setOutputSheet();
}

/* 5分以上かかる可能性のある処理 */
function setOutputSheet(){
  let startTime = new Date();
  //スクリプトプロパティにトリガーIDを保存するときに使用するkey名
  let triggerKey = "trigger";  
  ...
  for(let i = 0; i < targetArray.length; i++){
    //開始時刻(startTime)と現時点の処理時点の時間を比較する 
    let diff = parseInt((new Date() - startTime) / (1000 * 60));
    if(diff >= 5){
    ...
      //トリガー(1分後)を登録する
      setTrigger(triggerKey, "mainSort");
      return;
    }

    //todo 5分以上かかる可能性のある処理
  }   
  
  //全て実行終えたらトリガー不要なためを削除する
  deleteTrigger(triggerKey);
}


/**
 *指定したkeyに保存されているトリガーIDを使って、トリガーを削除する
 *
 * @param string triggerKey
 * @return void
 */
function deleteTrigger(triggerKey) {
  let triggerId = properties.getProperty(triggerKey);
  if(!triggerId) return;
  
  ScriptApp.getProjectTriggers().filter(function(trigger){
    return trigger.getUniqueId() == triggerId;
  })
  .forEach(function(trigger) {
      ScriptApp.deleteTrigger(trigger);
  });
  properties.deleteProperty(triggerKey);
}

/**
 * トリガーを発行する
 *
 * @param string triggerKey
 * @param string funcName
 * @return void
 */
function setTrigger(triggerKey, funcName){
  //既に同名で保存しているトリガーがあったら削除
  deleteTrigger(triggerKey);
  
  //1分後にトリガーを登録する
  let date = new Date();
  date.setMinutes(date.getMinutes() + 1);
  let triggerId = ScriptApp.newTrigger(funcName).timeBased().at(date).create().getUniqueId();
  Logger.log('setTrigger function_name "%s".', funcName);
  
  //あとでトリガーを削除するために「スクリプトのプロパティ」にトリガーIDを保存しておく
  properties.setProperty(triggerKey, triggerId);
}

 

作ってみたやつ

①②を組み合わせると、最大起動時間の壁を超えれるということです!
作ってみた全体はこんな感じです。
※このコード自体の処理は5分超えなかったのでsleep()とかして試してましたっw

 

こんな感じの横並びを、、、

縦にソートします。

 

プレーヤーが日々追加されることを想定して、
一年の日数 × 最終的なプレーヤー人数分で縦に並ぶように作ってます。

 

コード全体(テキトーなので参考程度で! )

let properties = PropertiesService.getScriptProperties();

//★メイン
function mainSort() {
  
  //プレーヤーマスター配列を作成
  let playerMaster = getPlayerMaster();
  let playerCount = Object.keys(playerMaster).length
  
  //日付の配列を取得
  let dataMaster = getDataMaster();
  let dataCount = Object.keys(dataMaster).length
  
  //日別結果データの取得
  let DayDate = getDayDate(playerCount, dataCount);
  
  //出力用シート(outputSheet)に日別結果の縦版の値を出力する。
  setOutputSheet(playerMaster, dataMaster, DayDate);
}

/**
 * 「No」「プレーヤー」を配列で取得する
 * 
 * @return arrShopMaster = {
 *   1: 'リュウ',
 *   2: 'ケン'
 * };
 */
function getPlayerMaster() {
  let ABcolumnAll = inputSheet.getRange('A2:B101').getValues();
  let LastRow = ABcolumnAll.filter(String).length;
  let arrPlayerMaster = [];
  
  //「-」を除く
  for(let i = 0; i < LastRow; i++){
    if(!ABcolumnAll[i][1].match(/\-/)){
      arrPlayerMaster[ABcolumnAll[i][0]] = ABcolumnAll[i][1]; 
    }  
  }

  return arrPlayerMaster;
}

/**
 * 日付を配列で取得する
 * 
 * @return array arrDataMaster
 */
function getDataMaster() {
  let rowAll = inputSheet.getRange('C1:ND1').getValues();
  let arrDataMaster = [];
  
  for(let i = 0; i < yearSum; i++){
    arrDataMaster[i] = rowAll[0][i];  
  }

  return arrDataMaster;
}

/**
 * 日付をキーにその日の結果を格納する。
 *
 * @param intger playerCount
 * @param intger dataCount
 * @return array arrRet
 */
function getDayDate(playerCount, dataCount)
{
  let col = inputSheet.getRange('C2:NQ'+(playerCount+1)).getValues();

  let arrRet = [];
  for(let i = 0; i < playerCount; i++){
    arrRet[i] = [];
    for(let j = 0; j < dataCount; j++){
      arrRet[i][j] = col[i][j];
    }
  }
  return arrRet;
}

/**
 * 5分以上かかる可能性のある処理
 *
 * @return void
 */
function setOutputSheet(playerMaster, dataMaster, DayDate){
  //処理の開始時刻を取得
  let startTime = new Date();
  //sleep(300000); //意図的に(300秒/60秒=)5分スリープさせる
  
  //途中経過保存用の変数
  let rowKey = "nowRow";   //何行目まで処理したかを保存するときに使用するkey
  let dateKey = "nowDate";  //yearSumが何行目まで処理したかを保存するkey
  let playerCountKey = "nowPlayerCount"; //lastRowが何行目まで処理したかを保存するkey
  let triggerKey = "trigger";  //トリガーIDを保存するときに使用するkey
  
  let nowRow = parseInt(properties.getProperty(rowKey));
  if(!nowRow) nowRow = 1;  //初回処理の時
  
  let lastRow = Object.keys(DayDate).length;

  let now_i = 0;
  let nowDate = parseInt(properties.getProperty(dateKey));
  if(nowDate){ 
    let now_i = nowDate;
    properties.deleteProperty(dateKey);
  }
  for(let i = now_i; i < yearSum; i++){
    let date = dataMaster[i];
    
    let now_j = 0;
    let nowPlayerCount = parseInt(properties.getProperty(playerCountKey));
    if(nowPlayerCount){ 
      let now_j = nowPlayerCount;
      properties.deleteProperty(playerCountKey);
    }
    for(let j = now_j; j < lastRow; j++) { 
        //開始時刻(startTime)と現時点の処理時点の時間を比較する
        let diff = parseInt((new Date() - startTime) / (1000 * 60));     
        if(diff >= 5){
        //何行まで処理したかなどを「スクリプトのプリロパティ」に保存する
        properties.setProperty(rowKey, nowRow);
        properties.setProperty(dateKey, i);
        properties.setProperty(playerCountKey, j);
        //トリガー(1分後)を登録する
        setTrigger(triggerKey, "mainSort");
        return;
      }

      outputSheet.getRange(nowRow, 1).setValue(date);
      outputSheet.getRange(nowRow, 2).setValue(playerMaster[j+1]);
      outputSheet.getRange(nowRow, 3).setValue(DayDate[j][i]);
      nowRow++;
    }
  }
  
  //全て実行終えたらトリガーや何行目まで実行したかなどのデータは
  //不要なためを削除する
  deleteTrigger(triggerKey);
  properties.deleteProperty(rowKey);
  properties.deleteProperty(dateKey);
  properties.deleteProperty(playerCountKey);
}


/**
 * 指定したkeyに保存されているトリガーIDを使って、トリガーを削除する
 *
 * @param string triggerKey
 * @return void
 */
function deleteTrigger(triggerKey) {
  let triggerId = properties.getProperty(triggerKey);
  if(!triggerId) return;
  
  ScriptApp.getProjectTriggers().filter(function(trigger){
    return trigger.getUniqueId() == triggerId;
  })
  .forEach(function(trigger) {
      ScriptApp.deleteTrigger(trigger);
  });
  properties.deleteProperty(triggerKey);
}

/**
 * トリガーを発行。トリガーを発行した箇所から
 *
 * @param string triggerKey
 * @param string funcName
 * @return void
 */
function setTrigger(triggerKey, funcName){
  //既に同じ名前で保存しているトリガーがあったら削除
  deleteTrigger(triggerKey);
  
  let date = new Date();
  date.setMinutes(date.getMinutes() + 1); //1分後に再実行
  
  let triggerId = ScriptApp.newTrigger(funcName).timeBased().at(date).create().getUniqueId();
  Logger.log('setTrigger function_name "%s".', funcName);
  
  //あとでトリガーを削除するために「スクリプトのプロパティ」にトリガーIDを保存しておくproperties.setProperty(triggerKey, triggerId);
}