LoginSignup
1
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-07-26

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

はじめに

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

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

コード

いきなりコード
いつも通り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

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

image.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ミリ秒

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

まとめ

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

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