jQueryの非同期メソッドにQPSの制限を加える

jQueryのDeferredを使って非同期処理にQPS制限してみます

#はじめに
[前回の記事](https://qiita.com/kanaxx/items/9102836292f219bf7ffc)では、jQueryのDeferredを使って、暴れん坊になりがちな非同期処理を1つずつ実行するようにしました。いくらなんでも1つずつ順番に呼び出すのは効率が悪かろうということで、もうちょっと改良してみます。Query Per Second(QPS)の考え方を導入し、1秒間にn回まで実行できる形に仕上げます。

実用的か全くわかりません。完全に趣味の世界に入りました。

#コード
いきなりコード
いつも通りjavascriptのコードです。

```javascript
void((function(f){
    if(window.jQuery && jQuery().jquery > '3.2') {
      f(jQuery);
    }else{
      var script = document.createElement('script');
      script.src = '//code.jquery.com/jquery-3.2.1.min.js';
  
      script.onload = function(){
        var $ = jQuery.noConflict(true);
        f($);
      };
      document.body.appendChild(script);
    }
  })(
    function($, undefined){
  
      //query per secondなので1
      var interval=1;
      var data = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q', 'r','s','t','u','v','w','x','y','z'];
      var answer = [];

      //同時実行数は10
      var concurrent_limit = 10;
      //並列処理の回数(データ数を同時実行数で割る)
      var api_calls = Math.ceil(data.length/concurrent_limit);

      //一番最初のDeferred。引き止め役
      var root = $.Deferred();
      var deferred = root.then(function(){return wait(3)});

      for( i = 0; i < api_calls ; i++){
        deferred = deferred
        .then( 
          function(counter){ 
            return function(){
              console.log('-API set [%s]', counter);
              var deferred_list=[];
              for(var j=0; j<concurrent_limit; j++){
                var df = api(data[concurrent_limit*counter+j]);
                if( df != null){
                  deferred_list.push(df);
                }
              }
              deferred_list.push(wait(interval));
              return $.when.apply($,deferred_list);
            }
          }(i)
        )
        .done(function(){
          //apiとwaitが終わったら答えが受け取れる部分
          console.log(arguments);
          for( var a=0; a<concurrent_limit; a++){
            answer.push(arguments[a]);
          }
        });
      
      }
      deferred.then(function(){
        //全部終わったら実行する処理
        console.log('final answer is');
        console.log(answer);
      });

      //引き止め役をresolveしてあげる
      root.resolve();
      console.log('--end function--');
  
      //この先関数 =======================================

      //ただ止めるだけの関数(引数で指定された秒数止めるように変更)
      function wait(second) {
        var df = new $.Deferred;
        console.log('wait() :setTimeout with %s', second);
        setTimeout(function (){
          df.resolve();
          console.log('wait() :resolve after %s second', second);
        }, second * 1000);
        return df.promise();
      }

      //高負荷にさせるのを避けたい処理
      //必要なものは関数内に閉じる。引数を受け取って、結果はresoleで返す
      function api(mydata){
        if( mydata == undefined ){
          return null;
        }
        console.log('call API [%s]', mydata);
        var d = $.Deferred();

        $.ajax({
            url: 'https://res.cloudinary.com/kanaxx/raw/upload/v1532448755/static/sample.json',
            type: "GET",
            dataType: "json",
            data: "param=" + encodeURI(mydata),
        }).done(function (response, textStatus, jqXHR) {
            console.log('done API [%s]', mydata);
            d.resolve({'param':mydata.toUpperCase(), 'result':true});
        }).fail(function (jqXHR, textStatus, errorThrown) {
          console.log(errorThrown);
            console.log('fail API [%s]', mydata);
            d.resolve({'param':mydata.toUpperCase(), 'result':false});
        });
        return d.promise();
  
      }
    }
  ))
  
```

前回の失敗例から学んだことを活かします。大事なことは

- thenは、関数が欲しい
- whenは、関数を実行したあとの複数のDeferred(Promise)が欲しい
 
です

#説明
やっていることは、同時実行数を10として合計26回APIを実行しています。
複数のdeferredを待ち合わせするにはwhenが必要なので、deferred_listの配列にdeferredを詰め込んでいます。deferredの数が予測不可能になってしまったので、whenを直接呼び出せなくなりました。このため、```$.when.apply($,deferred_list);``` で関数を呼び出しする形に変えました。
QueryPerSecondを正確に制御することはできないのですが、```deferred_list```に1秒待ったらresolveされるdeferredを余計に詰め込んであげることで、whenの待ち合わせは少なくとも1秒は掛かるようにしています。

10回のAPIの同時実行に1秒以上かかる場合には、1セットが終わったところで次のセットへ行きます。前回のように必ず1秒待つような形ではありません。10回呼び出ししても1秒以内で終わってしまう場合には、deferred_listに足したwaitのdeferredが生きてきて、1秒待たされます。


#実行結果

##Dev Toolのネットワーク

こちらは、前回のコードでの実行シーケンス
1回実行するごとに1秒休むので、きれいな階段状になります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/d7565378-1d66-fe18-6f33-8cbea9c8f70c.png)

