├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── appveyor.yml ├── enable-sql-server-tcp-ip.ps1 ├── lib └── connection-pool.js ├── package.json └── test └── test.js /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behavior 4 | 5 | 6 | 7 | ## Current Behavior 8 | 9 | 10 | 11 | ## Steps to Reproduce (for bugs) 12 | 13 | 14 | 15 | 1. 16 | 2. 17 | 3. 18 | 4. 19 | 20 | ## Reason For Request (for feature requests) 21 | 22 | 23 | 24 | 25 | ## Possible Solution 26 | 27 | 28 | 29 | ## Background Details 30 | 31 | 32 | ## Environment 33 | 34 | * Node.js Version: 35 | * Windows/Mac/Linux: 36 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | 11 | 12 | ## Motivation and Context 13 | 14 | 15 | ## How Has This Been Tested? 16 | 17 | 18 | 19 | 20 | ## Types of changes 21 | 22 | - [ ] Bug fix (non-breaking change which fixes an issue) 23 | - [ ] New feature (non-breaking change which adds functionality) 24 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 25 | 26 | ## Checklist: 27 | 28 | 29 | - [ ] My code follows the code style of this project. 30 | - [ ] My change requires a change to the documentation. 31 | - [ ] I have updated the documentation accordingly. 32 | - [ ] I have read the **CONTRIBUTING** document. 33 | - [ ] I have added tests to cover my changes. 34 | - [ ] All new and existing tests passed. 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules/ 3 | *.sublime-* 4 | .idea/ 5 | /test.js 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Version 1.0.5 2 | * no changes. published to update npm docs 3 | 4 | ### Version 1.0.4 5 | * bug fix only 6 | 7 | ### Version 1.0.3 8 | * Pool modifies the Tedious connection object rather than the Connection prototype. 9 | 10 | ### Version 1.0.2 11 | * Added additional log message when acquiring a connection. 12 | 13 | ### Version 1.0.0 14 | * No changes from v0.3.9. 15 | 16 | ### Version 0.3.9 17 | * bug fix only 18 | 19 | ### Version 0.3.7 20 | * bug fix only 21 | 22 | ### Version 0.3.6 23 | * bug fix only 24 | 25 | ### Version 0.3.5 26 | * `poolConfig` option `min` is limited to less than `max` 27 | 28 | ### Version 0.3.4 29 | * `poolConfig` option `min` supports being set to 0 30 | 31 | ### Version 0.3.3 32 | * Ignore calls to connection.release() on a connection that has been closed or not part of the connection pool. 33 | 34 | ### Version 0.3.2 35 | * Calls connection.reset() when the connection is released to the pool. This is very unlikely to cause anyone trouble. 36 | * Added a callback argument to connectionPool.drain() 37 | 38 | ### Version 0.3.0 39 | * Removed dependency on the `generic-pool` node module. 40 | * Added `poolConfig` options `retryDelay` 41 | * Added `poolConfig` options `aquireTimeout` **(Possibly Breaking)** 42 | * Added `poolConfig` options `log` 43 | * `idleTimeoutMillis` renamed to `idleTimeout` **(Possibly Breaking)** 44 | * The `ConnectionPool` `'error'` event added 45 | * The behavior of the err parameter of the callback passed to `acquire()` has changed. It only returns errors related to acquiring a connection not Tedious Connection errors. Connection errors can happen anytime the pool is being filled and could go unnoticed if only passed the the callback. Subscribe to the `'error'` event on the pool to be notified of all connection errors. **(Possibly Breaking)** 46 | * `PooledConnection` object removed. 47 | 48 | ### Version 0.2.x 49 | * To acquire a connection, call on `acquire()` on a `ConnectionPool` rather than `requestConnection()`. **(Breaking)** 50 | * After acquiring a `PooledConnection`, do not wait for the `'connected'` event. The connection is received connected. **(Breaking)** 51 | * Call `release()` on a `PooledConnection` to release the it back to the pool. `close()` permanently closes the connection (as `close()` behaves in in tedious). **(Breaking)** 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to tedious-connection-pool 2 | 3 | Looking to contribute something to tedious-connection-pool? Here's how you can help. 4 | 5 | 6 | ## Bugs reports 7 | 8 | A bug is a _demonstrable problem_ that is caused by the code in the 9 | repository. Good bug reports are extremely helpful – thank you! 10 | 11 | Guidelines for bug reports: 12 | 13 | 1. **Use the GitHub issue search** — check if the issue has already been 14 | reported. 15 | 16 | 2. **Check if the issue has been fixed** — try to reproduce it using the 17 | latest `master` or development branch in the repository. 18 | 19 | 3. **Isolate the problem** — ideally create a reduced test 20 | case and a live example. 21 | 22 | 4. Please try to be as detailed as possible in your report. Include specific 23 | information about the environment – operating system and version, browser 24 | and version, version of tedious-connection-pool – and steps required to reproduce 25 | the issue. 26 | 27 | 28 | ## Feature requests & contribution enquiries 29 | 30 | Feature requests are welcome. But take a moment to find out whether your idea 31 | fits with the scope and aims of the project. It's up to *you* to make a strong 32 | case for the inclusion of your feature. Please provide as much detail and 33 | context as possible. 34 | 35 | Contribution enquiries should take place before any significant pull request, 36 | otherwise you risk spending a lot of time working on something that we might 37 | have good reasons for rejecting. 38 | 39 | 40 | ## Pull requests 41 | 42 | Good pull requests—patches, improvements, new features—are a fantastic 43 | help. They should remain focused in scope and avoid containing unrelated 44 | commits. 45 | 46 | Make sure to adhere to the coding conventions used throughout the codebase 47 | (indentation, accurate comments, etc.). Please run `npm test` before you push: 48 | this will run the mocha unit tests (using karma runner). 49 | 50 | Please follow this process; it's the best way to get your work included in the 51 | project: 52 | 53 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 54 | and configure the remotes: 55 | 56 | ```bash 57 | # Clone your fork of the repo into the current directory 58 | git clone https://github.com//tedious-connection-pool 59 | # Navigate to the newly cloned directory 60 | cd 61 | # Assign the original repo to a remote called "upstream" 62 | git remote add upstream git://github.com/pekim/tedious-connection-pool 63 | ``` 64 | 65 | 2. If you cloned a while ago, get the latest changes from upstream: 66 | 67 | ```bash 68 | git checkout master 69 | git pull upstream master 70 | ``` 71 | 72 | 3. Install the dependencies and create a new topic branch (off the master 73 | branch) to contain your feature, change, or fix: 74 | 75 | ```bash 76 | npm install 77 | git checkout -b 78 | ``` 79 | 80 | 4. Make sure to update, or add to the tests when appropriate. Patches and 81 | features will not be accepted without tests. Run `make test` to check that 82 | all tests pass after you've made changes. 83 | 84 | 5. Commit your changes in logical chunks. Provide clear and explanatory commit 85 | messages. Use Git's [interactive rebase](https://help.github.com/articles/interactive-rebase) 86 | feature to tidy up your commits before making them public. 87 | 88 | 6. Locally merge (or rebase) the upstream development branch into your topic branch: 89 | 90 | ```bash 91 | git pull [--rebase] upstream master 92 | ``` 93 | 94 | 7. Push your topic branch up to your fork: 95 | 96 | ```bash 97 | git push origin 98 | ``` 99 | 100 | 8. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 101 | with a clear title and description. 102 | 103 | 9. If you are asked to amend your changes before they can be merged in, please 104 | use `git commit --amend` (or rebasing for multi-commit Pull Requests) and 105 | force push to your remote feature branch. You may also be asked to squash 106 | commits. 107 | 108 | ## License 109 | 110 | By contributing your code, 111 | 112 | You agree to license your contribution under the terms of the MIT License 113 | https://opensource.org/licenses/MIT 114 | 115 | ## Notes 116 | Thanks to the [Flight](https://github.com/flightjs/flight) project for this excellent CONTRIBUTING.md file. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Mike D Pilsbury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tedious-connection-pool 2 | [![Dependency Status](https://david-dm.org/tediousjs/tedious-connection-pool.svg)](https://david-dm.org/tediousjs/tedious-connection-pool) 3 | [![npm version](https://badge.fury.io/js/tedious-connection-pool.svg)](https://badge.fury.io/js/tedious-connection-pool) 4 | [![Build status](https://ci.appveyor.com/api/projects/status/jnurb48ao1wrbgbr?svg=true)](https://ci.appveyor.com/project/ben-page/tedious-connection-pool) 5 | 6 | 7 | A connection pool for [tedious](http://github.com/tediousjs/tedious). 8 | 9 | ## Installation 10 | 11 | npm install tedious-connection-pool 12 | 13 | ## Description 14 | The only difference from the regular tedious API is how the connection is obtained and released. Rather than creating a connection and then closing it when finished, acquire a connection from the pool and release it when finished. Releasing resets the connection and makes in available for another use. 15 | 16 | Once the Tedious Connection object has been acquired, the tedious API can be used with the connection as normal. 17 | 18 | ## Example 19 | 20 | ```javascript 21 | var ConnectionPool = require('tedious-connection-pool'); 22 | var Request = require('tedious').Request; 23 | 24 | var poolConfig = { 25 | min: 2, 26 | max: 4, 27 | log: true 28 | }; 29 | 30 | var connectionConfig = { 31 | userName: 'login', 32 | password: 'password', 33 | server: 'localhost' 34 | }; 35 | 36 | //create the pool 37 | var pool = new ConnectionPool(poolConfig, connectionConfig); 38 | 39 | pool.on('error', function(err) { 40 | console.error(err); 41 | }); 42 | 43 | //acquire a connection 44 | pool.acquire(function (err, connection) { 45 | if (err) { 46 | console.error(err); 47 | return; 48 | } 49 | 50 | //use the connection as normal 51 | var request = new Request('select 42', function(err, rowCount) { 52 | if (err) { 53 | console.error(err); 54 | return; 55 | } 56 | 57 | console.log('rowCount: ' + rowCount); 58 | 59 | //release the connection back to the pool when finished 60 | connection.release(); 61 | }); 62 | 63 | request.on('row', function(columns) { 64 | console.log('value: ' + columns[0].value); 65 | }); 66 | 67 | connection.execSql(request); 68 | }); 69 | ``` 70 | 71 | When you are finished with the pool, you can drain it (close all connections). 72 | ```javascript 73 | pool.drain(); 74 | ``` 75 | 76 | 77 | ## Class: ConnectionPool 78 | 79 | ### new ConnectionPool(poolConfig, connectionConfig) 80 | 81 | * `poolConfig` {Object} the pool configuration object 82 | * `min` {Number} The minimum of connections there can be in the pool. Default = `10` 83 | * `max` {Number} The maximum number of connections there can be in the pool. Default = `50` 84 | * `idleTimeout` {Number} The number of milliseconds before closing an unused connection. Default = `300000` 85 | * `retryDelay` {Number} The number of milliseconds to wait after a connection fails, before trying again. Default = `5000` 86 | * `acquireTimeout` {Number} The number of milliseconds to wait for a connection, before returning an error. Default = `60000` 87 | * `log` {Boolean|Function} Set to true to have debug log written to the console or pass a function to receive the log messages. Default = `undefined` 88 | 89 | * `connectionConfig` {Object} The same configuration that would be used to [create a 90 | tedious Connection](https://tediousjs.github.io/tedious/api-connection.html#function_newConnection). 91 | 92 | ### connectionPool.acquire(callback) 93 | Acquire a Tedious Connection object from the pool. 94 | 95 | * `callback(err, connection)` {Function} Callback function 96 | * `err` {Object} An Error object is an error occurred trying to acquire a connection, otherwise null. 97 | * `connection` {Object} A [Connection](https://tediousjs.github.io/tedious/api-connection.html) 98 | 99 | ### connectionPool.drain(callback) 100 | Close all pooled connections and stop making new ones. The pool should be discarded after it has been drained. 101 | * `callback()` {Function} Callback function 102 | 103 | ### connectionPool.error {event} 104 | The 'error' event is emitted when a connection fails to connect to the SQL Server. The pool will simply retry indefinitely. The application may want to handle errors in a more nuanced way. 105 | 106 | ## Class: Connection 107 | The following method is added to the Tedious [Connection](https://tediousjs.github.io/tedious/api-connection.html) object. 108 | 109 | ### Connection.release() 110 | Release the connect back to the pool to be used again 111 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | 3 | services: mssql2014 4 | 5 | environment: 6 | matrix: 7 | - nodejs_version: "0.10" 8 | - nodejs_version: "0.12" 9 | - nodejs_version: "4" 10 | - nodejs_version: "5" 11 | - nodejs_version: "6" 12 | 13 | branches: 14 | only: 15 | - master 16 | 17 | install: 18 | - ps: Install-Product node $env:nodejs_version 19 | - cmd: npm install 20 | 21 | cache: node_modules 22 | build: off 23 | 24 | before_test: 25 | - ps: Start-Process enable-sql-server-tcp-ip.ps1 26 | 27 | test_script: 28 | - cmd: npm test 29 | -------------------------------------------------------------------------------- /enable-sql-server-tcp-ip.ps1: -------------------------------------------------------------------------------- 1 | [reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | Out-Null 2 | [reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.SqlWmiManagement") | Out-Null 3 | 4 | $instanceName = 'sql2014' 5 | $computerName = $env:COMPUTERNAME 6 | $smo = 'Microsoft.SqlServer.Management.Smo.' 7 | $wmi = New-Object ($smo + 'Wmi.ManagedComputer') 8 | 9 | # For the named instance, on the current computer, for the TCP protocol, 10 | # loop through all the IPs and configure them to use the standard port 11 | # of 1433. 12 | $uri = "ManagedComputer[@Name='$computerName']/ ServerInstance[@Name='$instanceName']/ServerProtocol[@Name='Tcp']" 13 | $Tcp = $wmi.GetSmoObject($uri) 14 | ForEach ($ipAddress in $Tcp.IPAddresses) 15 | { 16 | $ipAddress.IPAddressProperties["TcpDynamicPorts"].Value = "" 17 | $ipAddress.IPAddressProperties["TcpPort"].Value = "1433" 18 | } 19 | $Tcp.IsEnabled = $true 20 | $Tcp.Alter() 21 | 22 | # Start services 23 | Set-Service SQLBrowser -StartupType Manual 24 | Start-Service SQLBrowser 25 | Restart-Service "MSSQL`$$instanceName" -------------------------------------------------------------------------------- /lib/connection-pool.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Connection = require('tedious').Connection; 3 | var EventEmitter = require('events').EventEmitter; 4 | var util = require('util'); 5 | 6 | function release() { 7 | this.pool.release(this); 8 | } 9 | 10 | var PENDING = 0; 11 | var FREE = 1; 12 | var USED = 2; 13 | var RETRY = 3; 14 | 15 | function ConnectionPool(poolConfig, connectionConfig) { 16 | this.connections = []; //open connections of any state 17 | this.waiting = []; //acquire() callbacks that are waiting for a connection to come available 18 | this.connectionConfig = connectionConfig; 19 | 20 | this.max = poolConfig.max || 50; 21 | 22 | this.min = Math.min(this.max, poolConfig.min >= 0 ? poolConfig.min : 10); 23 | 24 | this.idleTimeout = !poolConfig.idleTimeout && poolConfig.idleTimeout !== false 25 | ? 300000 //5 min 26 | : poolConfig.idleTimeout; 27 | 28 | this.retryDelay = !poolConfig.retryDelay && poolConfig.retryDelay !== false 29 | ? 5000 30 | : poolConfig.retryDelay; 31 | 32 | this.acquireTimeout = !poolConfig.acquireTimeout && poolConfig.acquireTimeout !== false 33 | ? 60000 //1 min 34 | : poolConfig.acquireTimeout; 35 | 36 | if (poolConfig.log) { 37 | if (Object.prototype.toString.call(poolConfig.log) == '[object Function]') 38 | this.log = poolConfig.log; 39 | else { 40 | this.log = function(text) { 41 | console.log('Tedious-Connection-Pool: ' + text); 42 | }; 43 | } 44 | } else { 45 | this.log = function() {}; 46 | } 47 | 48 | this.drained = false; 49 | 50 | setTimeout(fill.bind(this), 4); 51 | } 52 | 53 | util.inherits(ConnectionPool, EventEmitter); 54 | 55 | var cid = 1; 56 | 57 | function createConnection(pooled) { 58 | if (this.drained) //pool has been drained 59 | return; 60 | 61 | var self = this; 62 | var endHandler; 63 | 64 | this.log('creating connection: ' + cid); 65 | var connection = new Connection(this.connectionConfig); 66 | connection.release = release; 67 | connection.pool = this; 68 | if (pooled) { 69 | pooled.id = cid++; 70 | pooled.con = connection; 71 | pooled.status = PENDING; 72 | } else { 73 | pooled = { 74 | id: cid++, 75 | con: connection, 76 | status: PENDING 77 | }; 78 | 79 | this.connections.push(pooled); 80 | } 81 | 82 | var handleError = function(err) { 83 | self.log('connection closing because of error'); 84 | 85 | connection.removeListener('end', endHandler); 86 | pooled.status = RETRY; 87 | pooled.con = undefined; 88 | if (pooled.timeout) 89 | clearTimeout(pooled.timeout); 90 | 91 | pooled.timeout = setTimeout(createConnection.bind(self, pooled), self.retryDelay); 92 | self.emit('error', err); 93 | }; 94 | 95 | connection.on('connect', function (err) { 96 | self.log('connection connected: ' + pooled.id); 97 | if (self.drained) { //pool has been drained 98 | self.log('connection closing because pool is drained'); 99 | connection.close(); 100 | return; 101 | } 102 | 103 | if (err) { 104 | handleError(err); 105 | return; 106 | } 107 | 108 | var waiter = self.waiting.shift(); 109 | if (waiter !== undefined) 110 | setUsed.call(self, pooled, waiter); 111 | else 112 | setFree.call(self, pooled); 113 | }); 114 | 115 | connection.on('error', handleError); 116 | 117 | endHandler = function () { 118 | self.log('connection ended: ' + pooled.id); 119 | if (self.drained) //pool has been drained 120 | return; 121 | 122 | for (var i = self.connections.length - 1; i >= 0; i--) { 123 | if (self.connections[i].con === connection) { 124 | self.connections.splice(i, 1); 125 | fill.call(self); 126 | return; 127 | } 128 | } 129 | }; 130 | 131 | connection.on('end', endHandler); 132 | } 133 | 134 | function fill() { 135 | if (this.drained) //pool has been drained 136 | return; 137 | 138 | var available = 0; 139 | for (var i = this.connections.length - 1; i >= 0; i--) { 140 | if (this.connections[i].status !== USED) { 141 | available++; 142 | } 143 | } 144 | 145 | var amount = Math.min( 146 | this.max - this.connections.length, //max that can be created 147 | this.waiting.length - available); //how many are needed, minus how many are available 148 | 149 | amount = Math.max( 150 | this.min - this.connections.length, //amount to create to reach min 151 | amount); 152 | 153 | if (amount > 0) 154 | this.log('filling pool with ' + amount); 155 | 156 | for (i = 0; i < amount; i++) 157 | createConnection.call(this); 158 | } 159 | 160 | ConnectionPool.prototype.acquire = function (callback) { 161 | if (this.drained) //pool has been drained 162 | return; 163 | 164 | var self = this; 165 | var free; 166 | 167 | //look for free connection 168 | var l = this.connections.length; 169 | for (var i = 0; i < l; i++) { 170 | var pooled = this.connections[i]; 171 | 172 | if (pooled.status === FREE) { 173 | free = pooled; 174 | break; 175 | } 176 | } 177 | 178 | var waiter = { 179 | callback: callback 180 | }; 181 | 182 | if (free === undefined) { //no valid connection found 183 | if (this.acquireTimeout) { 184 | 185 | waiter.timeout = setTimeout(function () { 186 | for (var i = self.waiting.length - 1; i >= 0; i--) { 187 | var waiter2 = self.waiting[i]; 188 | 189 | if (waiter2.timeout === waiter.timeout) { 190 | self.waiting.splice(i, 1); 191 | waiter.callback(new Error('Acquire Timeout Exceeded')); 192 | return; 193 | } 194 | } 195 | }, this.acquireTimeout); 196 | } 197 | 198 | this.waiting.push(waiter); 199 | fill.call(this); 200 | } else { 201 | setUsed.call(this, free, waiter); 202 | } 203 | }; 204 | 205 | function setUsed(pooled, waiter) { 206 | pooled.status = USED; 207 | if (pooled.timeout) { 208 | clearTimeout(pooled.timeout); 209 | pooled.timeout = undefined; 210 | } 211 | if (waiter.timeout) { 212 | clearTimeout(waiter.timeout); 213 | waiter.timeout = undefined; 214 | } 215 | this.log('acquired connection ' + pooled.id); 216 | waiter.callback(null, pooled.con); 217 | } 218 | 219 | function setFree(pooled) { 220 | var self = this; 221 | 222 | pooled.status = FREE; 223 | pooled.timeout = setTimeout(function() { 224 | self.log('closing idle connection: ' + pooled.id); 225 | pooled.con.close(); 226 | }, this.idleTimeout); 227 | } 228 | 229 | ConnectionPool.prototype.release = function(connection) { 230 | if (this.drained) //pool has been drained 231 | return; 232 | 233 | var self = this; 234 | var i, pooled; 235 | 236 | for (i = self.connections.length - 1; i >= 0; i--) { 237 | pooled = self.connections[i]; 238 | 239 | if (pooled.con === connection) { 240 | //reset connection & release it 241 | connection.reset(function (err) { 242 | if (!pooled.con) //the connection failed during the reset 243 | return; 244 | 245 | if (err) { //there is an error, don't reuse the connection, just close it 246 | pooled.con.close(); 247 | return; 248 | } 249 | self.log('connection reset: ' + pooled.id); 250 | 251 | var waiter = self.waiting.shift(); 252 | 253 | if (waiter !== undefined) { 254 | setUsed.call(self, pooled, waiter); 255 | //if (waiter.timeout) 256 | // clearTimeout(waiter.timeout); 257 | //waiter.callback(null, connection); 258 | } else { 259 | setFree.call(self, pooled); 260 | } 261 | }); 262 | 263 | return; 264 | } 265 | } 266 | }; 267 | 268 | ConnectionPool.prototype.drain = function (callback) { 269 | this.log('draining pool'); 270 | if (this.drained) {//pool has been drained 271 | if (callback) 272 | callback(); 273 | return; 274 | } 275 | 276 | this.drained = true; 277 | 278 | for (i = this.waiting.length - 1; i >= 0; i--) { 279 | var waiter = this.waiting[i]; 280 | 281 | if (waiter.timeout) 282 | clearTimeout(waiter.timeout); 283 | } 284 | 285 | this.waiting = null; 286 | 287 | var eventTotal = this.connections.length; 288 | var eventCount = 0; 289 | 290 | if (eventTotal === 0 && callback) 291 | callback(); 292 | 293 | var ended = function() { 294 | if (++eventCount === eventTotal && callback) 295 | callback(); 296 | }; 297 | 298 | for (var i = this.connections.length - 1; i >= 0; i--) { 299 | var pooled = this.connections[i]; 300 | 301 | if (pooled.timeout) 302 | clearTimeout(pooled.timeout); 303 | 304 | if (pooled.con) { 305 | pooled.con.on('end', ended); 306 | pooled.con.close(); 307 | } else { 308 | ended(); 309 | } 310 | } 311 | 312 | this.connections = null; 313 | }; 314 | 315 | module.exports = ConnectionPool; 316 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tedious-connection-pool", 3 | "version": "1.0.5", 4 | "description": "Connection Pool for tedious.", 5 | "main": "lib/connection-pool.js", 6 | "scripts": { 7 | "test": "node_modules/.bin/mocha --expose-gc test/test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/tediousjs/tedious-connection-pool.git" 12 | }, 13 | "keywords": [ 14 | "tedious", 15 | "connection", 16 | "pool" 17 | ], 18 | "author": { 19 | "name": "Ben Page", 20 | "email": "ben.page@openreign.com" 21 | }, 22 | "contributors": [ 23 | { 24 | "name": "Ben Page", 25 | "email": "ben.page@openreign.com" 26 | }, 27 | { 28 | "name": "Mike D Pilsbury", 29 | "email": "mike.pilsbury@gmail.com" 30 | } 31 | ], 32 | "license": "MIT", 33 | "dependencies": { 34 | "tedious": "^1.14.0" 35 | }, 36 | "devDependencies": { 37 | "mocha": "^3.0.2", 38 | "simple-statistics": "^2.1.0" 39 | }, 40 | "readmeFilename": "README.md", 41 | "bugs": { 42 | "url": "https://github.com/tediousjs/tedious-connection-pool/issues" 43 | }, 44 | "homepage": "https://github.com/tediousjs/tedious-connection-pool" 45 | } 46 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var assert = require('assert'); 3 | var Request = require('tedious').Request; 4 | var ConnectionPool = require('../lib/connection-pool'); 5 | var Connection = require('tedious').Connection; 6 | 7 | var connectionConfig, timeout; 8 | 9 | if (process.env.APPVEYOR) { 10 | timeout = 30000; 11 | connectionConfig = { 12 | server: 'localhost', 13 | userName: 'sa', 14 | password: 'Password12!', 15 | options: { 16 | appName: 'pool-test', 17 | database: 'master', 18 | requestTimeout: 25000, 19 | cryptoCredentialsDetails: { 20 | ciphers: 'RC4-MD5' 21 | } 22 | } 23 | }; 24 | } else { 25 | timeout = 10000; 26 | connectionConfig = { 27 | userName: 'test', 28 | password: 'test', 29 | server: 'dev1', 30 | options: { 31 | appName: 'pool-test', 32 | database: 'test' 33 | } 34 | }; 35 | } 36 | 37 | /* create a db user with the correct permissions: 38 | CREATE DATABASE test 39 | CREATE LOGIN test WITH PASSWORD=N'test', DEFAULT_DATABASE=test, CHECK_POLICY=OFF 40 | GRANT ALTER ANY CONNECTION TO test 41 | 42 | USE test 43 | CREATE USER test FOR LOGIN test WITH DEFAULT_SCHEMA=dbo 44 | ALTER ROLE db_owner ADD MEMBER test 45 | 46 | USE msdb 47 | CREATE USER test FOR LOGIN test WITH DEFAULT_SCHEMA=dbo 48 | ALTER ROLE SQLAgentOperatorRole ADD MEMBER test 49 | ALTER ROLE SQLAgentReaderRole ADD MEMBER test 50 | ALTER ROLE SQLAgentUserRole ADD MEMBER test 51 | */ 52 | 53 | /* disable the user when not testing: 54 | ALTER LOGIN test DISABLE 55 | */ 56 | 57 | describe('Name Collision', function () { 58 | 59 | it('release', function () { 60 | assert(!Connection.prototype.release); 61 | 62 | var con = new Connection({}); 63 | assert(!con.release); 64 | con.close(); 65 | }); 66 | 67 | it('pool', function () { 68 | assert(!Connection.prototype.pool); 69 | 70 | var con = new Connection({}); 71 | assert(!con.pool); 72 | con.close(); 73 | }); 74 | }); 75 | 76 | describe('ConnectionPool', function () { 77 | 78 | it('min', function (done) { 79 | this.timeout(timeout); 80 | 81 | var poolConfig = {min: 2}; 82 | var pool = new ConnectionPool(poolConfig, connectionConfig); 83 | 84 | setTimeout(function() { 85 | assert.equal(pool.connections.length, poolConfig.min); 86 | pool.drain(done); 87 | }, 4); 88 | }); 89 | 90 | it('min=0', function (done) { 91 | this.timeout(timeout); 92 | 93 | var poolConfig = {min: 0, idleTimeout: 10}; 94 | var pool = new ConnectionPool(poolConfig, connectionConfig); 95 | 96 | setTimeout(function() { 97 | assert.equal(pool.connections.length, 0); 98 | }, 4); 99 | 100 | setTimeout(function() { 101 | pool.acquire(function(err, connection) { 102 | assert(!err); 103 | 104 | var request = new Request('select 42', function (err, rowCount) { 105 | assert.strictEqual(rowCount, 1); 106 | connection.release(); 107 | setTimeout(function () { 108 | assert.equal(pool.connections.length, 0); 109 | pool.drain(done); 110 | }, 200); 111 | }); 112 | 113 | request.on('row', function (columns) { 114 | assert.strictEqual(columns[0].value, 42); 115 | }); 116 | 117 | connection.execSql(request); 118 | }); 119 | }, 2000); 120 | }); 121 | 122 | it('max', function (done) { 123 | this.timeout(timeout); 124 | 125 | var poolConfig = {min: 2, max: 5}; 126 | var pool = new ConnectionPool(poolConfig, connectionConfig); 127 | 128 | var count = 20; 129 | var run = 0; 130 | 131 | var createRequest = function (err, connection) { 132 | assert(!err); 133 | 134 | var request = new Request('select 42', function (err, rowCount) { 135 | assert.strictEqual(rowCount, 1); 136 | setTimeout(function() { 137 | run++; 138 | assert(pool.connections.length <= poolConfig.max); 139 | if (run === count) { 140 | pool.drain(done); 141 | return; 142 | } 143 | connection.release(); 144 | }, 200); 145 | }); 146 | 147 | request.on('row', function (columns) { 148 | assert.strictEqual(columns[0].value, 42); 149 | }); 150 | 151 | connection.execSql(request); 152 | }; 153 | 154 | for (var i = 0; i < count; i++) { 155 | setTimeout(function() { 156 | pool.acquire(createRequest); 157 | }, 1); 158 | } 159 | }); 160 | 161 | it('min<=max, min specified > max specified', function (done) { 162 | this.timeout(timeout); 163 | 164 | var poolConfig = { min: 5, max: 1, idleTimeout: 10}; 165 | var pool = new ConnectionPool(poolConfig, connectionConfig); 166 | 167 | setTimeout(function() { 168 | assert.equal(pool.connections.length, 1); 169 | }, 4); 170 | 171 | setTimeout(function() { 172 | pool.acquire(function(err, connection) { 173 | assert(!err); 174 | 175 | var request = new Request('select 42', function (err, rowCount) { 176 | assert.strictEqual(rowCount, 1); 177 | connection.release(); 178 | setTimeout(function () { 179 | assert.equal(pool.connections.length, 1); 180 | pool.drain(done); 181 | }, 200); 182 | }); 183 | 184 | request.on('row', function (columns) { 185 | assert.strictEqual(columns[0].value, 42); 186 | }); 187 | 188 | connection.execSql(request); 189 | }); 190 | }, 2000); 191 | }); 192 | 193 | it('min<=max, no min specified', function (done) { 194 | this.timeout(timeout); 195 | 196 | var poolConfig = {max: 1, idleTimeout: 10}; 197 | var pool = new ConnectionPool(poolConfig, connectionConfig); 198 | 199 | setTimeout(function() { 200 | assert.equal(pool.connections.length, 1); 201 | }, 4); 202 | 203 | setTimeout(function() { 204 | pool.acquire(function(err, connection) { 205 | assert(!err); 206 | 207 | var request = new Request('select 42', function (err, rowCount) { 208 | assert.strictEqual(rowCount, 1); 209 | connection.release(); 210 | setTimeout(function () { 211 | assert.equal(pool.connections.length, 1); 212 | pool.drain(done); 213 | }, 200); 214 | }); 215 | 216 | request.on('row', function (columns) { 217 | assert.strictEqual(columns[0].value, 42); 218 | }); 219 | 220 | connection.execSql(request); 221 | }); 222 | }, 2000); 223 | }); 224 | 225 | it('pool error event', function (done) { 226 | this.timeout(timeout); 227 | var poolConfig = {min: 3}; 228 | var pool = new ConnectionPool(poolConfig, {}); 229 | 230 | pool.acquire(function() { }); 231 | 232 | pool.on('error', function(err) { 233 | assert(!!err); 234 | pool.drain(done); 235 | }); 236 | }); 237 | 238 | it('connection retry', function (done) { 239 | this.timeout(timeout); 240 | var poolConfig = {min: 1, max: 5, retryDelay: 5}; 241 | var pool = new ConnectionPool(poolConfig, {}); 242 | 243 | pool.on('error', function(err) { 244 | assert(!!err); 245 | pool.connectionConfig = connectionConfig; 246 | }); 247 | 248 | function testConnected() { 249 | for (var i = pool.connections.length - 1; i >= 0; i--) { 250 | if (pool.connections[i].status === 3/*RETRY*/) { 251 | setTimeout(testConnected, 100); 252 | return; 253 | } 254 | } 255 | 256 | assert.equal(pool.connections.length, poolConfig.min); 257 | pool.drain(done); 258 | } 259 | 260 | setTimeout(testConnected, 100); 261 | }); 262 | 263 | it('acquire timeout', function (done) { 264 | this.timeout(timeout); 265 | 266 | var poolConfig = {min: 1, max: 1, acquireTimeout: 2000}; 267 | var pool = new ConnectionPool(poolConfig, connectionConfig); 268 | 269 | pool.acquire(function(err, connection) { 270 | assert(!err); 271 | assert(!!connection); 272 | }); 273 | 274 | pool.acquire(function(err, connection) { 275 | assert(!!err); 276 | assert(!connection); 277 | done(); 278 | }); 279 | }); 280 | 281 | it('idle timeout', function (done) { 282 | this.timeout(timeout); 283 | var poolConfig = {min: 1, max: 5, idleTimeout: 100}; 284 | var pool = new ConnectionPool(poolConfig, connectionConfig); 285 | 286 | setTimeout(function() { 287 | pool.acquire(function (err, connection) { 288 | assert(!err); 289 | 290 | var request = new Request('select 42', function (err, rowCount) { 291 | assert.strictEqual(rowCount, 1); 292 | pool.drain(done); 293 | }); 294 | 295 | request.on('row', function (columns) { 296 | assert.strictEqual(columns[0].value, 42); 297 | }); 298 | 299 | connection.execSql(request); 300 | }); 301 | 302 | }, 300); 303 | }); 304 | 305 | it('connection error handling', function (done) { 306 | this.timeout(timeout); 307 | var poolConfig = {min: 1, max: 5}; 308 | 309 | var pool = new ConnectionPool(poolConfig, connectionConfig); 310 | 311 | pool.on('error', function(err) { 312 | assert(err && err.name === 'ConnectionError'); 313 | }); 314 | 315 | //This simulates a lost connections by creating a job that kills the current session and then deletesthe job. 316 | pool.acquire(function (err, connection) { 317 | assert(!err); 318 | 319 | var command = 'DECLARE @jobName VARCHAR(68) = \'pool\' + CONVERT(VARCHAR(64),NEWID()), @jobId UNIQUEIDENTIFIER;' + 320 | 'EXECUTE msdb..sp_add_job @jobName, @owner_login_name=\'' + connectionConfig.userName + '\', @job_id=@jobId OUTPUT;' + 321 | 'EXECUTE msdb..sp_add_jobserver @job_id=@jobId;' + 322 | 323 | 'DECLARE @cmd VARCHAR(50);' + 324 | 'SELECT @cmd = \'kill \' + CONVERT(VARCHAR(10), @@SPID);' + 325 | 'EXECUTE msdb..sp_add_jobstep @job_id=@jobId, @step_name=\'Step1\', @command = @cmd, @database_name = \'' + connectionConfig.options.database + '\', @on_success_action = 3;' + 326 | 327 | 'DECLARE @deleteCommand VARCHAR(200);' + 328 | 'SET @deleteCommand = \'execute msdb..sp_delete_job @job_name=\'\'\'+@jobName+\'\'\'\';' + 329 | 'EXECUTE msdb..sp_add_jobstep @job_id=@jobId, @step_name=\'Step2\', @command = @deletecommand;' + 330 | 331 | 'EXECUTE msdb..sp_start_job @job_id=@jobId;' + 332 | 'WAITFOR DELAY \'01:00:00\';' + 333 | 'SELECT 42'; 334 | 335 | var request = new Request(command, function (err, rowCount) { 336 | assert(err); 337 | pool.drain(done); 338 | }); 339 | 340 | request.on('row', function (columns) { 341 | assert(false); 342 | }); 343 | 344 | connection.execSql(request); 345 | }); 346 | }); 347 | 348 | it('release(), reset()', function (done) { 349 | this.timeout(timeout); 350 | 351 | var poolConfig = {max: 1}; 352 | var pool = new ConnectionPool(poolConfig, connectionConfig); 353 | 354 | var createRequest = function(query, value, callback) { 355 | var request = new Request(query, function (err, rowCount) { 356 | assert.strictEqual(rowCount, 1); 357 | callback(); 358 | }); 359 | 360 | request.on('row', function (columns) { 361 | assert.strictEqual(columns[0].value, value); 362 | }); 363 | 364 | return request; 365 | }; 366 | 367 | pool.acquire(function(err, conn) { 368 | assert(!err); 369 | 370 | conn.execSql(createRequest('SELECT 42', 42, function () { 371 | pool.release(conn); //release the connect 372 | 373 | pool.acquire(function (err, conn) { //re-acquire the connection 374 | assert(!err); 375 | 376 | conn.execSql(createRequest('SELECT 42', 42, function () { 377 | 378 | pool.drain(done); 379 | })); 380 | }); 381 | })); 382 | }); 383 | }); 384 | 385 | it('drain', function (done) { 386 | this.timeout(timeout); 387 | 388 | var poolConfig = {min: 3}; 389 | var pool = new ConnectionPool(poolConfig, connectionConfig); 390 | 391 | pool.acquire(function() { }); 392 | 393 | setTimeout(function() { 394 | assert.equal(pool.connections.length, poolConfig.min); 395 | pool.drain(done); 396 | }, 4); 397 | }); 398 | }); 399 | 400 | if (!process.env.APPVEYOR) { 401 | describe('Load Test', function () { 402 | var statistics = require('simple-statistics'); 403 | 404 | it('Memory Leak Detection - Connection Error', function (done) { 405 | this.timeout(60000); 406 | if (!global.gc) 407 | throw new Error('must run nodejs with --expose-gc'); 408 | var count = 0; 409 | var mem = []; 410 | var groupCount = 20; 411 | var poolSize = 1000; 412 | var max = poolSize * groupCount; 413 | 414 | var pool = new ConnectionPool({max: poolSize, min: poolSize, retryDelay: 1}, { 415 | userName: 'testLogin', 416 | password: 'wrongPassword', 417 | server: 'localhost' 418 | }); 419 | 420 | pool.on('error', function () { 421 | if ((++count % poolSize) !== 0) 422 | return; 423 | 424 | global.gc(); 425 | 426 | var heapUsedKB = Math.round(process.memoryUsage().heapUsed / 1024); 427 | mem.push([count, heapUsedKB]); 428 | // console.log(count + ': ' + heapUsedKB + 'KB'); 429 | 430 | if (count === max) { 431 | var data = statistics.linearRegression(mem); 432 | //console.log(data.m); 433 | if (data.m >= 0.025) 434 | done(new Error('Memory leak detected.')); 435 | else 436 | done(); 437 | 438 | pool.drain(); 439 | } 440 | }); 441 | }); 442 | 443 | it('Memory Leak Detection - acquire() and Request', function (done) { 444 | this.timeout(60000); 445 | if (!global.gc) 446 | throw new Error('must run nodejs with --expose-gc'); 447 | 448 | var poolConfig = {min: 67, max: 123}; 449 | var pool = new ConnectionPool(poolConfig, { 450 | userName: 'test', 451 | password: 'test', 452 | server: 'dev1' 453 | }); 454 | 455 | var clients = 1000; 456 | var connections = 100; 457 | var max = clients * connections; 458 | var mem = []; 459 | 460 | for (var i = 0; i < clients; i++) 461 | createClient(); 462 | 463 | var count = 0; 464 | 465 | function end(err) { 466 | done(err); 467 | pool.drain(); 468 | } 469 | 470 | function createClient() { 471 | var clientCount = 0; 472 | 473 | function createRequest() { 474 | pool.acquire(function (err, connection) { 475 | if (err) 476 | return end(err); 477 | 478 | var request = new Request('select 42', function (err) { 479 | if (err) 480 | return end(err); 481 | 482 | connection.release(); 483 | 484 | if (++clientCount < connections) 485 | createRequest(); 486 | 487 | if ((++count % 1000) === 0) { 488 | global.gc(); 489 | 490 | if (count === max) { 491 | var data = statistics.linearRegression(mem); 492 | //console.log(data.m); 493 | if (data.m >= 0.025) 494 | end(new Error('Memory leak not detected.')); 495 | else 496 | end(); 497 | } else { 498 | var heapUsedKB = Math.round(process.memoryUsage().heapUsed / 1024); 499 | mem.push([count, heapUsedKB]); 500 | // console.log(count + ': ' + heapUsedKB + 'KB'); 501 | } 502 | } 503 | }); 504 | 505 | connection.execSql(request); 506 | }); 507 | } 508 | 509 | createRequest(); 510 | } 511 | }); 512 | }); 513 | } --------------------------------------------------------------------------------