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秒休むので、きれいな階段状になります。
今回のコードで同時実行を可能にしたもの
前回のように階段状にならず、10個の塊、1秒の空白、10個の塊、1秒の空白、6個の塊ときれいに出ています。
ログ
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は怖くなくなりました。