今回のコードで同時実行を可能にしたもの
前回のように階段状にならず、10個の塊、1秒の空白、10個の塊、1秒の空白、6個の塊ときれいに出ています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/428a38b1-d1ec-52c1-426c-c65a2e861d00.png)



##ログ

```
01:12:59.833 VM125:1 --end function--
01:12:59.834 VM125:1 wait() :setTimeout with 3
01:13:02.837 VM125:1 wait() :resolve after 3 second
01:13:02.840 VM125:1 -API set [0]
01:13:02.842 VM125:1 call API [a]
01:13:02.852 VM125:1 call API [b]
01:13:02.872 VM125:1 call API [c]
01:13:02.881 VM125:1 call API [d]
01:13:02.884 VM125:1 call API [e]
01:13:02.889 VM125:1 call API [f]
01:13:02.893 VM125:1 call API [g]
01:13:02.896 VM125:1 call API [h]
01:13:02.900 VM125:1 call API [i]
01:13:02.906 VM125:1 call API [j]
01:13:02.909 VM125:1 wait() :setTimeout with 1
01:13:02.916 VM125:1 done API [a]
01:13:02.921 VM125:1 done API [b]
01:13:02.925 VM125:1 done API [c]
01:13:02.927 VM125:1 done API [d]
01:13:02.929 VM125:1 done API [e]
01:13:02.931 VM125:1 done API [f]
01:13:02.934 VM125:1 done API [g]
01:13:02.937 VM125:1 done API [h]
01:13:02.939 VM125:1 done API [i]
01:13:02.942 VM125:1 done API [j]
01:13:03.912 VM125:1 wait() :resolve after 1 second
01:13:03.918 VM125:1 Arguments(11) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, undefined, callee: ƒ, Symbol(Symbol.iterator): ƒ]
01:13:03.922 VM125:1 -API set [1]
01:13:03.925 VM125:1 call API [k]
01:13:03.931 VM125:1 call API [l]
01:13:03.948 VM125:1 call API [m]
01:13:03.958 VM125:1 call API [n]
01:13:03.963 VM125:1 call API [o]
01:13:03.966 VM125:1 call API [p]
01:13:03.971 VM125:1 call API [q]
01:13:03.975 VM125:1 call API [r]
01:13:03.979 VM125:1 call API [s]
01:13:03.983 VM125:1 call API [t]
01:13:03.989 VM125:1 wait() :setTimeout with 1
01:13:03.993 VM125:1 done API [k]
01:13:03.996 VM125:1 done API [l]
01:13:03.999 VM125:1 done API [m]
01:13:04.002 VM125:1 done API [n]
01:13:04.004 VM125:1 done API [o]
01:13:04.007 VM125:1 done API [p]
01:13:04.009 VM125:1 done API [q]
01:13:04.012 VM125:1 done API [r]
01:13:04.014 VM125:1 done API [s]
01:13:04.018 VM125:1 done API [t]
01:13:04.993 VM125:1 wait() :resolve after 1 second
01:13:04.998 VM125:1 Arguments(11) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, undefined, callee: ƒ, Symbol(Symbol.iterator): ƒ]
01:13:05.002 VM125:1 -API set [2]
01:13:05.006 VM125:1 call API [u]
01:13:05.012 VM125:1 call API [v]
01:13:05.027 VM125:1 call API [w]
01:13:05.043 VM125:1 call API [x]
01:13:05.048 VM125:1 call API [y]
01:13:05.053 VM125:1 call API [z]
01:13:05.057 VM125:1 wait() :setTimeout with 1
01:13:05.063 VM125:1 done API [u]
01:13:05.067 VM125:1 done API [v]
01:13:05.070 VM125:1 done API [w]
01:13:05.072 VM125:1 done API [x]
01:13:05.076 VM125:1 done API [y]
01:13:05.078 VM125:1 done API [z]
01:13:06.062 VM125:1 wait() :resolve after 1 second
01:13:06.067 VM125:1 Arguments(7) [{…}, {…}, {…}, {…}, {…}, {…}, undefined, callee: ƒ, Symbol(Symbol.iterator): ƒ]
01:13:06.072 VM125:1 final answer is
01:13:06.075 VM125:1 (30) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, undefined, undefined, undefined, undefined]0: {param: "A", result: true}1: {param: "B", result: true}2: {param: "C", result: true}3: {param: "D", result: true}4: {param: "E", result: true}5: {param: "F", result: true}6: {param: "G", result: true}7: {param: "H", result: true}8: {param: "I", result: true}9: {param: "J", result: true}10: {param: "K", result: true}11: {param: "L", result: true}12: {param: "M", result: true}13: {param: "N", result: true}14: {param: "O", result: true}15: {param: "P", result: true}16: {param: "Q", result: true}17: {param: "R", result: true}18: {param: "S", result: true}19: {param: "T", result: true}20: {param: "U", result: true}21: {param: "V", result: true}22: {param: "W", result: true}23: {param: "X", result: true}24: {param: "Y", result: true}25: {param: "Z", result: true}26: undefined27: undefined28: undefined29: undefinedlength: 30__proto__: Array(0)

```

