├── .eslintignore ├── .eslintrc ├── .github └── dependabot.yml ├── .gitignore ├── .travis.yml ├── MIT-License ├── Makefile ├── README.md ├── README_CN.md ├── index.js ├── lib └── bagpipe.js ├── package.json └── test └── bagpipe.test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | *.debug.js 2 | *.min.js 3 | node_modules/* 4 | assets/scripts/lib/* 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [2, 2], 4 | "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], 5 | "linebreak-style": [2, "unix"], 6 | "semi": [2, "always"], 7 | "strict": [2, "global"], 8 | "curly": 2, 9 | "eqeqeq": 2, 10 | "no-eval": 2, 11 | "guard-for-in": 2, 12 | "no-caller": 2, 13 | "no-else-return": 2, 14 | "no-eq-null": 2, 15 | "no-extend-native": 2, 16 | "no-extra-bind": 2, 17 | "no-floating-decimal": 2, 18 | "no-implied-eval": 2, 19 | "no-labels": 2, 20 | "no-with": 2, 21 | "no-loop-func": 1, 22 | "no-native-reassign": 2, 23 | "no-redeclare": [2, {"builtinGlobals": true}], 24 | "no-delete-var": 2, 25 | "no-shadow-restricted-names": 2, 26 | "no-undef-init": 2, 27 | "no-use-before-define": 2, 28 | "no-unused-vars": [2, {"args": "none"}], 29 | "no-undef": 2, 30 | "callback-return": [2, ["callback", "cb", "next"]], 31 | "global-require": 0, 32 | "no-console": 0, 33 | "generator-star-spacing": ["error", "after"], 34 | "require-yield": 0 35 | }, 36 | "env": { 37 | "es6": true, 38 | "node": true, 39 | "browser": true 40 | }, 41 | "globals": { 42 | "describe": true, 43 | "it": true, 44 | "before": true, 45 | "after": true, 46 | "xdescribe": true 47 | }, 48 | "extends": "eslint:recommended" 49 | } 50 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | node_modules 15 | npm-debug.log 16 | coverage 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "7" 5 | -------------------------------------------------------------------------------- /MIT-License: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Jackson Tian 2 | http://weibo.com/shyvo 3 | 4 | The MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/*.js 2 | REPORTER = spec 3 | TIMEOUT = 20000 4 | ISTANBUL = ./node_modules/.bin/istanbul 5 | MOCHA = ./node_modules/mocha/bin/_mocha 6 | COVERALLS = ./node_modules/coveralls/bin/coveralls.js 7 | 8 | test: 9 | @NODE_ENV=test $(MOCHA) -R $(REPORTER) -t $(TIMEOUT) \ 10 | $(MOCHA_OPTS) \ 11 | $(TESTS) 12 | 13 | test-cov: 14 | @$(ISTANBUL) cover --report html $(MOCHA) -- -t $(TIMEOUT) -R spec $(TESTS) 15 | 16 | test-coveralls: 17 | @$(ISTANBUL) cover --report lcovonly $(MOCHA) -- -t $(TIMEOUT) -R spec $(TESTS) 18 | @echo TRAVIS_JOB_ID $(TRAVIS_JOB_ID) 19 | @cat ./coverage/lcov.info | $(COVERALLS) && rm -rf ./coverage 20 | 21 | test-all: test test-coveralls 22 | 23 | .PHONY: test 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bagpipe 2 | ======= 3 | You are the bagpiper. 4 | 5 | - [中文](https://github.com/JacksonTian/bagpipe/blob/master/README_CN.md) 6 | 7 | ## Introduction 8 | It is convenient for us to use asynchrony or concurrency to promote our business speed in Node. However, if the amount of concurrency is too large, our server may not support it, and we'll need to limit its amount. The HTTP module contains [http.Agent](http://nodejs.org/docs/latest/api/http.html#http_class_http_agent) to control the amount of sockets our asynchronous API has packaged (usually) in advance. It is not realistic to change the inner API agent; let’s realize it on our own logical layer. 9 | 10 | ## Installation 11 | 12 | ```sh 13 | $ npm install bagpipe 14 | ``` 15 | 16 | ## API 17 | The APIs exposed by Bagpipe only include constructor and instance methods `push`. 18 | 19 | Under original status, we may execute concurrent calls like this, forming 100 concurrent asynchronous invokes: 20 | 21 | ```js 22 | for (var i = 0; i < 100; i++) { 23 |   async(function () { 24 |     // Asynchronous call 25 |   }); 26 | } 27 | ``` 28 | 29 | If need to limit concurrency, what is your solution? 30 | 31 | Solution from Bagpipe: 32 | 33 | ```js 34 | var Bagpipe = require('bagpipe'); 35 | // Sets the max concurrency as 10 36 | var bagpipe = new Bagpipe(10); 37 | for (var i = 0; i < 100; i++) { 38 | bagpipe.push(async, function () { 39 | // execute asynchronous callback 40 | }); 41 | } 42 | ``` 43 | 44 | Yes. The invoke method only splits method, parameter and callback, then delivers it to bagpipe through `push`. 45 | 46 | How does Bagpipe compare with your anticipated solution? 47 | 48 | ### Options 49 | 50 | - `refuse`, when queue is fulled, bagpipe will refuse the new async call and execute the callback with a `TooMuchAsyncCallError` exception. default `false`. 51 | - `timeout`, setting global ansyn call timeout. If async call doesn't complete in time, will execute the callback with `BagpipeTimeoutError` exception. default `null`. 52 | 53 | ## Principles 54 | Bagpipe delivers invoke into inner queue through `push`. If active invoke amount is less than max concurrent, it will be popped and executed directly, or it will stay in the queue. When an asynchronous invoke ends, a invoke in the head of the queue will be popped and executed, such that assures active asynchronous invoke amount no larger than restricted value. 55 | 56 | When the queue length is larger than 1, Bagpipe object will fire its `full` event, which delivers the queue length value. The value helps to assess business performance. For example: 57 | 58 | ```js 59 | bagpipe.on('full', function (length) { 60 | console.warn(`Button system cannot deal on time, queue jam, current queue length is: ${length}`); 61 | }); 62 | ``` 63 | 64 | If queue length more than limit, you can set the `refuse` option to decide continue in queue or refuse call. The `refuse` default `false`. If set as `true`, the `TooMuchAsyncCallError` exception will pass to callback directly: 65 | 66 | ```js 67 | var bagpipe = new BagPipe(10, { 68 | refuse: true 69 | }); 70 | ``` 71 | 72 | If complete the async call is unexpected, the queue will not balanced. Set the timeout, let the callback executed with the `BagpipeTimeoutError` exception: 73 | 74 | ```js 75 | var bagpipe = new BagPipe(10, { 76 | timeout: 1000 77 | }); 78 | ``` 79 | 80 | ## Module status 81 | The unit testing status: [![Build Status](https://secure.travis-ci.org/JacksonTian/bagpipe.png)](http://travis-ci.org/JacksonTian/bagpipe). 82 | 83 | ## Best Practices 84 | - Ensure that the last parameter of the asynchronous invoke is callback. 85 | - Listen to the `full` event, adding your business performance assessment. 86 | - Current asynchronous method has not supported context yet. Ensure that there is no `this` reference in asynchronous method. If there is `this` reference in asynchronous method, please use `bind` pass into correct context. 87 | - Asynchronous invoke should process method to deal with timeout, it should ensure the invoke will return in a certain time no matter whether the business has been finished or not. 88 | 89 | ## Real case 90 | When you want to traverse file directories, asynchrony can ensure `full` use of IO. You can invoke thousands of file reading easily. But, system file descriptors are limited. If disobedient, read this article again when occurring errors as follows. 91 | 92 | ```js 93 | Error: EMFILE, too many open files 94 | ``` 95 | 96 | Someone may consider dealing it with synchronous method. But, when synchronous, CPU and IO cannot be used concurrently, performance is an indefeasible index under certain condition. You can enjoy concurrent easily, as well as limit concurrent with Bagpipe. 97 | 98 | ```js 99 | var bagpipe = new Bagpipe(10); 100 | 101 | var files = ['Here are many files']; 102 | for (var i = 0; i < files.length; i++) { 103 | // fs.readFile(files[i], 'utf-8', function (err, data) { 104 | bagpipe.push(fs.readFile, files[i], 'utf-8', function (err, data) { 105 | // won’t occur error because of too many file descriptors 106 | // well done 107 | }); 108 | } 109 | ``` 110 | 111 | ## License 112 | Released under the license of [MIT](https://github.com/JacksonTian/bagpipe/blob/master/MIT-License), welcome to enjoy open source. 113 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | Bagpipe(风笛) 2 | ======= 3 | You are the bagpiper. 4 | 5 | - [English](https://github.com/JacksonTian/bagpipe/blob/master/README.md) 6 | 7 | ## 起源 8 | 在Node中我们可以十分方便利用异步和并行来提升我们的业务速度。但是,如果并发量过大,我们的服务器却可能吃不消,我们需要限制并发量。尽管`http`模块自身有[http.Agent](http://nodejs.org/docs/latest/api/http.html#http_class_http_agent)这样的玩意,用于控制socket的数量,但是通常我们的异步API早就封装好了。改动API的内部agent是不现实的,那么我们自己在逻辑层实现吧。 9 | 10 | ## 安装 11 | ``` 12 | npm install bagpipe 13 | ``` 14 | 15 | ## API 16 | `Bagpipe`暴露的API只有构造器和实例方法`push`。 17 | 18 | 在原始状态下,我们执行并发可能是如下这样的,这会形成100个并发异步调用。 19 | 20 | ``` 21 | for (var i = 0; i < 100; i++) { 22 | async(function () { 23 | // 异步调用 24 | }); 25 | } 26 | ``` 27 | 如果需要限制并发,你的方案会是怎样? 28 | 29 | `Bagpipe`的方案是如下这样的: 30 | 31 | ``` 32 | var Bagpipe = require('bagpipe'); 33 | // 设定最大并发数为10 34 | var bagpipe = new Bagpipe(10); 35 | for (var i = 0; i < 100; i++) { 36 | bagpipe.push(async, function () { 37 | // 异步回调执行 38 | }); 39 | } 40 | ``` 41 | 42 | 是的,调用方式仅仅是将方法、参数、回调分拆一下通过`push`交给`bagpipe`即可。 43 | 44 | 这个方案与你预想的方案相比,如何? 45 | 46 | ### 选项 47 | 48 | - `refuse`,当队列填满时,拒绝新到来的异步调用。执行异步调用的回调函数,传递一个`TooMuchAsyncCallError`异常。默认为`false`。 49 | - `timeout`,设置全局异步调用超时时间,经过`push`后执行的异步调用,如果在超时时间内没有返回执行,将会执行异步调用的回调函数,传递一个`BagpipeTimeoutError`异常。默认为`null`不开启。 50 | 51 | ## 原理 52 | `Bagpipe`通过`push`将调用传入内部队列。如果活跃调用小于最大并发数,将会被取出直接执行,反之则继续呆在队列中。当一个异步调用结束的时候,会从队列前取出调用执行。以此来保证异步调用的活跃量不高于限定值。 53 | 54 | 当队列的长度大于1时,Bagpipe对象将会触发它的`full`事件,该事件传递队列长度值。该值有助于评估业务性能参数。示例如下: 55 | 56 | ``` 57 | bagpipe.on('full', function (length) { 58 | console.warn('底层系统处理不能及时完成,排队中,目前队列长度为:' + length); 59 | }); 60 | ``` 61 | 62 | 如果队列的长度也超过限制值,这里可以通过`refuse`选项控制,是直接传递异常拒绝服务还是继续排队。默认情况`refuse`为`false`。如果设置为`true`,新的异步调用将会得到`TooMuchAsyncCallError`异常,而被拒绝服务。 63 | 64 | ``` 65 | var bagpipe = new BagPipe(10, { 66 | refuse: true 67 | }); 68 | ``` 69 | 70 | 如果异步调用的超时不可预期,可能存在等待队列不均衡的情况,为此可以全局设置一个超时时间,对于过长的响应时间,提前返回超时状态。 71 | 72 | ``` 73 | var bagpipe = new BagPipe(10, { 74 | timeout: 1000 75 | }); 76 | ``` 77 | 78 | ## 模块状态 79 | 单元测试通过状态:[![Build Status](https://secure.travis-ci.org/JacksonTian/bagpipe.png)](http://travis-ci.org/JacksonTian/bagpipe)。单元测试覆盖率[100%](http://html5ify.com/bagpipe/coverage.html)。 80 | 81 | ## 最佳实践 82 | - 确保异步调用的最后一个参数为回调参数 83 | - 监听`full`事件,以增加你对业务性能的评估 84 | - 目前异步方法未支持上下文。确保异步方法内部没有`this`引用。如果存在`this`引用,请用`bind`方法传递正确的`this`上下文 85 | - 异步调用应当具备timeout的业务处理,无论业务是否完成,总在一定的时间内保证返回 86 | 87 | ## 实际案例 88 | 当你需要遍历文件目录的时候,异步可以确保充分利用IO。你可以轻松发起成千上万个文件的读取。但是,系统文件描述符是有限的。不服的话,遇见下面这个错误再来重读此文。 89 | 90 | ``` 91 | Error: EMFILE, too many open files 92 | ``` 93 | 94 | 也有人会考虑用同步方法来进行处理。但是,同步时,CPU与IO并不能并行利用,一定情况下,性能是不可弃的一项指标。用上`Bagpipe`,可以轻松享受并发,也能限制并发。 95 | 96 | ``` 97 | var bagpipe = new Bagpipe(10); 98 | 99 | var files = ['这里有很多很多文件']; 100 | for (var i = 0; i < files.length; i++) { 101 | // fs.readFile(files[i], 'utf-8', function (err, data) { 102 | bagpipe.push(fs.readFile, files[i], 'utf-8', function (err, data) { 103 | // 不会因为文件描述符过多出错 104 | // 妥妥的 105 | }); 106 | } 107 | ``` 108 | 109 | ## License 110 | 在[MIT](https://github.com/JacksonTian/bagpipe/blob/master/MIT-License)许可证下发布,欢迎享受开源 111 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Bagpipe = require('./lib/bagpipe'); 4 | 5 | Bagpipe.limitify = function (asyncCall, bagpipe) { 6 | return function (...args) { 7 | bagpipe.push(asyncCall, ...args); 8 | }; 9 | }; 10 | 11 | module.exports = Bagpipe; 12 | -------------------------------------------------------------------------------- /lib/bagpipe.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events'); 4 | 5 | function hasOwnProperty(obj, key) { 6 | return Object.prototype.hasOwnProperty.call(obj, key); 7 | } 8 | 9 | /** 10 | * 构造器,传入限流值,设置异步调用最大并发数 11 | * Examples: 12 | * ``` 13 | * var bagpipe = new Bagpipe(100); 14 | * bagpipe.push(fs.readFile, 'path', 'utf-8', function (err, data) { 15 | * // TODO 16 | * }); 17 | * ``` 18 | * Events: 19 | * - `full`, 当活动异步达到限制值时,后续异步调用将被暂存于队列中。当队列的长度大于限制值的2倍或100的时候时候,触发`full`事件。事件传递队列长度值。 20 | * - `outdated`, 超时后的异步调用异常返回。 21 | * Options: 22 | * - `disabled`, 禁用限流,测试时用 23 | * - `refuse`, 拒绝模式,排队超过限制值时,新来的调用将会得到`TooMuchAsyncCallError`异常 24 | * - `timeout`, 设置异步调用的时间上线,保证异步调用能够恒定的结束,不至于花费太长时间 25 | * @param {Number} limit 并发数限制值 26 | * @param {Object} options Options 27 | */ 28 | class Bagpipe extends EventEmitter { 29 | constructor(limit, options = {}) { 30 | super(); 31 | 32 | this.limit = limit; 33 | this.active = 0; 34 | this.queue = []; 35 | this.options = { 36 | disabled: false, 37 | refuse: false, 38 | ratio: 1, 39 | timeout: null 40 | }; 41 | 42 | if (typeof options === 'boolean') { 43 | options = { 44 | disabled: options 45 | }; 46 | } 47 | 48 | for (var key in this.options) { 49 | if (hasOwnProperty(options, key)) { 50 | this.options[key] = options[key]; 51 | } 52 | } 53 | 54 | // queue length 55 | this.queueLength = Math.round(this.limit * (this.options.ratio || 1)); 56 | } 57 | 58 | /** 59 | * 推入方法,参数。最后一个参数为回调函数 60 | * @param {Function} method 异步方法 61 | * @param {Mix} args 参数列表,最后一个参数为回调函数。 62 | */ 63 | push(method, ...args) { 64 | if (typeof args[args.length - 1] !== 'function') { 65 | args.push(function () {}); 66 | } 67 | 68 | var callback = args[args.length - 1]; 69 | 70 | if (this.options.disabled || this.limit < 1) { 71 | method(...args); 72 | return this; 73 | } 74 | 75 | // 队列长度也超过限制值时 76 | if (this.queue.length < this.queueLength || !this.options.refuse) { 77 | this.queue.push({ 78 | method: method, 79 | args: args 80 | }); 81 | } else { 82 | var err = new Error('Too much async call in queue'); 83 | err.name = 'TooMuchAsyncCallError'; 84 | callback(err); 85 | } 86 | 87 | if (this.queue.length > 1) { 88 | this.emit('full', this.queue.length); 89 | } 90 | 91 | this.next(); 92 | return this; 93 | } 94 | 95 | /*! 96 | * 继续执行队列中的后续动作 97 | */ 98 | next() { 99 | // 没到限制,或者没有排队 100 | if (this.active >= this.limit || !this.queue.length) { 101 | return; 102 | } 103 | 104 | const {method, args} = this.queue.shift(); 105 | 106 | this.active++; 107 | 108 | const callback = args[args.length - 1]; 109 | var timer = null; 110 | var called = false; 111 | 112 | // inject logic 113 | args[args.length - 1] = (err, ...rest) => { 114 | // anyway, clear the timer 115 | if (timer) { 116 | clearTimeout(timer); 117 | timer = null; 118 | } 119 | 120 | // if timeout, don't execute 121 | if (!called) { 122 | this._next(); 123 | callback(err, ...rest); 124 | } else { 125 | // pass the outdated error 126 | if (err) { 127 | this.emit('outdated', err); 128 | } 129 | } 130 | }; 131 | 132 | var timeout = this.options.timeout; 133 | if (timeout) { 134 | timer = setTimeout(() => { 135 | // set called as true 136 | called = true; 137 | this._next(); 138 | // pass the exception 139 | var err = new Error(timeout + 'ms timeout'); 140 | err.name = 'BagpipeTimeoutError'; 141 | err.data = { 142 | name: method.name, 143 | method: method.toString(), 144 | args: args.slice(0, -1) 145 | }; 146 | callback(err); 147 | }, timeout); 148 | } 149 | 150 | method(...args); 151 | } 152 | 153 | _next() { 154 | this.active--; 155 | this.next(); 156 | } 157 | } 158 | 159 | module.exports = Bagpipe; 160 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bagpipe", 3 | "version": "1.0.0", 4 | "description": "Concurrency limit", 5 | "keywords": [ 6 | "limiter", 7 | "concurrency limit", 8 | "parallel limit" 9 | ], 10 | "main": "index.js", 11 | "author": "Jackson Tian", 12 | "license": "MIT", 13 | "directories": { 14 | "test": "test" 15 | }, 16 | "devDependencies": { 17 | "coveralls": "^3.1.1", 18 | "istanbul": "^0.4.5", 19 | "mocha": "*", 20 | "pedding": "*", 21 | "should": "*", 22 | "travis-cov": "*" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git://github.com/JacksonTian/bagpipe.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/JacksonTian/bagpipe/issues" 30 | }, 31 | "homepage": "https://github.com/JacksonTian/bagpipe#readme", 32 | "scripts": { 33 | "test": "make test" 34 | }, 35 | "files": ["lib", "index.js"] 36 | } 37 | -------------------------------------------------------------------------------- /test/bagpipe.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const should = require('should'); 4 | const pedding = require('pedding'); 5 | const Bagpipe = require('../'); 6 | 7 | describe('bagpipe', function () { 8 | var async = function (ms, callback) { 9 | setTimeout(function () { 10 | callback(null, {}); 11 | }, ms); 12 | }; 13 | 14 | it('constructor', function () { 15 | var bagpipe = new Bagpipe(10); 16 | bagpipe.limit.should.be.equal(10); 17 | bagpipe.queue.should.have.length(0); 18 | bagpipe.active.should.be.equal(0); 19 | bagpipe.options.disabled.should.be.equal(false); 20 | }); 21 | 22 | it('constructor disabled', function () { 23 | var bagpipe = new Bagpipe(10, true); 24 | bagpipe.limit.should.be.equal(10); 25 | bagpipe.queue.should.have.length(0); 26 | bagpipe.active.should.be.equal(0); 27 | bagpipe.options.disabled.should.be.equal(true); 28 | }); 29 | 30 | it('constructor limit less than 1', function (done) { 31 | var bagpipe = new Bagpipe(0); 32 | bagpipe.push(async, 10, function () { 33 | bagpipe.active.should.be.equal(0); 34 | done(); 35 | }); 36 | bagpipe.active.should.be.equal(0); 37 | }); 38 | 39 | it('constructor limit less than 1 for nextTick', function (done) { 40 | var bagpipe = new Bagpipe(0); 41 | bagpipe.push(process.nextTick, function () { 42 | bagpipe.active.should.be.equal(0); 43 | done(); 44 | }); 45 | bagpipe.active.should.be.equal(0); 46 | }); 47 | 48 | it('constructor disabled is true', function (done) { 49 | var bagpipe = new Bagpipe(10, true); 50 | bagpipe.push(async, 10, function () { 51 | bagpipe.active.should.be.equal(0); 52 | done(); 53 | }); 54 | bagpipe.active.should.be.equal(0); 55 | }); 56 | 57 | it('push', function (done) { 58 | var bagpipe = new Bagpipe(5); 59 | bagpipe.limit.should.be.equal(5); 60 | bagpipe.queue.should.have.length(0); 61 | bagpipe.active.should.be.equal(0); 62 | bagpipe.push(async, 10, function () { 63 | bagpipe.active.should.be.equal(0); 64 | done(); 65 | }); 66 | bagpipe.active.should.be.equal(1); 67 | }); 68 | 69 | it('push, async with this', function (done) { 70 | var bagpipe = new Bagpipe(5); 71 | bagpipe.limit.should.be.equal(5); 72 | bagpipe.queue.should.have.length(0); 73 | bagpipe.active.should.be.equal(0); 74 | var context = {value: 10}; 75 | context.async = function (callback) { 76 | this.value--; 77 | var that = this; 78 | process.nextTick(function() { 79 | callback(that.value); 80 | }); 81 | }; 82 | 83 | bagpipe.push(context.async.bind(context), function () { 84 | bagpipe.active.should.be.equal(0); 85 | done(); 86 | }); 87 | bagpipe.active.should.be.equal(1); 88 | }); 89 | 90 | it('push, active should not be above limit', function (done) { 91 | var limit = 5; 92 | var bagpipe = new Bagpipe(limit); 93 | bagpipe.limit.should.be.equal(limit); 94 | bagpipe.queue.should.have.length(0); 95 | bagpipe.active.should.be.equal(0); 96 | var counter = 10; 97 | for (var i = 0; i < counter; i++) { 98 | bagpipe.push(async, 1 + Math.round(Math.random() * 10), function () { 99 | bagpipe.active.should.not.be.above(limit); 100 | counter--; 101 | if (counter === 0) { 102 | done(); 103 | } 104 | }); 105 | bagpipe.active.should.not.be.above(limit); 106 | } 107 | }); 108 | 109 | it('push, disabled, active should not be above limit', function (done) { 110 | var limit = 5; 111 | var bagpipe = new Bagpipe(limit, true); 112 | bagpipe.limit.should.be.equal(limit); 113 | bagpipe.queue.should.have.length(0); 114 | bagpipe.active.should.be.equal(0); 115 | var counter = 10; 116 | for (var i = 0; i < counter; i++) { 117 | bagpipe.push(async, 10 + Math.round(Math.random() * 10), function () { 118 | bagpipe.active.should.be.equal(0); 119 | counter--; 120 | if (counter === 0) { 121 | done(); 122 | } 123 | }); 124 | bagpipe.active.should.be.equal(0); 125 | } 126 | }); 127 | 128 | it('full event should fired when above limit', function (done) { 129 | var limit = 5; 130 | var bagpipe = new Bagpipe(limit); 131 | bagpipe.limit.should.be.equal(limit); 132 | bagpipe.queue.should.have.length(0); 133 | bagpipe.active.should.be.equal(0); 134 | var counter = 0; 135 | bagpipe.on('full', function (length) { 136 | length.should.above(1); 137 | counter++; 138 | }); 139 | 140 | var noop = function () {}; 141 | for (var i = 0; i < 100; i++) { 142 | bagpipe.push(async, 10, noop); 143 | } 144 | counter.should.above(0); 145 | done(); 146 | }); 147 | 148 | it('should support without callback', function (done) { 149 | var limit = 5; 150 | var bagpipe = new Bagpipe(limit); 151 | bagpipe.limit.should.be.equal(limit); 152 | bagpipe.queue.should.have.length(0); 153 | bagpipe.active.should.be.equal(0); 154 | bagpipe.push(async, 10); 155 | bagpipe.active.should.be.equal(1); 156 | done(); 157 | }); 158 | 159 | it('should get TooMuchAsyncCallError', function (done) { 160 | done = pedding(5, done); 161 | var limit = 2; 162 | var bagpipe = new Bagpipe(limit, { 163 | refuse: true 164 | }); 165 | bagpipe.limit.should.be.equal(limit); 166 | bagpipe.queue.should.have.length(0); 167 | bagpipe.active.should.be.equal(0); 168 | for (var i = 0; i < 4; i++) { 169 | bagpipe.push(async, 10, function (err) { 170 | should.not.exist(err); 171 | done(); 172 | }); 173 | } 174 | bagpipe.push(async, 10, function (err) { 175 | should.exist(err); 176 | done(); 177 | }); 178 | bagpipe.active.should.be.equal(2); 179 | }); 180 | 181 | it('should get TooMuchAsyncCallError with ratio', function (done) { 182 | done = pedding(7, done); 183 | var limit = 2; 184 | var bagpipe = new Bagpipe(limit, { 185 | refuse: true, 186 | ratio: 2 187 | }); 188 | bagpipe.limit.should.be.equal(limit); 189 | bagpipe.queue.should.have.length(0); 190 | bagpipe.active.should.be.equal(0); 191 | for (var i = 0; i < 2; i++) { 192 | bagpipe.push(async, 10, function (err) { 193 | should.not.exist(err); 194 | done(); 195 | }); 196 | } 197 | bagpipe.queue.should.have.length(0); 198 | for (i = 0; i < 4; i++) { 199 | bagpipe.push(async, 10, function (err) { 200 | should.not.exist(err); 201 | done(); 202 | }); 203 | } 204 | bagpipe.queue.should.have.length(4); 205 | bagpipe.push(async, 10, function (err) { 206 | should.exist(err); 207 | done(); 208 | }); 209 | bagpipe.active.should.be.equal(2); 210 | bagpipe.queue.should.have.length(4); 211 | }); 212 | 213 | it('should get BagpipeTimeoutError', function (done) { 214 | done = pedding(3, done); 215 | var _async = function _async(ms, callback) { 216 | setTimeout(function () { 217 | callback(null, {ms: ms}); 218 | }, ms); 219 | }; 220 | var _async2 = function _async(ms, callback) { 221 | setTimeout(function () { 222 | callback(new Error('Timeout')); 223 | }, ms); 224 | }; 225 | var bagpipe = new Bagpipe(10, { 226 | timeout: 10 227 | }); 228 | bagpipe.on('outdated', function (err) { 229 | should.exist(err); 230 | done(); 231 | }); 232 | 233 | bagpipe.push(_async, 5, function (err, data) { 234 | should.not.exist(err); 235 | should.exist(data); 236 | data.should.have.property('ms', 5); 237 | done(); 238 | }); 239 | 240 | bagpipe.push(_async2, 15, function (err) { 241 | should.exist(err); 242 | err.name.should.eql('BagpipeTimeoutError'); 243 | err.message.should.eql('10ms timeout'); 244 | err.data.should.have.property('name', '_async'); 245 | err.data.should.have.property('method'); 246 | err.data.args.should.eql([15]); 247 | done(); 248 | }); 249 | }); 250 | }); 251 | --------------------------------------------------------------------------------