10個セットでAPIの呼び出す部分のログをピックアップしてみると、ちゃんと1秒と少々空いてます。

```
01:13:02.840 VM125:1 -API set [0]
01:13:03.922 VM125:1 -API set [1]
01:13:05.002 VM125:1 -API set [2]
```

実際にAPIを呼び出しているコードは、0.06秒くらいで10回の外部接続を完了させています。暴れん坊の非同期処理です。

```
01:13:02.842 VM125:1 call API [a]
01:13:02.852 VM125:1 call API [b]
01:13:02.872 VM125:1 call API [c]
01:13:02.881 VM125:1 call API [d]
01:13:02.884 VM125:1 call API [e]
01:13:02.889 VM125:1 call API [f]
01:13:02.893 VM125:1 call API [g]
01:13:02.896 VM125:1 call API [h]
01:13:02.900 VM125:1 call API [i]
01:13:02.906 VM125:1 call API [j]
```

API単体の実行速度をログで見てみると

```
01:13:02.842 VM125:1 call API [a]
01:13:02.916 VM125:1 done API [a]
```
aは700ミリ秒

```
01:13:03.948 VM125:1 call API [m]
01:13:03.999 VM125:1 done API [m]
```
mは500ミリ秒

このプログラムを前回の例のように１つずつ順番に実行するように制御すると、500ミリｘ26回＝13秒かかることになります。APIとAPIの間にやさしさのwaitを入れるとさらに時間がかかります。


#まとめ
ダレトクな記事になりましたが、deferred周辺は乗りこなしたかなと思います。これで429 Too Many Requestsは怖くなくなりました。